From 54c74a6a7488a2d3975d3b61e02fa8b712784602 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 27 Jun 2023 16:44:10 +0100 Subject: [PATCH 001/366] Text area experiments --- poetry.lock | 14 ++++++- pyproject.toml | 1 + src/textual/widgets/_text_area.py | 67 +++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/textual/widgets/_text_area.py diff --git a/poetry.lock b/poetry.lock index fcde137db2..4e7d4037ec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1001,6 +1001,14 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "tree-sitter" +version = "0.20.1" +description = "Python bindings to the Tree-sitter parsing library" +category = "main" +optional = false +python-versions = ">=3.3" + [[package]] name = "typed-ast" version = "1.5.4" @@ -1118,7 +1126,7 @@ dev = ["aiohttp", "click", "msgpack"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "54585998c7e97c8766b5daa9411b49616e014168eeeed5120390ebc19d102b15" +content-hash = "27d651fcaa49e9921f51604a7364a4025c8bb485649b3dc24878c08d542249d9" [metadata.files] aiohttp = [ @@ -2131,6 +2139,10 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +tree-sitter = [ + {file = "tree_sitter-0.20.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:6f11a1fd909dcf569e7b1d98861a837436799e757bbbc5cd5280989050929e12"}, + {file = "tree_sitter-0.20.1.tar.gz", hash = "sha256:e93f082c545d6649bcfb5d681ed255eb004a6ce22988971a128f40692feec60d"}, +] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, diff --git a/pyproject.toml b/pyproject.toml index 25285a195e..52964fe612 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ typing-extensions = "^4.4.0" aiohttp = { version = ">=3.8.1", optional = true } click = {version = ">=8.1.2", optional = true} msgpack = { version = ">=1.0.3", optional = true } +tree-sitter = "^0.20.1" [tool.poetry.extras] dev = ["aiohttp", "click", "msgpack"] diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py new file mode 100644 index 0000000000..00748e7501 --- /dev/null +++ b/src/textual/widgets/_text_area.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from pathlib import Path + +from tree_sitter import Language, Parser + +from textual.strip import Strip +from textual.widget import Widget + + +class TextArea(Widget): + def __init__( + self, + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + ) -> None: + super().__init__(name=name, id=id, classes=classes, disabled=disabled) + + # Load the tree-sitter libraries + + def render_line(self, y: int) -> Strip: + pass + + +if __name__ == "__main__": + # Language.build_library( + # '../../../build/textual-languages.so', + # [ + # 'tree-sitter-libraries/tree-sitter-python' + # ] + # ) + this_directory = Path(__file__).parent + languages = this_directory / "../../../build/textual-languages.so" + python_language = Language(languages.resolve(), "python") + + parser = Parser() + parser.set_language(python_language) + + tree = parser.parse( + bytes( + """\ + def foo(): + if bar: + baz() + """, + "utf8", + ) + ) + + def traverse(cursor): + # Start with the first child + if cursor.goto_first_child(): + print(cursor.node) + traverse(cursor) + + # Continue with the siblings + while cursor.goto_next_sibling(): + print(cursor.node) + traverse(cursor) + + # Go back up to the parent when done + cursor.goto_parent() + + # Start traversal with the root of the tree + traverse(tree.walk()) From 1fed4bbc817140eb4e5847d5bd464ca18efd1124 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 28 Jun 2023 09:10:29 +0100 Subject: [PATCH 002/366] Rendering text using the line API --- src/textual/widgets/_text_area.py | 69 +++++++++++++++++-- tests/text_area/test_text_area_tree_sitter.py | 0 2 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 tests/text_area/test_text_area_tree_sitter.py diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 00748e7501..05676d7fc4 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -2,13 +2,42 @@ from pathlib import Path +from rich.segment import Segment from tree_sitter import Language, Parser +from textual._cells import cell_len +from textual.reactive import Reactive, reactive +from textual.scroll_view import ScrollView from textual.strip import Strip -from textual.widget import Widget +LANGUAGES_PATH = Path(__file__) / "../../../tree-sitter-languages/textual-languages.so" +SAMPLE_TEXT = [ + "Hello, world!", + "", + "你好,世界!", # Chinese characters, which are usually double-width + "こんにちは、世界!", # Japanese characters, also usually double-width + "안녕하세요, 세계!", # Korean characters, also usually double-width + " This line has leading white space", + "This line has trailing white space ", + " This line has both leading and trailing white space ", + " ", # Line with only spaces + "こんにちは、world! 你好,world!", # Mixed script line + "Hello, 🌍! Hello, 🌏! Hello, 🌎!", # Line with emoji (which are often double-width) + "The quick brown 🦊 jumps over the lazy 🐶.", # Line with emoji interspersed in text + "Special characters: ~!@#$%^&*()_+`-={}|[]\\:\";'<>?,./", + # Line with special characters + "Unicode example: Привет, мир!", # Russian text + "Unicode example: Γειά σου Κόσμε!", # Greek text + "Unicode example: مرحبا بك في", # Arabic text +] + + +class TextArea(ScrollView): + language: Reactive[str | None] = reactive(None) + """The language to use for syntax highlighting (via tree-sitter).""" + cursor_position = reactive((0, 0)) + """The cursor position (zero-based line_index, offset).""" -class TextArea(Widget): def __init__( self, name: str | None = None, @@ -17,22 +46,48 @@ def __init__( disabled: bool = False, ) -> None: super().__init__(name=name, id=id, classes=classes, disabled=disabled) + self.parser: Parser | None = None + """The tree-sitter parser which extracts the syntax tree from the document.""" + self.document_lines: list[str] = SAMPLE_TEXT.copy() + """Each string in this list represents a line in the document.""" + + def watch_language(self, new_language: str | None) -> None: + """Update the language used in AST parsing. + + When the language reactive string is updated, fetch the Language definition + from our tree-sitter library file. If the language reactive is set to None, + then the no parser is used.""" + if new_language: + language = Language(LANGUAGES_PATH.resolve(), new_language) + parser = Parser() + parser.set_language(language) + self.parser = parser + else: + self.parser = None + + def render_line(self, widget_y: int) -> Strip: + document_lines = self.document_lines - # Load the tree-sitter libraries + document_y = round(self.scroll_y + widget_y) + out_of_bounds = document_y >= len(document_lines) + if out_of_bounds: + return Strip.blank(self.size.width) - def render_line(self, y: int) -> Strip: - pass + # TODO For now, we naively just pull the line from the document based on + # y_offset. This will later need to be adjusted to account for wrapping. + line = document_lines[document_y] + return Strip([Segment(line)], cell_len(line)) if __name__ == "__main__": # Language.build_library( - # '../../../build/textual-languages.so', + # '../../../tree-sitter-languages/textual-languages.so', # [ # 'tree-sitter-libraries/tree-sitter-python' # ] # ) this_directory = Path(__file__).parent - languages = this_directory / "../../../build/textual-languages.so" + languages = this_directory / "../../../tree-sitter-languages/textual-languages.so" python_language = Language(languages.resolve(), "python") parser = Parser() diff --git a/tests/text_area/test_text_area_tree_sitter.py b/tests/text_area/test_text_area_tree_sitter.py new file mode 100644 index 0000000000..e69de29bb2 From 7cb5e8a39a35bcab8787a73e1d29470afbbca9c7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 28 Jun 2023 09:25:00 +0100 Subject: [PATCH 003/366] Fix the languages path --- .../{_text_area.py => _text_editor.py} | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) rename src/textual/widgets/{_text_area.py => _text_editor.py} (72%) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_editor.py similarity index 72% rename from src/textual/widgets/_text_area.py rename to src/textual/widgets/_text_editor.py index 05676d7fc4..af9fc7d43f 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_editor.py @@ -6,33 +6,17 @@ from tree_sitter import Language, Parser from textual._cells import cell_len +from textual.geometry import Size from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView from textual.strip import Strip -LANGUAGES_PATH = Path(__file__) / "../../../tree-sitter-languages/textual-languages.so" -SAMPLE_TEXT = [ - "Hello, world!", - "", - "你好,世界!", # Chinese characters, which are usually double-width - "こんにちは、世界!", # Japanese characters, also usually double-width - "안녕하세요, 세계!", # Korean characters, also usually double-width - " This line has leading white space", - "This line has trailing white space ", - " This line has both leading and trailing white space ", - " ", # Line with only spaces - "こんにちは、world! 你好,world!", # Mixed script line - "Hello, 🌍! Hello, 🌏! Hello, 🌎!", # Line with emoji (which are often double-width) - "The quick brown 🦊 jumps over the lazy 🐶.", # Line with emoji interspersed in text - "Special characters: ~!@#$%^&*()_+`-={}|[]\\:\";'<>?,./", - # Line with special characters - "Unicode example: Привет, мир!", # Russian text - "Unicode example: Γειά σου Κόσμε!", # Greek text - "Unicode example: مرحبا بك في", # Arabic text -] - - -class TextArea(ScrollView): +LANGUAGES_PATH = ( + Path(__file__) / "../../../../tree-sitter-languages/textual-languages.so" +) + + +class TextEditor(ScrollView): language: Reactive[str | None] = reactive(None) """The language to use for syntax highlighting (via tree-sitter).""" cursor_position = reactive((0, 0)) @@ -48,7 +32,7 @@ def __init__( super().__init__(name=name, id=id, classes=classes, disabled=disabled) self.parser: Parser | None = None """The tree-sitter parser which extracts the syntax tree from the document.""" - self.document_lines: list[str] = SAMPLE_TEXT.copy() + self.document_lines: list[str] = [] """Each string in this list represents a line in the document.""" def watch_language(self, new_language: str | None) -> None: @@ -65,6 +49,20 @@ def watch_language(self, new_language: str | None) -> None: else: self.parser = None + def open_text(self, text: str) -> None: + """Load text from a string into the editor.""" + lines = text.splitlines(keepends=False) + self.open_lines(lines) + + def open_lines(self, lines: list[str]) -> None: + """Load text from a list of lines into the editor.""" + self.document_lines = lines + + # TODO Offer maximum line width and wrap if needed + width = max(cell_len(line) for line in lines) + height = len(lines) + self.virtual_size = Size(width, height) + def render_line(self, widget_y: int) -> Strip: document_lines = self.document_lines From a33ad1db01cdec5f12f6daf8ecfb1dc05156ec33 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 28 Jun 2023 09:30:26 +0100 Subject: [PATCH 004/366] Some method renaming --- src/textual/widgets/_text_editor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index af9fc7d43f..4df00c70dc 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -49,12 +49,12 @@ def watch_language(self, new_language: str | None) -> None: else: self.parser = None - def open_text(self, text: str) -> None: + def load_text(self, text: str) -> None: """Load text from a string into the editor.""" lines = text.splitlines(keepends=False) - self.open_lines(lines) + self.load_lines(lines) - def open_lines(self, lines: list[str]) -> None: + def load_lines(self, lines: list[str]) -> None: """Load text from a list of lines into the editor.""" self.document_lines = lines From 6cc666ff2ed2f08e73a215f0fa8abc7f9462b216 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 28 Jun 2023 09:54:59 +0100 Subject: [PATCH 005/366] Method for fully constructing AST --- src/textual/widgets/_text_editor.py | 42 ++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 4df00c70dc..eaba4d2ee6 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -3,7 +3,7 @@ from pathlib import Path from rich.segment import Segment -from tree_sitter import Language, Parser +from tree_sitter import Language, Parser, Tree from textual._cells import cell_len from textual.geometry import Size @@ -30,11 +30,17 @@ def __init__( disabled: bool = False, ) -> None: super().__init__(name=name, id=id, classes=classes, disabled=disabled) - self.parser: Parser | None = None - """The tree-sitter parser which extracts the syntax tree from the document.""" + + # --- Core editor data self.document_lines: list[str] = [] """Each string in this list represents a line in the document.""" + # --- Abstract syntax tree and related parsing machinery + self.parser: Parser | None = None + """The tree-sitter parser which extracts the syntax tree from the document.""" + self.ast: Tree | None = None + """The tree-sitter Tree (AST) built from the document.""" + def watch_language(self, new_language: str | None) -> None: """Update the language used in AST parsing. @@ -44,14 +50,38 @@ def watch_language(self, new_language: str | None) -> None: if new_language: language = Language(LANGUAGES_PATH.resolve(), new_language) parser = Parser() - parser.set_language(language) self.parser = parser + self.parser.set_language(language) + self.ast = self._full_tree_build(parser, self.document_lines) + else: + self.ast = None + + def _full_tree_build( + self, + parser: Parser, + document_lines: list[str], + ) -> Tree | None: + """Fully parse the document and build the abstract syntax tree for it. + + Returns None if there's no parser available (e.g. when no language is selected). + """ + + document_lines = self.document_lines + + def read_callable(byte_offset, point): + row, column = point + if row >= len(document_lines) or column >= len([row]): + return None + return document_lines[row][column:].encode("utf8") + + if self.parser: + return parser.parse(read_callable) else: - self.parser = None + return None def load_text(self, text: str) -> None: """Load text from a string into the editor.""" - lines = text.splitlines(keepends=False) + lines = text.splitlines(keepends=True) self.load_lines(lines) def load_lines(self, lines: list[str]) -> None: From 72c0b79c3679434f095db3a52fe68f14384dd6a4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 28 Jun 2023 16:46:31 +0100 Subject: [PATCH 006/366] Progress --- src/textual/widgets/_text_editor.py | 147 ++++++++++++++++++---------- 1 file changed, 94 insertions(+), 53 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index eaba4d2ee6..e2731b8812 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -1,11 +1,16 @@ from __future__ import annotations +from collections import defaultdict from pathlib import Path +from typing import Iterable from rich.segment import Segment -from tree_sitter import Language, Parser, Tree +from rich.style import Style +from tree_sitter import Language, Node, Parser, Tree +from textual import log from textual._cells import cell_len +from textual.binding import Binding from textual.geometry import Size from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView @@ -16,7 +21,11 @@ ) -class TextEditor(ScrollView): +class TextEditor(ScrollView, can_focus=True): + BINDINGS = [ + Binding("ctrl+l", "print_line_cache", "[debug] Line Cache"), + ] + language: Reactive[str | None] = reactive(None) """The language to use for syntax highlighting (via tree-sitter).""" cursor_position = reactive((0, 0)) @@ -35,10 +44,18 @@ def __init__( self.document_lines: list[str] = [] """Each string in this list represents a line in the document.""" + self._line_cache: dict[int, list[Segment]] = defaultdict(list) + """Caches segments for lines. Note that a line may span multiple y-offsets + due to wrapping. These segments do NOT include the cursor highlighting. + A portion of the line cache will be updated when an edit operation occurs + or when a file is loaded for the first time. + Tree sitter will tell us the modified ranges of the AST and we update + the corresponding line ranges in this cache.""" + # --- Abstract syntax tree and related parsing machinery - self.parser: Parser | None = None + self._parser: Parser | None = None """The tree-sitter parser which extracts the syntax tree from the document.""" - self.ast: Tree | None = None + self._ast: Tree | None = None """The tree-sitter Tree (AST) built from the document.""" def watch_language(self, new_language: str | None) -> None: @@ -50,13 +67,15 @@ def watch_language(self, new_language: str | None) -> None: if new_language: language = Language(LANGUAGES_PATH.resolve(), new_language) parser = Parser() - self.parser = parser - self.parser.set_language(language) - self.ast = self._full_tree_build(parser, self.document_lines) + self._parser = parser + self._parser.set_language(language) + self._ast = self._build_ast(parser, self.document_lines) else: - self.ast = None + self._ast = None + + log.debug(f"parser set to {self._parser}") - def _full_tree_build( + def _build_ast( self, parser: Parser, document_lines: list[str], @@ -66,15 +85,13 @@ def _full_tree_build( Returns None if there's no parser available (e.g. when no language is selected). """ - document_lines = self.document_lines - def read_callable(byte_offset, point): row, column = point - if row >= len(document_lines) or column >= len([row]): + if row >= len(document_lines) or column >= len(document_lines[row]): return None return document_lines[row][column:].encode("utf8") - if self.parser: + if parser: return parser.parse(read_callable) else: return None @@ -93,6 +110,13 @@ def load_lines(self, lines: list[str]) -> None: height = len(lines) self.virtual_size = Size(width, height) + # TODO - clear caches + if self._parser is not None: + self._ast = self._build_ast(self._parser, lines) + self._cache_highlights(self._ast.walk(), lines) + + log.debug(f"loaded text. parser = {self._parser} ast = {self._ast}") + def render_line(self, widget_y: int) -> Strip: document_lines = self.document_lines @@ -101,50 +125,67 @@ def render_line(self, widget_y: int) -> Strip: if out_of_bounds: return Strip.blank(self.size.width) - # TODO For now, we naively just pull the line from the document based on - # y_offset. This will later need to be adjusted to account for wrapping. - line = document_lines[document_y] - return Strip([Segment(line)], cell_len(line)) + # Fetch the segments from the cache + strip = Strip(self._line_cache.get(document_y, Strip.blank(self.size.width))) + return strip + + def _cache_highlights( + self, + cursor, + document: list[str], + line_range: tuple[int, int] | None = None, + ) -> None: + """Traverse the AST and highlight the document. + + Args: + cursor: The tree-sitter Tree cursor. + document: The document as a list of strings. + line_range: The start and end line index that is visible. If None, highlight the whole document. + """ + # The range of the document (line indices) that we want to highlight. + if line_range is not None: + window_start, window_end = line_range + else: + window_start = 0 + window_end = len(document) - 1 + # Get the range of this node + node_start_line = cursor.node.start_point[0] + node_end_line = cursor.node.end_point[0] -if __name__ == "__main__": - # Language.build_library( - # '../../../tree-sitter-languages/textual-languages.so', - # [ - # 'tree-sitter-libraries/tree-sitter-python' - # ] - # ) - this_directory = Path(__file__).parent - languages = this_directory / "../../../tree-sitter-languages/textual-languages.so" - python_language = Language(languages.resolve(), "python") - - parser = Parser() - parser.set_language(python_language) - - tree = parser.parse( - bytes( - """\ - def foo(): - if bar: - baz() - """, - "utf8", + node_in_window = line_range is None or ( + window_start <= node_end_line and window_end >= node_start_line ) - ) - def traverse(cursor): - # Start with the first child - if cursor.goto_first_child(): - print(cursor.node) - traverse(cursor) + # Apply simple highlighting to the node based on its type. + # if cursor.node.type == 'identifier': + # style = Style(color="black", bgcolor="red") + # elif cursor.node.type == 'string': + # style = Style(color="green", italic=True) + # else: + # style = Style.null() - # Continue with the siblings - while cursor.goto_next_sibling(): - print(cursor.node) - traverse(cursor) + def action_print_line_cache(self) -> None: + log.debug(self._line_cache) - # Go back up to the parent when done - cursor.goto_parent() + # TODO - this traversal is correct - see notes in Notion + def traverse(cursor) -> Iterable[Node]: + yield cursor.node - # Start traversal with the root of the tree - traverse(tree.walk()) + if cursor.goto_first_child(): + yield from traverse(cursor) + while cursor.goto_next_sibling(): + yield from traverse(cursor) + cursor.goto_parent() + + log.debug(list(traverse(self._ast.walk()))) + + +if __name__ == "__main__": + pass +# Language.build_library( +# '../../../tree-sitter-languages/textual-languages.so', +# [ +# 'tree-sitter-libraries/tree-sitter-python' +# ] +# ) From 9199f96cb2916aafee12d90bafb8d4c3f2d7e679 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 11:32:08 +0100 Subject: [PATCH 007/366] Cache highlights --- src/textual/widgets/_text_editor.py | 159 ++++++++++++++++++++++++---- 1 file changed, 137 insertions(+), 22 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index e2731b8812..763d4b7ee2 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -2,10 +2,11 @@ from collections import defaultdict from pathlib import Path -from typing import Iterable +from typing import Iterable, NamedTuple from rich.segment import Segment from rich.style import Style +from rich.text import Text from tree_sitter import Language, Node, Parser, Tree from textual import log @@ -21,9 +22,18 @@ ) +class Highlight(NamedTuple): + """A range to highlight within a single line""" + + start_column: int | None + end_column: int | None + node_type: str + + class TextEditor(ScrollView, can_focus=True): BINDINGS = [ - Binding("ctrl+l", "print_line_cache", "[debug] Line Cache"), + Binding("ctrl+s", "print_highlight_cache", "[debug] Print highlight cache"), + Binding("ctrl+l", "print_line_cache", "[debug] Print line cache"), ] language: Reactive[str | None] = reactive(None) @@ -44,6 +54,10 @@ def __init__( self.document_lines: list[str] = [] """Each string in this list represents a line in the document.""" + self._highlight_cache: dict[int, set[Highlight]] = defaultdict(set) + """Mapping line numbers to the set of cached highlights for that line.""" + + # TODO - currently unused self._line_cache: dict[int, list[Segment]] = defaultdict(list) """Caches segments for lines. Note that a line may span multiple y-offsets due to wrapping. These segments do NOT include the cursor highlighting. @@ -87,7 +101,11 @@ def _build_ast( def read_callable(byte_offset, point): row, column = point - if row >= len(document_lines) or column >= len(document_lines[row]): + row_out_of_bounds = row >= len(document_lines) + column_out_of_bounds = not row_out_of_bounds and column >= len( + document_lines[row] + ) + if row_out_of_bounds or column_out_of_bounds: return None return document_lines[row][column:].encode("utf8") @@ -125,10 +143,36 @@ def render_line(self, widget_y: int) -> Strip: if out_of_bounds: return Strip.blank(self.size.width) - # Fetch the segments from the cache - strip = Strip(self._line_cache.get(document_y, Strip.blank(self.size.width))) + # Get the line from the document and apply highlighting. + highlights = self._highlight_cache[document_y] + line_string = document_lines[document_y].replace("\n", "").replace("\r", "") + line_text = Text(line_string, end="") + + # Apply the highlights to the ranges + for start, end, node_type in highlights: + node_style = self._get_node_style(node_type) + line_text.stylize(node_style, start, end) + + segments = self.app.console.render(line_text) + strip = ( + Strip(segments, line_text.cell_len) + .adjust_cell_length(self.size.width) + .simplify() + ) + log.debug(f"{document_y}|{repr(strip.text)}|") + return strip + def _get_node_style(self, node_type: str) -> Style: + # Apply simple highlighting to the node based on its type. + if node_type == "identifier": + style = Style(color="black", bgcolor="red") + elif node_type == "string": + style = Style(color="green", italic=True) + else: + style = Style.null() + return style + def _cache_highlights( self, cursor, @@ -150,20 +194,44 @@ def _cache_highlights( window_end = len(document) - 1 # Get the range of this node - node_start_line = cursor.node.start_point[0] - node_end_line = cursor.node.end_point[0] + node_start_row, node_start_column = cursor.node.start_point + node_end_row, node_end_column = cursor.node.end_point node_in_window = line_range is None or ( - window_start <= node_end_line and window_end >= node_start_line + window_start <= node_end_row and window_end >= node_start_row ) - # Apply simple highlighting to the node based on its type. - # if cursor.node.type == 'identifier': - # style = Style(color="black", bgcolor="red") - # elif cursor.node.type == 'string': - # style = Style(color="green", italic=True) - # else: - # style = Style.null() + # Cache the highlight data for this node if it's within the window range + # At this point we're not actually looking at the document at all, we're + # just storing data on the locations to highlight within the document. + # This data will be referenced only when we render. + log.debug(f"Processing highlights for node {cursor.node}") + if node_in_window: + highlight_cache = self._highlight_cache + node_type = cursor.node.type + if node_start_row == node_end_row: + highlight = Highlight(node_start_column, node_end_column, node_type) + highlight_cache[node_start_row].add(highlight) + else: + # Add the first line + highlight_cache[node_start_row].add( + Highlight(node_start_column, None, node_type) + ) + # Add the middle lines - entire row of this node is highlighted + for node_row in range(node_start_row + 1, node_end_row): + highlight_cache[node_row].add(Highlight(0, None, node_type)) + + # Add the last line + highlight_cache[node_end_row].add( + Highlight(0, node_end_column, node_type) + ) + + # Recurse to children + if cursor.goto_first_child(): + self._cache_highlights(cursor, document, line_range) + while cursor.goto_next_sibling(): + self._cache_highlights(cursor, document, line_range) + cursor.goto_parent() def action_print_line_cache(self) -> None: log.debug(self._line_cache) @@ -180,12 +248,59 @@ def traverse(cursor) -> Iterable[Node]: log.debug(list(traverse(self._ast.walk()))) + def action_print_highlight_cache(self) -> None: + log.debug(self._highlight_cache) + if __name__ == "__main__": - pass -# Language.build_library( -# '../../../tree-sitter-languages/textual-languages.so', -# [ -# 'tree-sitter-libraries/tree-sitter-python' -# ] -# ) + + def traverse_tree(cursor): + reached_root = False + while reached_root == False: + yield cursor.node + + if cursor.goto_first_child(): + continue + + if cursor.goto_next_sibling(): + continue + + retracing = True + while retracing: + if not cursor.goto_parent(): + retracing = False + reached_root = True + + if cursor.goto_next_sibling(): + retracing = False + + language = Language(LANGUAGES_PATH.resolve(), "python") + parser = Parser() + parser.set_language(language) + + CODE = """\ + from textual.app import App + + + class ScreenApp(App): + def on_mount(self) -> None: + self.screen.styles.background = "darkblue" + self.screen.styles.border = ("heavy", "white") + + + if __name__ == "__main__": + app = ScreenApp() + app.run() + """ + + document_lines = CODE.splitlines(keepends=False) + + def read_callable(byte_offset, point): + row, column = point + if row >= len(document_lines) or column >= len(document_lines[row]): + return None + return document_lines[row][column:].encode("utf8") + + tree = parser.parse(bytes(CODE, "utf-8")) + + print(list(traverse_tree(tree.walk()))) From e32d77174ee36c992647b65345373da1c103a0fd Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 11:39:56 +0100 Subject: [PATCH 008/366] Checking AST highlighting --- src/textual/widgets/_text_editor.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 763d4b7ee2..f39798b2de 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -166,9 +166,17 @@ def render_line(self, widget_y: int) -> Strip: def _get_node_style(self, node_type: str) -> Style: # Apply simple highlighting to the node based on its type. if node_type == "identifier": - style = Style(color="black", bgcolor="red") + style = Style(color="cyan") elif node_type == "string": - style = Style(color="green", italic=True) + style = Style(color="green") + elif node_type == "block": + style = Style(bgcolor="#434343") + elif node_type == "if_statement": + style = Style(bgcolor="#636363") + elif node_type == "class_definition": + style = Style(bgcolor="#820912") + elif node_type == "call": + style = Style(bgcolor="blue") else: style = Style.null() return style From 9c73beb0cb2b5f465ab01b812fa474ca20f60d3e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 13:10:48 +0100 Subject: [PATCH 009/366] Fixing some rendering issues --- src/textual/widgets/_text_editor.py | 138 +++++++++++++++------------- 1 file changed, 75 insertions(+), 63 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index f39798b2de..db700f41b7 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -54,7 +54,7 @@ def __init__( self.document_lines: list[str] = [] """Each string in this list represents a line in the document.""" - self._highlight_cache: dict[int, set[Highlight]] = defaultdict(set) + self._highlights: dict[int, set[Highlight]] = defaultdict(set) """Mapping line numbers to the set of cached highlights for that line.""" # TODO - currently unused @@ -143,21 +143,26 @@ def render_line(self, widget_y: int) -> Strip: if out_of_bounds: return Strip.blank(self.size.width) - # Get the line from the document and apply highlighting. - highlights = self._highlight_cache[document_y] line_string = document_lines[document_y].replace("\n", "").replace("\r", "") line_text = Text(line_string, end="") - # Apply the highlights to the ranges - for start, end, node_type in highlights: - node_style = self._get_node_style(node_type) - line_text.stylize(node_style, start, end) - - segments = self.app.console.render(line_text) + # Apply highlighting to the line if necessary. + if self._highlights: + highlights = self._highlights[document_y] + for start, end, node_type in highlights: + node_style = self._get_node_style(node_type) + line_text.stylize(node_style, start, end) + + # We need to render according to the virtual size otherwise the rendering + # will wrap the text content incorrectly. + segments = self.app.console.render( + line_text, self.app.console.options.update_width(self.virtual_size.width) + ) strip = ( - Strip(segments, line_text.cell_len) - .adjust_cell_length(self.size.width) - .simplify() + Strip(segments) + .crop(int(self.scroll_x), int(self.scroll_x) + self.content_size.width) + .adjust_cell_length(self.content_size.width) + # .simplify() ) log.debug(f"{document_y}|{repr(strip.text)}|") @@ -169,14 +174,8 @@ def _get_node_style(self, node_type: str) -> Style: style = Style(color="cyan") elif node_type == "string": style = Style(color="green") - elif node_type == "block": - style = Style(bgcolor="#434343") - elif node_type == "if_statement": - style = Style(bgcolor="#636363") - elif node_type == "class_definition": - style = Style(bgcolor="#820912") - elif node_type == "call": - style = Style(bgcolor="blue") + elif node_type == "import_from_statement": + style = Style(bgcolor="magenta") else: style = Style.null() return style @@ -194,52 +193,65 @@ def _cache_highlights( document: The document as a list of strings. line_range: The start and end line index that is visible. If None, highlight the whole document. """ - # The range of the document (line indices) that we want to highlight. - if line_range is not None: - window_start, window_end = line_range - else: - window_start = 0 - window_end = len(document) - 1 - # Get the range of this node - node_start_row, node_start_column = cursor.node.start_point - node_end_row, node_end_column = cursor.node.end_point - - node_in_window = line_range is None or ( - window_start <= node_end_row and window_end >= node_start_row - ) + reached_root = False - # Cache the highlight data for this node if it's within the window range - # At this point we're not actually looking at the document at all, we're - # just storing data on the locations to highlight within the document. - # This data will be referenced only when we render. - log.debug(f"Processing highlights for node {cursor.node}") - if node_in_window: - highlight_cache = self._highlight_cache - node_type = cursor.node.type - if node_start_row == node_end_row: - highlight = Highlight(node_start_column, node_end_column, node_type) - highlight_cache[node_start_row].add(highlight) + while not reached_root: + # The range of the document (line indices) that we want to highlight. + if line_range is not None: + window_start, window_end = line_range else: - # Add the first line - highlight_cache[node_start_row].add( - Highlight(node_start_column, None, node_type) - ) - # Add the middle lines - entire row of this node is highlighted - for node_row in range(node_start_row + 1, node_end_row): - highlight_cache[node_row].add(Highlight(0, None, node_type)) - - # Add the last line - highlight_cache[node_end_row].add( - Highlight(0, node_end_column, node_type) - ) - - # Recurse to children + window_start = 0 + window_end = len(document) - 1 + + # Get the range of this node + node_start_row, node_start_column = cursor.node.start_point + node_end_row, node_end_column = cursor.node.end_point + + node_in_window = line_range is None or ( + window_start <= node_end_row and window_end >= node_start_row + ) + + # Cache the highlight data for this node if it's within the window range + # At this point we're not actually looking at the document at all, we're + # just storing data on the locations to highlight within the document. + # This data will be referenced only when we render. + log.debug(f"Processing highlights for node {cursor.node}") + + if node_in_window: + highlight_cache = self._highlights + node_type = cursor.node.type + if node_start_row == node_end_row: + highlight = Highlight(node_start_column, node_end_column, node_type) + highlight_cache[node_start_row].add(highlight) + else: + # Add the first line + highlight_cache[node_start_row].add( + Highlight(node_start_column, None, node_type) + ) + # Add the middle lines - entire row of this node is highlighted + for node_row in range(node_start_row + 1, node_end_row): + highlight_cache[node_row].add(Highlight(0, None, node_type)) + + # Add the last line + highlight_cache[node_end_row].add( + Highlight(0, node_end_column, node_type) + ) + if cursor.goto_first_child(): - self._cache_highlights(cursor, document, line_range) - while cursor.goto_next_sibling(): - self._cache_highlights(cursor, document, line_range) - cursor.goto_parent() + continue + + if cursor.goto_next_sibling(): + continue + + retracing = True + while retracing: + if not cursor.goto_parent(): + retracing = False + reached_root = True + + if cursor.goto_next_sibling(): + retracing = False def action_print_line_cache(self) -> None: log.debug(self._line_cache) @@ -257,7 +269,7 @@ def traverse(cursor) -> Iterable[Node]: log.debug(list(traverse(self._ast.walk()))) def action_print_highlight_cache(self) -> None: - log.debug(self._highlight_cache) + log.debug(self._highlights) if __name__ == "__main__": From d66d0dd21d163b30f4784c3684af47016120781a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 13:12:09 +0100 Subject: [PATCH 010/366] Adjusting document width/height correctly --- src/textual/widgets/_text_editor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index db700f41b7..5b823e00e9 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -160,9 +160,9 @@ def render_line(self, widget_y: int) -> Strip: ) strip = ( Strip(segments) - .crop(int(self.scroll_x), int(self.scroll_x) + self.content_size.width) - .adjust_cell_length(self.content_size.width) - # .simplify() + .adjust_cell_length(self.virtual_size.width) + .crop(int(self.scroll_x), int(self.scroll_x) + self.virtual_size.width) + .simplify() ) log.debug(f"{document_y}|{repr(strip.text)}|") From ce003493e77817ad6ac0444133b7ce6475141480 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 14:02:46 +0100 Subject: [PATCH 011/366] Painting the cursor --- src/textual/widgets/_text_editor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 5b823e00e9..81553f0be4 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -153,6 +153,11 @@ def render_line(self, widget_y: int) -> Strip: node_style = self._get_node_style(node_type) line_text.stylize(node_style, start, end) + # Show the cursor if necessary + cursor_row, cursor_column = self.cursor_position + if cursor_row == document_y: + line_text.stylize(Style(reverse=True), cursor_column, cursor_column + 1) + # We need to render according to the virtual size otherwise the rendering # will wrap the text content incorrectly. segments = self.app.console.render( From 9ee0f8d82ec7bc6badea1f5732173b300e3ff167 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 14:42:07 +0100 Subject: [PATCH 012/366] Cursor left and right navigation --- src/textual/widgets/_text_editor.py | 107 ++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 6 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 81553f0be4..93eb79fdc2 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -9,10 +9,10 @@ from rich.text import Text from tree_sitter import Language, Node, Parser, Tree -from textual import log +from textual import events, log from textual._cells import cell_len from textual.binding import Binding -from textual.geometry import Size +from textual.geometry import Size, clamp from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView from textual.strip import Strip @@ -32,6 +32,12 @@ class Highlight(NamedTuple): class TextEditor(ScrollView, can_focus=True): BINDINGS = [ + # Cursor movement + Binding("up", "cursor_up", "cursor up", show=False), + Binding("down", "cursor_down", "cursor down", show=False), + Binding("left", "cursor_left", "cursor left", show=False), + Binding("right", "cursor_right", "cursor right", show=False), + # Debugging bindings Binding("ctrl+s", "print_highlight_cache", "[debug] Print highlight cache"), Binding("ctrl+l", "print_line_cache", "[debug] Print line cache"), ] @@ -144,7 +150,7 @@ def render_line(self, widget_y: int) -> Strip: return Strip.blank(self.size.width) line_string = document_lines[document_y].replace("\n", "").replace("\r", "") - line_text = Text(line_string, end="") + line_text = Text(f"{line_string} ", end="") # Apply highlighting to the line if necessary. if self._highlights: @@ -156,7 +162,9 @@ def render_line(self, widget_y: int) -> Strip: # Show the cursor if necessary cursor_row, cursor_column = self.cursor_position if cursor_row == document_y: - line_text.stylize(Style(reverse=True), cursor_column, cursor_column + 1) + line_text.stylize( + Style(color="black", bgcolor="white"), cursor_column, cursor_column + 1 + ) # We need to render according to the virtual size otherwise the rendering # will wrap the text content incorrectly. @@ -165,8 +173,8 @@ def render_line(self, widget_y: int) -> Strip: ) strip = ( Strip(segments) - .adjust_cell_length(self.virtual_size.width) - .crop(int(self.scroll_x), int(self.scroll_x) + self.virtual_size.width) + .adjust_cell_length(self.virtual_size.width - 1) + .crop(int(self.scroll_x), int(self.scroll_x) + self.virtual_size.width - 1) .simplify() ) log.debug(f"{document_y}|{repr(strip.text)}|") @@ -258,6 +266,93 @@ def _cache_highlights( if cursor.goto_next_sibling(): retracing = False + # --- Key handling + async def _on_key(self, event: events.Key) -> None: + if event.is_printable: + event.stop() + assert event.character is not None + + # --- Reactive watchers and validators + # def validate_cursor_position(self, new_position: tuple[int, int]) -> tuple[int, int]: + # new_row, new_column = new_position + # clamped_row = clamp(new_row, 0, len(self.document_lines) - 1) + # clamped_column = clamp(new_column, 0, len(self.document_lines[clamped_row]) - 1) + # return clamped_row, clamped_column + + def watch_cursor_position(self, new_position: tuple[int, int]) -> None: + log.debug(f"cursor_position = {new_position!r}") + + # --- Cursor utilities + @property + def cursor_at_first_row(self) -> bool: + return self.cursor_position[0] == 0 + + @property + def cursor_at_last_row(self) -> bool: + return self.cursor_position[0] == len(self.document_lines) - 1 + + @property + def cursor_at_start_of_row(self) -> bool: + return self.cursor_position[1] == 0 + + @property + def cursor_at_end_of_row(self) -> bool: + cursor_row, cursor_column = self.cursor_position + row_length = len(self.document_lines[cursor_row]) + cursor_at_end = cursor_column == row_length - 1 + return cursor_at_end + + @property + def cursor_at_start_of_document(self) -> bool: + return self.cursor_at_first_row and self.cursor_at_start_of_row + + @property + def cursor_at_end_of_document(self) -> bool: + """True if the cursor is at the very end of the document.""" + return self.cursor_at_last_row and self.cursor_at_end_of_row + + # ------ Cursor movement actions + def action_cursor_left(self) -> None: + """Move the cursor one position to the left. + + If the cursor is at the left edge of the document, try to move it to + the end of the previous line. + """ + if self.cursor_at_start_of_document: + return + + cursor_row, cursor_column = self.cursor_position + length_of_row_above = len(self.document_lines[cursor_row - 1]) + + target_row = cursor_row if cursor_column != 0 else cursor_row - 1 + target_column = ( + cursor_column - 1 if cursor_column != 0 else length_of_row_above - 1 + ) + + self.cursor_position = (target_row, target_column) + + def action_cursor_right(self) -> None: + """Move the cursor one position to the right. + + If the cursor is at the end of a line, attempt to go to the start of the next line. + """ + if self.cursor_at_end_of_document: + return + + cursor_row, cursor_column = self.cursor_position + + print(f"action_cursor_right {self.cursor_position!r}") + target_row = cursor_row + 1 if self.cursor_at_end_of_row else cursor_row + target_column = 0 if self.cursor_at_end_of_row else cursor_column + 1 + + self.cursor_position = (target_row, target_column) + + # --- Editor operations + def insert_text_at_cursor(self, text: str) -> None: + pass + + # --- Debug actions + def action_print_line_cache(self) -> None: log.debug(self._line_cache) From 1efc3237e3fa21706a3c61a8057753d935668c33 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 15:06:15 +0100 Subject: [PATCH 013/366] Adding a repr for debugging --- src/textual/widgets/_text_editor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 93eb79fdc2..dc32cc5084 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -177,7 +177,6 @@ def render_line(self, widget_y: int) -> Strip: .crop(int(self.scroll_x), int(self.scroll_x) + self.virtual_size.width - 1) .simplify() ) - log.debug(f"{document_y}|{repr(strip.text)}|") return strip @@ -229,8 +228,6 @@ def _cache_highlights( # At this point we're not actually looking at the document at all, we're # just storing data on the locations to highlight within the document. # This data will be referenced only when we render. - log.debug(f"Processing highlights for node {cursor.node}") - if node_in_window: highlight_cache = self._highlights node_type = cursor.node.type @@ -356,7 +353,6 @@ def insert_text_at_cursor(self, text: str) -> None: def action_print_line_cache(self) -> None: log.debug(self._line_cache) - # TODO - this traversal is correct - see notes in Notion def traverse(cursor) -> Iterable[Node]: yield cursor.node @@ -371,6 +367,14 @@ def traverse(cursor) -> Iterable[Node]: def action_print_highlight_cache(self) -> None: log.debug(self._highlights) + def __repr__(self): + return f"""\ +cursor {self.cursor_position!r} +language {self.language!r} +document rows {len(self.document_lines)} +highlight cache size {sum(len(highlights) for key, highlights in self._highlights.items())} +current row highlight cache size {len(self._highlights[self.cursor_position[0]])}""" + if __name__ == "__main__": From fb3d04de16956a2af0f17c63901c0ac596543e6c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 15:20:53 +0100 Subject: [PATCH 014/366] Add debugging tools --- src/textual/widgets/_text_editor.py | 44 ++++++++++++++++------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index dc32cc5084..365c9ee64a 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -27,7 +27,7 @@ class Highlight(NamedTuple): start_column: int | None end_column: int | None - node_type: str + node: Node class TextEditor(ScrollView, can_focus=True): @@ -60,7 +60,7 @@ def __init__( self.document_lines: list[str] = [] """Each string in this list represents a line in the document.""" - self._highlights: dict[int, set[Highlight]] = defaultdict(set) + self._highlights: dict[int, list[Highlight]] = defaultdict(list) """Mapping line numbers to the set of cached highlights for that line.""" # TODO - currently unused @@ -180,13 +180,13 @@ def render_line(self, widget_y: int) -> Strip: return strip - def _get_node_style(self, node_type: str) -> Style: + def _get_node_style(self, node: Node) -> Style: # Apply simple highlighting to the node based on its type. - if node_type == "identifier": + if node.type == "identifier": style = Style(color="cyan") - elif node_type == "string": + elif node.type == "string": style = Style(color="green") - elif node_type == "import_from_statement": + elif node.type == "import_from_statement": style = Style(bgcolor="magenta") else: style = Style.null() @@ -230,22 +230,22 @@ def _cache_highlights( # This data will be referenced only when we render. if node_in_window: highlight_cache = self._highlights - node_type = cursor.node.type + node = cursor.node if node_start_row == node_end_row: - highlight = Highlight(node_start_column, node_end_column, node_type) - highlight_cache[node_start_row].add(highlight) + highlight = Highlight(node_start_column, node_end_column, node) + highlight_cache[node_start_row].append(highlight) else: # Add the first line - highlight_cache[node_start_row].add( - Highlight(node_start_column, None, node_type) + highlight_cache[node_start_row].append( + Highlight(node_start_column, None, node) ) # Add the middle lines - entire row of this node is highlighted for node_row in range(node_start_row + 1, node_end_row): - highlight_cache[node_row].add(Highlight(0, None, node_type)) + highlight_cache[node_row].append(Highlight(0, None, node)) # Add the last line - highlight_cache[node_end_row].add( - Highlight(0, node_end_column, node_type) + highlight_cache[node_end_row].append( + Highlight(0, node_end_column, node) ) if cursor.goto_first_child(): @@ -338,7 +338,6 @@ def action_cursor_right(self) -> None: cursor_row, cursor_column = self.cursor_position - print(f"action_cursor_right {self.cursor_position!r}") target_row = cursor_row + 1 if self.cursor_at_end_of_row else cursor_row target_column = 0 if self.cursor_at_end_of_row else cursor_column + 1 @@ -367,13 +366,20 @@ def traverse(cursor) -> Iterable[Node]: def action_print_highlight_cache(self) -> None: log.debug(self._highlights) - def __repr__(self): + def debug_state(self) -> str: return f"""\ cursor {self.cursor_position!r} language {self.language!r} -document rows {len(self.document_lines)} -highlight cache size {sum(len(highlights) for key, highlights in self._highlights.items())} -current row highlight cache size {len(self._highlights[self.cursor_position[0]])}""" +document rows {len(self.document_lines)}""" + + def debug_highlights(self) -> str: + return f"""\ +highlight cache keys (rows) {len(self._highlights)} +highlight cache total size {sum(len(highlights) for key, highlights in self._highlights.items())} +current row highlight cache size {len(self._highlights[self.cursor_position[0]])} + +[b]current row highlights[/] +{self._highlights[self.cursor_position[0]]}""" if __name__ == "__main__": From e8616e4b68965b4a84533e7a0a377ff6d1e15850 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 15:25:20 +0100 Subject: [PATCH 015/366] Scrolling the cursor into view when its moved and off screen --- src/textual/widgets/_text_editor.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 365c9ee64a..014c92d94c 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -12,7 +12,7 @@ from textual import events, log from textual._cells import cell_len from textual.binding import Binding -from textual.geometry import Size, clamp +from textual.geometry import Region, Size, clamp from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView from textual.strip import Strip @@ -278,6 +278,7 @@ async def _on_key(self, event: events.Key) -> None: def watch_cursor_position(self, new_position: tuple[int, int]) -> None: log.debug(f"cursor_position = {new_position!r}") + self.scroll_cursor_into_view() # --- Cursor utilities @property @@ -308,6 +309,13 @@ def cursor_at_end_of_document(self) -> bool: """True if the cursor is at the very end of the document.""" return self.cursor_at_last_row and self.cursor_at_end_of_row + def scroll_cursor_into_view(self) -> None: + """Scroll the cursor into view.""" + cursor_row, cursor_column = self.cursor_position + self.scroll_to_region( + Region(x=cursor_column, y=cursor_row, width=1, height=1), animate=False + ) + # ------ Cursor movement actions def action_cursor_left(self) -> None: """Move the cursor one position to the left. @@ -348,7 +356,6 @@ def insert_text_at_cursor(self, text: str) -> None: pass # --- Debug actions - def action_print_line_cache(self) -> None: log.debug(self._line_cache) From 3d4d01c5d96fd16441cee20dd06f90f21c498393 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 15:58:16 +0100 Subject: [PATCH 016/366] Vertical cursor movement --- src/textual/widgets/_text_editor.py | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 014c92d94c..9bed83f0dd 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -309,6 +309,14 @@ def cursor_at_end_of_document(self) -> bool: """True if the cursor is at the very end of the document.""" return self.cursor_at_last_row and self.cursor_at_end_of_row + def cursor_to_line_end(self) -> None: + cursor_row, cursor_column = self.cursor_position + self.cursor_position = (cursor_row, len(self.document_lines[cursor_row])) + + def cursor_to_line_start(self) -> None: + cursor_row, cursor_column = self.cursor_position + self.cursor_position = (0, cursor_row) + def scroll_cursor_into_view(self) -> None: """Scroll the cursor into view.""" cursor_row, cursor_column = self.cursor_position @@ -351,6 +359,36 @@ def action_cursor_right(self) -> None: self.cursor_position = (target_row, target_column) + def action_cursor_down(self) -> None: + """Move the cursor down one cell.""" + if self.cursor_at_last_row: + self.cursor_to_line_end() + + cursor_row, cursor_column = self.cursor_position + + target_row = min(len(self.document_lines) - 1, cursor_row + 1) + # TODO: Fetch last active column on this row + target_column = clamp( + cursor_column, 0, len(self.document_lines[target_row]) - 1 + ) + + self.cursor_position = (target_row, target_column) + + def action_cursor_up(self) -> None: + """Move the cursor up one cell.""" + if self.cursor_at_first_row: + self.cursor_to_line_start() + + cursor_row, cursor_column = self.cursor_position + + target_row = max(0, cursor_row - 1) + # TODO: Fetch last active column on this row + target_column = clamp( + cursor_column, 0, len(self.document_lines[target_row]) - 1 + ) + + self.cursor_position = (target_row, target_column) + # --- Editor operations def insert_text_at_cursor(self, text: str) -> None: pass From 7f1ba2d2d34c8dc375b07d0e6778d88d299c1016 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 16:29:50 +0100 Subject: [PATCH 017/366] Simple text insertion --- src/textual/widgets/_text_editor.py | 59 ++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 9bed83f0dd..0a25bb0d86 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -130,9 +130,7 @@ def load_lines(self, lines: list[str]) -> None: self.document_lines = lines # TODO Offer maximum line width and wrap if needed - width = max(cell_len(line) for line in lines) - height = len(lines) - self.virtual_size = Size(width, height) + self.virtual_size = self._get_document_size(lines) # TODO - clear caches if self._parser is not None: @@ -141,6 +139,11 @@ def load_lines(self, lines: list[str]) -> None: log.debug(f"loaded text. parser = {self._parser} ast = {self._ast}") + def _get_document_size(self, document_lines: list[str]) -> Size: + width = max(cell_len(line) for line in document_lines) + height = len(document_lines) + return Size(width, height) + def render_line(self, widget_y: int) -> Strip: document_lines = self.document_lines @@ -268,6 +271,9 @@ async def _on_key(self, event: events.Key) -> None: if event.is_printable: event.stop() assert event.character is not None + self.insert_text_at_cursor(event.character) + event.prevent_default() + self.refresh() # --- Reactive watchers and validators # def validate_cursor_position(self, new_position: tuple[int, int]) -> tuple[int, int]: @@ -277,7 +283,6 @@ async def _on_key(self, event: events.Key) -> None: # return clamped_row, clamped_column def watch_cursor_position(self, new_position: tuple[int, int]) -> None: - log.debug(f"cursor_position = {new_position!r}") self.scroll_cursor_into_view() # --- Cursor utilities @@ -391,7 +396,51 @@ def action_cursor_up(self) -> None: # --- Editor operations def insert_text_at_cursor(self, text: str) -> None: - pass + log.debug(f"insert {text!r} at {self.cursor_position!r}") + cursor_row, cursor_column = self.cursor_position + old_text = self.document_lines[cursor_row] + + # TODO: If the text has newline characters, this operation becomes + # more complex. + new_text = old_text[:cursor_column] + text + old_text[cursor_column:] + self.document_lines[cursor_row] = new_text + self.cursor_position = (cursor_row, cursor_column + cell_len(text)) + # cursor_row, cursor_column = self.cursor_position + # virtual_width, virtual_height = self.virtual_size + # if cursor_column > virtual_width: + # virtual_width = cursor_column + # if cursor_row > virtual_height: + # virtual_height = cursor_row + # + # self.virtual_size = Size(virtual_width, virtual_height) + + virtual_width, virtual_height = self.virtual_size + new_row_cell_length = cell_len(new_text) + if new_row_cell_length > virtual_width: + # TODO: The virtual height may change if the inserted text + # contains newline characters. We should count them an increment + # by that number. + self.virtual_size = Size(new_row_cell_length, virtual_height) + + self.refresh() + # TODO: Need to update the AST to inform it of the edit operation + + def delete_left(self) -> None: + log.debug(f"delete left at {self.cursor_position!r}") + + if self.cursor_at_start_of_document: + return + + cursor_row, cursor_column = self.cursor_position + + # If the cursor is at the start of a row, then the deletion "merges" the rows + # as it deletes the newline character that separates them. + if self.cursor_at_start_of_row: + pass + else: + old_text = self.document_lines[cursor_row] + + new_text = old_text[: cursor_column - 1] # --- Debug actions def action_print_line_cache(self) -> None: From 75d0e4592d2e62e11584e12e78413d716d623f1b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 29 Jun 2023 16:58:06 +0100 Subject: [PATCH 018/366] Virtual size updates --- src/textual/widgets/_text_editor.py | 35 +++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 0a25bb0d86..ca6e1fb739 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -37,6 +37,8 @@ class TextEditor(ScrollView, can_focus=True): Binding("down", "cursor_down", "cursor down", show=False), Binding("left", "cursor_left", "cursor left", show=False), Binding("right", "cursor_right", "cursor right", show=False), + Binding("home", "cursor_line_start", "cursor line start", show=False), + Binding("end", "cursor_line_end", "cursor line end", show=False), # Debugging bindings Binding("ctrl+s", "print_highlight_cache", "[debug] Print highlight cache"), Binding("ctrl+l", "print_line_cache", "[debug] Print line cache"), @@ -44,7 +46,7 @@ class TextEditor(ScrollView, can_focus=True): language: Reactive[str | None] = reactive(None) """The language to use for syntax highlighting (via tree-sitter).""" - cursor_position = reactive((0, 0)) + cursor_position = reactive((0, 0), always_update=True) """The cursor position (zero-based line_index, offset).""" def __init__( @@ -142,9 +144,16 @@ def load_lines(self, lines: list[str]) -> None: def _get_document_size(self, document_lines: list[str]) -> Size: width = max(cell_len(line) for line in document_lines) height = len(document_lines) - return Size(width, height) + # We add one to the width to leave a space for the cursor, since it can + # rest at the end of a line where there isn't yet any character. + return Size( + width + self.scrollbar_size_vertical + 1, + height + self.scrollbar_size_horizontal, + ) def render_line(self, widget_y: int) -> Strip: + log.debug(f"render_line {widget_y!r}") + document_lines = self.document_lines document_y = round(self.scroll_y + widget_y) @@ -176,8 +185,8 @@ def render_line(self, widget_y: int) -> Strip: ) strip = ( Strip(segments) - .adjust_cell_length(self.virtual_size.width - 1) - .crop(int(self.scroll_x), int(self.scroll_x) + self.virtual_size.width - 1) + .adjust_cell_length(self.virtual_size.width) + .crop(int(self.scroll_x), int(self.scroll_x) + self.virtual_size.width) .simplify() ) @@ -316,11 +325,11 @@ def cursor_at_end_of_document(self) -> bool: def cursor_to_line_end(self) -> None: cursor_row, cursor_column = self.cursor_position - self.cursor_position = (cursor_row, len(self.document_lines[cursor_row])) + self.cursor_position = (cursor_row, len(self.document_lines[cursor_row]) - 1) def cursor_to_line_start(self) -> None: cursor_row, cursor_column = self.cursor_position - self.cursor_position = (0, cursor_row) + self.cursor_position = (cursor_row, 0) def scroll_cursor_into_view(self) -> None: """Scroll the cursor into view.""" @@ -394,6 +403,12 @@ def action_cursor_up(self) -> None: self.cursor_position = (target_row, target_column) + def action_cursor_line_end(self) -> None: + self.cursor_to_line_end() + + def action_cursor_line_start(self) -> None: + self.cursor_to_line_start() + # --- Editor operations def insert_text_at_cursor(self, text: str) -> None: log.debug(f"insert {text!r} at {self.cursor_position!r}") @@ -420,7 +435,12 @@ def insert_text_at_cursor(self, text: str) -> None: # TODO: The virtual height may change if the inserted text # contains newline characters. We should count them an increment # by that number. - self.virtual_size = Size(new_row_cell_length, virtual_height) + + # TODO: Does the virtual size need to account for the scrollbar width + # to ensure that the text never gets hidden behind the scrollbar? + self.virtual_size = Size( + new_row_cell_length + self.scrollbar_size_vertical + 1, virtual_height + ) self.refresh() # TODO: Need to update the AST to inform it of the edit operation @@ -464,6 +484,7 @@ def debug_state(self) -> str: return f"""\ cursor {self.cursor_position!r} language {self.language!r} +virtual_size {self.virtual_size!r} document rows {len(self.document_lines)}""" def debug_highlights(self) -> str: From 8e859c49d505203062ae54e77a6ddcdda14e5617 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 3 Jul 2023 11:52:25 +0100 Subject: [PATCH 019/366] Ensure we update virtual_size at correct time --- src/textual/widgets/_text_editor.py | 77 +++++++++++++++++++---------- 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index ca6e1fb739..3c1cd6cc0f 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -2,7 +2,7 @@ from collections import defaultdict from pathlib import Path -from typing import Iterable, NamedTuple +from typing import ClassVar, Iterable, NamedTuple from rich.segment import Segment from rich.style import Style @@ -31,6 +31,18 @@ class Highlight(NamedTuple): class TextEditor(ScrollView, can_focus=True): + DEFAULT_CSS = """\ +TextEditor > .text-editor--active-line { + background: $success; +} +""" + + COMPONENT_CLASSES: ClassVar[set[str]] = { + "text-editor--active-line", + "text-editor--active-line-gutter", + "text-editor--gutter", + } + BINDINGS = [ # Cursor movement Binding("up", "cursor_up", "cursor up", show=False), @@ -46,8 +58,10 @@ class TextEditor(ScrollView, can_focus=True): language: Reactive[str | None] = reactive(None) """The language to use for syntax highlighting (via tree-sitter).""" - cursor_position = reactive((0, 0), always_update=True) + cursor_position: Reactive[tuple[int, int]] = reactive((0, 0), always_update=True) """The cursor position (zero-based line_index, offset).""" + show_line_numbers: Reactive[bool] = reactive(True) + """True to show line number gutter, otherwise False.""" def __init__( self, @@ -132,7 +146,16 @@ def load_lines(self, lines: list[str]) -> None: self.document_lines = lines # TODO Offer maximum line width and wrap if needed - self.virtual_size = self._get_document_size(lines) + virtual_size = self._get_document_size(lines) + width, height = virtual_size + if self.show_line_numbers: + total_gutter_padding = 2 + gutter_width = len(str(len(lines) + 1)) + total_gutter_padding + virtual_size = Size( + width + gutter_width + self.scrollbar_size_vertical, height + ) + + self.virtual_size = virtual_size # TODO - clear caches if self._parser is not None: @@ -141,6 +164,7 @@ def load_lines(self, lines: list[str]) -> None: log.debug(f"loaded text. parser = {self._parser} ast = {self._ast}") + # --- Methods for measuring things (e.g. virtual sizes) def _get_document_size(self, document_lines: list[str]) -> Size: width = max(cell_len(line) for line in document_lines) height = len(document_lines) @@ -163,6 +187,7 @@ def render_line(self, widget_y: int) -> Strip: line_string = document_lines[document_y].replace("\n", "").replace("\r", "") line_text = Text(f"{line_string} ", end="") + line_text.set_length(self.virtual_size.width) # Apply highlighting to the line if necessary. if self._highlights: @@ -177,6 +202,7 @@ def render_line(self, widget_y: int) -> Strip: line_text.stylize( Style(color="black", bgcolor="white"), cursor_column, cursor_column + 1 ) + line_text.stylize_before(Style(bgcolor="#363636")) # We need to render according to the virtual size otherwise the rendering # will wrap the text content incorrectly. @@ -185,8 +211,7 @@ def render_line(self, widget_y: int) -> Strip: ) strip = ( Strip(segments) - .adjust_cell_length(self.virtual_size.width) - .crop(int(self.scroll_x), int(self.scroll_x) + self.virtual_size.width) + .crop(int(self.scroll_x), int(self.scroll_x) + self.virtual_size.width + 1) .simplify() ) @@ -218,6 +243,9 @@ def _cache_highlights( line_range: The start and end line index that is visible. If None, highlight the whole document. """ + # TODO: Instead of traversing the AST, use AST queries and the + # .scm files from tree-sitter GitHub org for highlighting. + reached_root = False while not reached_root: @@ -276,7 +304,7 @@ def _cache_highlights( retracing = False # --- Key handling - async def _on_key(self, event: events.Key) -> None: + def _on_key(self, event: events.Key) -> None: if event.is_printable: event.stop() assert event.character is not None @@ -292,7 +320,16 @@ async def _on_key(self, event: events.Key) -> None: # return clamped_row, clamped_column def watch_cursor_position(self, new_position: tuple[int, int]) -> None: - self.scroll_cursor_into_view() + log.debug("scrolling cursor into view") + row, column = new_position + self.scroll_to_region( + Region(x=column, y=row, width=1, height=1), + animate=False, + origin_visible=False, + ) + + def watch_virtual_size(self, vs): + log.debug(f"new virtual_size = {vs!r}") # --- Cursor utilities @property @@ -331,13 +368,6 @@ def cursor_to_line_start(self) -> None: cursor_row, cursor_column = self.cursor_position self.cursor_position = (cursor_row, 0) - def scroll_cursor_into_view(self) -> None: - """Scroll the cursor into view.""" - cursor_row, cursor_column = self.cursor_position - self.scroll_to_region( - Region(x=cursor_column, y=cursor_row, width=1, height=1), animate=False - ) - # ------ Cursor movement actions def action_cursor_left(self) -> None: """Move the cursor one position to the left. @@ -419,7 +449,6 @@ def insert_text_at_cursor(self, text: str) -> None: # more complex. new_text = old_text[:cursor_column] + text + old_text[cursor_column:] self.document_lines[cursor_row] = new_text - self.cursor_position = (cursor_row, cursor_column + cell_len(text)) # cursor_row, cursor_column = self.cursor_position # virtual_width, virtual_height = self.virtual_size # if cursor_column > virtual_width: @@ -431,17 +460,17 @@ def insert_text_at_cursor(self, text: str) -> None: virtual_width, virtual_height = self.virtual_size new_row_cell_length = cell_len(new_text) - if new_row_cell_length > virtual_width: + + # The virtual width of the row is the cell length of the text in the row + # plus 1 to accommodate for a cursor potentially "resting" at the end of the row. + row_virtual_width = new_row_cell_length + 1 + if row_virtual_width > virtual_width: # TODO: The virtual height may change if the inserted text # contains newline characters. We should count them an increment # by that number. + self.virtual_size = Size(row_virtual_width, virtual_height) - # TODO: Does the virtual size need to account for the scrollbar width - # to ensure that the text never gets hidden behind the scrollbar? - self.virtual_size = Size( - new_row_cell_length + self.scrollbar_size_vertical + 1, virtual_height - ) - + self.cursor_position = (cursor_row, cursor_column + cell_len(text)) self.refresh() # TODO: Need to update the AST to inform it of the edit operation @@ -492,9 +521,7 @@ def debug_highlights(self) -> str: highlight cache keys (rows) {len(self._highlights)} highlight cache total size {sum(len(highlights) for key, highlights in self._highlights.items())} current row highlight cache size {len(self._highlights[self.cursor_position[0]])} - -[b]current row highlights[/] -{self._highlights[self.cursor_position[0]]}""" +""" if __name__ == "__main__": From 0d1aeff785f1e195f7d677e1c4c19ce75f7aed37 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 3 Jul 2023 12:43:34 +0100 Subject: [PATCH 020/366] Tidying up virtual size calculation --- src/textual/widgets/_text_editor.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 3c1cd6cc0f..da5d705ea6 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -146,16 +146,7 @@ def load_lines(self, lines: list[str]) -> None: self.document_lines = lines # TODO Offer maximum line width and wrap if needed - virtual_size = self._get_document_size(lines) - width, height = virtual_size - if self.show_line_numbers: - total_gutter_padding = 2 - gutter_width = len(str(len(lines) + 1)) + total_gutter_padding - virtual_size = Size( - width + gutter_width + self.scrollbar_size_vertical, height - ) - - self.virtual_size = virtual_size + self.virtual_size = self._get_document_size(lines) # TODO - clear caches if self._parser is not None: @@ -166,14 +157,11 @@ def load_lines(self, lines: list[str]) -> None: # --- Methods for measuring things (e.g. virtual sizes) def _get_document_size(self, document_lines: list[str]) -> Size: - width = max(cell_len(line) for line in document_lines) + text_width = max(cell_len(line) for line in document_lines) height = len(document_lines) - # We add one to the width to leave a space for the cursor, since it can - # rest at the end of a line where there isn't yet any character. - return Size( - width + self.scrollbar_size_vertical + 1, - height + self.scrollbar_size_horizontal, - ) + # We add one to the text width to leave a space for the cursor, since it + # can rest at the end of a line where there isn't yet any character. + return Size(text_width + 1, height) def render_line(self, widget_y: int) -> Strip: log.debug(f"render_line {widget_y!r}") @@ -514,7 +502,9 @@ def debug_state(self) -> str: cursor {self.cursor_position!r} language {self.language!r} virtual_size {self.virtual_size!r} -document rows {len(self.document_lines)}""" +document rows {len(self.document_lines)!r} +scroll {(self.scroll_x, self.scroll_y)!r} +""" def debug_highlights(self) -> str: return f"""\ From 127b6c9b75dbcbd1b0966a0fb27cbdedbd890e21 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 3 Jul 2023 12:46:23 +0100 Subject: [PATCH 021/366] Naive tab key handling --- src/textual/widgets/_text_editor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index da5d705ea6..adc3ff8c9d 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -174,7 +174,7 @@ def render_line(self, widget_y: int) -> Strip: return Strip.blank(self.size.width) line_string = document_lines[document_y].replace("\n", "").replace("\r", "") - line_text = Text(f"{line_string} ", end="") + line_text = Text(f"{line_string} ", end="", tab_size=4) line_text.set_length(self.virtual_size.width) # Apply highlighting to the line if necessary. @@ -300,6 +300,9 @@ def _on_key(self, event: events.Key) -> None: event.prevent_default() self.refresh() + if event.key == "tab": + self.insert_text_at_cursor(" ") + # --- Reactive watchers and validators # def validate_cursor_position(self, new_position: tuple[int, int]) -> tuple[int, int]: # new_row, new_column = new_position From 80558d25b13987836a13a77a49e6775fddd8672d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 3 Jul 2023 12:48:01 +0100 Subject: [PATCH 022/366] Preventing default tab usage --- src/textual/widgets/_text_editor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index adc3ff8c9d..6db04862b8 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -298,10 +298,11 @@ def _on_key(self, event: events.Key) -> None: assert event.character is not None self.insert_text_at_cursor(event.character) event.prevent_default() - self.refresh() if event.key == "tab": + event.stop() self.insert_text_at_cursor(" ") + event.prevent_default() # --- Reactive watchers and validators # def validate_cursor_position(self, new_position: tuple[int, int]) -> tuple[int, int]: From 0f08c12687a1ac64a2c8bd2da0d5ef74ad52437e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 3 Jul 2023 16:15:56 +0100 Subject: [PATCH 023/366] Line splitting, pasting multi-line text --- src/textual/widgets/_text_editor.py | 113 +++++++++++++++++++--------- 1 file changed, 76 insertions(+), 37 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 6db04862b8..1b4760ed16 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -74,7 +74,7 @@ def __init__( # --- Core editor data self.document_lines: list[str] = [] - """Each string in this list represents a line in the document.""" + """Each string in this list represents a line in the document. Includes new line characters.""" self._highlights: dict[int, list[Highlight]] = defaultdict(list) """Mapping line numbers to the set of cached highlights for that line.""" @@ -291,18 +291,27 @@ def _cache_highlights( if cursor.goto_next_sibling(): retracing = False - # --- Key handling + # --- Lower level event/key handling def _on_key(self, event: events.Key) -> None: - if event.is_printable: + log.debug(f"{event!r}") + key = event.key + if event.is_printable or key == "tab": + if key == "tab": + insert = " " + else: + insert = event.character event.stop() assert event.character is not None - self.insert_text_at_cursor(event.character) + self.insert_text(insert) event.prevent_default() + elif key == "enter": + self.split_line() - if event.key == "tab": - event.stop() - self.insert_text_at_cursor(" ") - event.prevent_default() + def _on_paste(self, event: events.Paste) -> None: + text = event.text + if text: + self.insert_text(text) + event.stop() # --- Reactive watchers and validators # def validate_cursor_position(self, new_position: tuple[int, int]) -> tuple[int, int]: @@ -313,17 +322,21 @@ def _on_key(self, event: events.Key) -> None: def watch_cursor_position(self, new_position: tuple[int, int]) -> None: log.debug("scrolling cursor into view") - row, column = new_position + self.scroll_cursor_visible() + + def watch_virtual_size(self, vs): + log.debug(f"new virtual_size = {vs!r}") + + # --- Cursor utilities + + def scroll_cursor_visible(self): + row, column = self.cursor_position self.scroll_to_region( Region(x=column, y=row, width=1, height=1), animate=False, origin_visible=False, ) - def watch_virtual_size(self, vs): - log.debug(f"new virtual_size = {vs!r}") - - # --- Cursor utilities @property def cursor_at_first_row(self) -> bool: return self.cursor_position[0] == 0 @@ -432,40 +445,66 @@ def action_cursor_line_start(self) -> None: self.cursor_to_line_start() # --- Editor operations - def insert_text_at_cursor(self, text: str) -> None: + def insert_text(self, text: str) -> None: log.debug(f"insert {text!r} at {self.cursor_position!r}") cursor_row, cursor_column = self.cursor_position - old_text = self.document_lines[cursor_row] - - # TODO: If the text has newline characters, this operation becomes - # more complex. - new_text = old_text[:cursor_column] + text + old_text[cursor_column:] - self.document_lines[cursor_row] = new_text - # cursor_row, cursor_column = self.cursor_position - # virtual_width, virtual_height = self.virtual_size - # if cursor_column > virtual_width: - # virtual_width = cursor_column - # if cursor_row > virtual_height: - # virtual_height = cursor_row - # - # self.virtual_size = Size(virtual_width, virtual_height) + lines = self.document_lines + + line = lines[cursor_row] + text_before_cursor = line[:cursor_column] + text_after_cursor = line[cursor_column:] + + replacement_lines = text.splitlines(keepends=False) + replacement_lines[0] = text_before_cursor + replacement_lines[0] + end_column = cell_len(replacement_lines[-1]) + replacement_lines[-1] += text_after_cursor + + lines[cursor_row : cursor_row + 1] = replacement_lines + + longest_modified_line = max(cell_len(line) for line in replacement_lines) virtual_width, virtual_height = self.virtual_size - new_row_cell_length = cell_len(new_text) # The virtual width of the row is the cell length of the text in the row # plus 1 to accommodate for a cursor potentially "resting" at the end of the row. - row_virtual_width = new_row_cell_length + 1 - if row_virtual_width > virtual_width: - # TODO: The virtual height may change if the inserted text - # contains newline characters. We should count them an increment - # by that number. - self.virtual_size = Size(row_virtual_width, virtual_height) - - self.cursor_position = (cursor_row, cursor_column + cell_len(text)) + insertion_virtual_width = longest_modified_line + 1 + + new_virtual_width = max(insertion_virtual_width, virtual_width) + new_virtual_height = len(lines) + + self.virtual_size = Size(new_virtual_width, new_virtual_height) + + print("final_insert = ", replacement_lines) + + # TODO: Update the cursor column (length of final insert line?) + self.cursor_position = (cursor_row + len(replacement_lines) - 1, end_column) self.refresh() # TODO: Need to update the AST to inform it of the edit operation + def split_line(self): + cursor_row, cursor_column = self.cursor_position + lines = self.document_lines + + line = lines[cursor_row] + text_before_cursor = line[:cursor_column] + text_after_cursor = line[cursor_column:] + + lines = ( + lines[:cursor_row] + + [text_before_cursor, text_after_cursor] + + lines[cursor_row + 1 :] + ) + + self.document_lines = lines + + # Update virtual size and cursor position + self.cursor_position = (cursor_row + 1, 0) + width, height = self.virtual_size + + # If this line was responsible for the document virtual width + if width == cell_len(line) + 1: + self.virtual_size = self._get_document_size(lines) + def delete_left(self) -> None: log.debug(f"delete left at {self.cursor_position!r}") From 669975f6f93239393a3b7d99139739770b343648 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 3 Jul 2023 16:39:29 +0100 Subject: [PATCH 024/366] Ensure cursor is visible after pasting --- src/textual/widgets/_text_editor.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 1b4760ed16..cef5c06e4e 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -161,6 +161,8 @@ def _get_document_size(self, document_lines: list[str]) -> Size: height = len(document_lines) # We add one to the text width to leave a space for the cursor, since it # can rest at the end of a line where there isn't yet any character. + # Similarly, the cursor can rest below the bottom line of text, where + # a line doesn't currently exist. return Size(text_width + 1, height) def render_line(self, widget_y: int) -> Strip: @@ -331,10 +333,9 @@ def watch_virtual_size(self, vs): def scroll_cursor_visible(self): row, column = self.cursor_position + log.debug(f"scrolling to cursor at {row,column}") self.scroll_to_region( - Region(x=column, y=row, width=1, height=1), - animate=False, - origin_visible=False, + Region(x=column, y=row, width=1, height=1), animate=False, force=True ) @property @@ -473,12 +474,10 @@ def insert_text(self, text: str) -> None: new_virtual_height = len(lines) self.virtual_size = Size(new_virtual_width, new_virtual_height) + self.cursor_position = (cursor_row + len(replacement_lines) - 1, end_column) print("final_insert = ", replacement_lines) - # TODO: Update the cursor column (length of final insert line?) - self.cursor_position = (cursor_row + len(replacement_lines) - 1, end_column) - self.refresh() # TODO: Need to update the AST to inform it of the edit operation def split_line(self): @@ -496,14 +495,9 @@ def split_line(self): ) self.document_lines = lines - - # Update virtual size and cursor position - self.cursor_position = (cursor_row + 1, 0) width, height = self.virtual_size - - # If this line was responsible for the document virtual width - if width == cell_len(line) + 1: - self.virtual_size = self._get_document_size(lines) + self.virtual_size = Size(width, height + 1) + self.cursor_position = (cursor_row + 1, 0) def delete_left(self) -> None: log.debug(f"delete left at {self.cursor_position!r}") From 91d5e0f83030892f5e918664a43e3c6bbf952396 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 3 Jul 2023 16:47:46 +0100 Subject: [PATCH 025/366] Fix width calculation --- src/textual/widgets/_text_editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index cef5c06e4e..3bb150c295 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -201,7 +201,7 @@ def render_line(self, widget_y: int) -> Strip: ) strip = ( Strip(segments) - .crop(int(self.scroll_x), int(self.scroll_x) + self.virtual_size.width + 1) + .crop(int(self.scroll_x), int(self.scroll_x) + self.virtual_size.width) .simplify() ) From b670916630852a300f90a1925657d25c38938196 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 11:11:51 +0100 Subject: [PATCH 026/366] Rendering gutter --- src/textual/widgets/_text_editor.py | 59 ++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 3bb150c295..70a86c019d 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -146,7 +146,8 @@ def load_lines(self, lines: list[str]) -> None: self.document_lines = lines # TODO Offer maximum line width and wrap if needed - self.virtual_size = self._get_document_size(lines) + print("setting vs in load_lines") + self.virtual_size = self._get_virtual_size() # TODO - clear caches if self._parser is not None: @@ -157,6 +158,9 @@ def load_lines(self, lines: list[str]) -> None: # --- Methods for measuring things (e.g. virtual sizes) def _get_document_size(self, document_lines: list[str]) -> Size: + """Return the virtual size of the document - the document only + refers to the area in which the cursor can move. It does not, for + example, include the width of the gutter.""" text_width = max(cell_len(line) for line in document_lines) height = len(document_lines) # We add one to the text width to leave a space for the cursor, since it @@ -165,6 +169,14 @@ def _get_document_size(self, document_lines: list[str]) -> Size: # a line doesn't currently exist. return Size(text_width + 1, height) + def _get_virtual_size(self) -> Size: + document_width, document_height = self._get_document_size(self.document_lines) + gutter_width = self.gutter_width + return Size( + document_width + max(gutter_width - int(self.scroll_x), 0), + document_height, + ) + def render_line(self, widget_y: int) -> Strip: log.debug(f"render_line {widget_y!r}") @@ -194,19 +206,45 @@ def render_line(self, widget_y: int) -> Strip: ) line_text.stylize_before(Style(bgcolor="#363636")) - # We need to render according to the virtual size otherwise the rendering - # will wrap the text content incorrectly. - segments = self.app.console.render( + if self.show_line_numbers: + gutter_style = self.get_component_rich_style("text-editor--gutter") + gutter_width_no_margin = self.gutter_width - 2 + gutter = Text( + f"{document_y + 1:>{gutter_width_no_margin}}│ ", + style=gutter_style, + end="", + ) + else: + gutter = Text("", end="") + + gutter_segments = self.app.console.render(gutter) + text_segments = self.app.console.render( line_text, self.app.console.options.update_width(self.virtual_size.width) ) - strip = ( - Strip(segments) - .crop(int(self.scroll_x), int(self.scroll_x) + self.virtual_size.width) - .simplify() - ) + + virtual_width, virtual_height = self.virtual_size + text_crop_start = int(self.scroll_x) + text_crop_end = text_crop_start + virtual_width + + gutter_strip = Strip(gutter_segments) + text_strip = Strip(text_segments).crop(text_crop_start, text_crop_end) + + strip = Strip.join([gutter_strip, text_strip]).simplify() + log.debug(f"combined_strip = {strip.text!r}") return strip + @property + def gutter_width(self) -> int: + # The longest number in the gutter plus two extra characters: `│ `. + gutter_margin = 2 + gutter_longest_number = ( + len(str(len(self.document_lines) + 1)) + gutter_margin + if self.show_line_numbers + else 0 + ) + return gutter_longest_number + def _get_node_style(self, node: Node) -> Style: # Apply simple highlighting to the node based on its type. if node.type == "identifier": @@ -333,7 +371,8 @@ def watch_virtual_size(self, vs): def scroll_cursor_visible(self): row, column = self.cursor_position - log.debug(f"scrolling to cursor at {row,column}") + log.debug(f"scrolling to cursor at {row, column}") + # TODO - this should account for gutter? self.scroll_to_region( Region(x=column, y=row, width=1, height=1), animate=False, force=True ) From 2ce2f49e6f18365b93d470599af17b48179b3285 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 11:41:41 +0100 Subject: [PATCH 027/366] Scroll to region accommodates for gutter now --- src/textual/widgets/_text_editor.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 70a86c019d..b8a7dbd4ce 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -12,7 +12,7 @@ from textual import events, log from textual._cells import cell_len from textual.binding import Binding -from textual.geometry import Region, Size, clamp +from textual.geometry import Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView from textual.strip import Strip @@ -172,14 +172,13 @@ def _get_document_size(self, document_lines: list[str]) -> Size: def _get_virtual_size(self) -> Size: document_width, document_height = self._get_document_size(self.document_lines) gutter_width = self.gutter_width + # gutter_width_contribution = max(gutter_width - int(self.scroll_x), 0) return Size( - document_width + max(gutter_width - int(self.scroll_x), 0), + document_width + gutter_width, document_height, ) def render_line(self, widget_y: int) -> Strip: - log.debug(f"render_line {widget_y!r}") - document_lines = self.document_lines document_y = round(self.scroll_y + widget_y) @@ -230,7 +229,6 @@ def render_line(self, widget_y: int) -> Strip: text_strip = Strip(text_segments).crop(text_crop_start, text_crop_end) strip = Strip.join([gutter_strip, text_strip]).simplify() - log.debug(f"combined_strip = {strip.text!r}") return strip @@ -371,10 +369,17 @@ def watch_virtual_size(self, vs): def scroll_cursor_visible(self): row, column = self.cursor_position - log.debug(f"scrolling to cursor at {row, column}") # TODO - this should account for gutter? + + target_x = column + target_y = row + target_region = Region(x=target_x, y=target_y, width=1, height=1) + log.debug(f"scrolling to target {target_x, target_y}") self.scroll_to_region( - Region(x=column, y=row, width=1, height=1), animate=False, force=True + target_region, + spacing=Spacing(right=self.gutter_width), + animate=False, + force=True, ) @property From 25bebc5803338fbb0da59db18799f2efff70cee5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 12:39:04 +0100 Subject: [PATCH 028/366] Ensure linesep is maintained, use cell_len for checking if cursor is at end --- src/textual/widgets/_text_editor.py | 64 +++++++++++++++-------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index b8a7dbd4ce..f1b0789ee3 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from collections import defaultdict from pathlib import Path from typing import ClassVar, Iterable, NamedTuple @@ -62,6 +63,9 @@ class TextEditor(ScrollView, can_focus=True): """The cursor position (zero-based line_index, offset).""" show_line_numbers: Reactive[bool] = reactive(True) """True to show line number gutter, otherwise False.""" + _document_size: Reactive[Size] = reactive(Size(), init=False) + """Tracks the width of the document. Used to update virtual size. Do not + update virtual size directly.""" def __init__( self, @@ -111,6 +115,11 @@ def watch_language(self, new_language: str | None) -> None: log.debug(f"parser set to {self._parser}") + def watch__document_size(self, size: Size) -> None: + log.debug(f"document size set to {size!r} ") + document_width, document_height = size + self.virtual_size = Size(document_width + self.gutter_width, document_height) + def _build_ast( self, parser: Parser, @@ -147,7 +156,7 @@ def load_lines(self, lines: list[str]) -> None: # TODO Offer maximum line width and wrap if needed print("setting vs in load_lines") - self.virtual_size = self._get_virtual_size() + self._document_size = self._get_document_size(lines) # TODO - clear caches if self._parser is not None: @@ -169,15 +178,6 @@ def _get_document_size(self, document_lines: list[str]) -> Size: # a line doesn't currently exist. return Size(text_width + 1, height) - def _get_virtual_size(self) -> Size: - document_width, document_height = self._get_document_size(self.document_lines) - gutter_width = self.gutter_width - # gutter_width_contribution = max(gutter_width - int(self.scroll_x), 0) - return Size( - document_width + gutter_width, - document_height, - ) - def render_line(self, widget_y: int) -> Strip: document_lines = self.document_lines @@ -366,17 +366,10 @@ def watch_virtual_size(self, vs): log.debug(f"new virtual_size = {vs!r}") # --- Cursor utilities - def scroll_cursor_visible(self): row, column = self.cursor_position - # TODO - this should account for gutter? - - target_x = column - target_y = row - target_region = Region(x=target_x, y=target_y, width=1, height=1) - log.debug(f"scrolling to target {target_x, target_y}") self.scroll_to_region( - target_region, + Region(x=column, y=row, width=1, height=1), spacing=Spacing(right=self.gutter_width), animate=False, force=True, @@ -397,7 +390,7 @@ def cursor_at_start_of_row(self) -> bool: @property def cursor_at_end_of_row(self) -> bool: cursor_row, cursor_column = self.cursor_position - row_length = len(self.document_lines[cursor_row]) + row_length = cell_len(self.document_lines[cursor_row]) cursor_at_end = cursor_column == row_length - 1 return cursor_at_end @@ -412,7 +405,10 @@ def cursor_at_end_of_document(self) -> bool: def cursor_to_line_end(self) -> None: cursor_row, cursor_column = self.cursor_position - self.cursor_position = (cursor_row, len(self.document_lines[cursor_row]) - 1) + self.cursor_position = ( + cursor_row, + cell_len(self.document_lines[cursor_row]) - 1, + ) def cursor_to_line_start(self) -> None: cursor_row, cursor_column = self.cursor_position @@ -489,6 +485,11 @@ def action_cursor_line_end(self) -> None: def action_cursor_line_start(self) -> None: self.cursor_to_line_start() + @property + def active_line_text(self) -> str: + # TODO - consider empty documents + return self.document_lines[self.cursor_position[0]] + # --- Editor operations def insert_text(self, text: str) -> None: log.debug(f"insert {text!r} at {self.cursor_position!r}") @@ -500,7 +501,7 @@ def insert_text(self, text: str) -> None: text_before_cursor = line[:cursor_column] text_after_cursor = line[cursor_column:] - replacement_lines = text.splitlines(keepends=False) + replacement_lines = text.splitlines(keepends=True) replacement_lines[0] = text_before_cursor + replacement_lines[0] end_column = cell_len(replacement_lines[-1]) replacement_lines[-1] += text_after_cursor @@ -508,16 +509,16 @@ def insert_text(self, text: str) -> None: lines[cursor_row : cursor_row + 1] = replacement_lines longest_modified_line = max(cell_len(line) for line in replacement_lines) - virtual_width, virtual_height = self.virtual_size + document_width, document_height = self._document_size # The virtual width of the row is the cell length of the text in the row # plus 1 to accommodate for a cursor potentially "resting" at the end of the row. - insertion_virtual_width = longest_modified_line + 1 + insertion_width = longest_modified_line + 1 - new_virtual_width = max(insertion_virtual_width, virtual_width) - new_virtual_height = len(lines) + new_document_width = max(insertion_width, document_width) + new_document_height = len(lines) - self.virtual_size = Size(new_virtual_width, new_virtual_height) + self._document_size = Size(new_document_width, new_document_height) self.cursor_position = (cursor_row + len(replacement_lines) - 1, end_column) print("final_insert = ", replacement_lines) @@ -534,13 +535,13 @@ def split_line(self): lines = ( lines[:cursor_row] - + [text_before_cursor, text_after_cursor] + + [text_before_cursor + os.linesep, text_after_cursor] + lines[cursor_row + 1 :] ) self.document_lines = lines - width, height = self.virtual_size - self.virtual_size = Size(width, height + 1) + width, height = self._document_size + self._document_size = Size(width, height + 1) self.cursor_position = (cursor_row + 1, 0) def delete_left(self) -> None: @@ -582,9 +583,12 @@ def debug_state(self) -> str: return f"""\ cursor {self.cursor_position!r} language {self.language!r} +document_size {self._document_size!r} virtual_size {self.virtual_size!r} -document rows {len(self.document_lines)!r} scroll {(self.scroll_x, self.scroll_y)!r} + +[b]LINE INDEX {self.cursor_position[0]} (cell_len={cell_len(self.active_line_text)})[/] +{self.active_line_text!r} """ def debug_highlights(self) -> str: From 78bd84ded9b06e419b3d817afc174dad21475cd1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 12:45:32 +0100 Subject: [PATCH 029/366] Fixing some length calculations --- src/textual/widgets/_text_editor.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index f1b0789ee3..5f4dfaed90 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -391,7 +391,7 @@ def cursor_at_start_of_row(self) -> bool: def cursor_at_end_of_row(self) -> bool: cursor_row, cursor_column = self.cursor_position row_length = cell_len(self.document_lines[cursor_row]) - cursor_at_end = cursor_column == row_length - 1 + cursor_at_end = cursor_column == row_length return cursor_at_end @property @@ -407,7 +407,7 @@ def cursor_to_line_end(self) -> None: cursor_row, cursor_column = self.cursor_position self.cursor_position = ( cursor_row, - cell_len(self.document_lines[cursor_row]) - 1, + cell_len(self.document_lines[cursor_row]), ) def cursor_to_line_start(self) -> None: @@ -425,12 +425,10 @@ def action_cursor_left(self) -> None: return cursor_row, cursor_column = self.cursor_position - length_of_row_above = len(self.document_lines[cursor_row - 1]) + length_of_row_above = cell_len(self.document_lines[cursor_row - 1]) target_row = cursor_row if cursor_column != 0 else cursor_row - 1 - target_column = ( - cursor_column - 1 if cursor_column != 0 else length_of_row_above - 1 - ) + target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above self.cursor_position = (target_row, target_column) @@ -459,7 +457,7 @@ def action_cursor_down(self) -> None: target_row = min(len(self.document_lines) - 1, cursor_row + 1) # TODO: Fetch last active column on this row target_column = clamp( - cursor_column, 0, len(self.document_lines[target_row]) - 1 + cursor_column, 0, cell_len(self.document_lines[target_row]) - 1 ) self.cursor_position = (target_row, target_column) @@ -474,7 +472,7 @@ def action_cursor_up(self) -> None: target_row = max(0, cursor_row - 1) # TODO: Fetch last active column on this row target_column = clamp( - cursor_column, 0, len(self.document_lines[target_row]) - 1 + cursor_column, 0, cell_len(self.document_lines[target_row]) - 1 ) self.cursor_position = (target_row, target_column) From a887d262a5fdea5bb048e81d7c040435bd321aaa Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 12:58:31 +0100 Subject: [PATCH 030/366] Ensure scrolling supports CJK width characters --- src/textual/widgets/_text_editor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 5f4dfaed90..8ad4a0c972 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -368,8 +368,10 @@ def watch_virtual_size(self, vs): # --- Cursor utilities def scroll_cursor_visible(self): row, column = self.cursor_position + text = self.active_line_text[:column] + column_offset = cell_len(text) self.scroll_to_region( - Region(x=column, y=row, width=1, height=1), + Region(x=column_offset, y=row, width=1, height=1), spacing=Spacing(right=self.gutter_width), animate=False, force=True, From 1900eb7fe302721bd6124ad3e0cd06a91327e1f6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 13:09:55 +0100 Subject: [PATCH 031/366] Fixes for double width characters --- src/textual/widgets/_text_editor.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 8ad4a0c972..f09c2bf5e5 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -147,7 +147,7 @@ def read_callable(byte_offset, point): def load_text(self, text: str) -> None: """Load text from a string into the editor.""" - lines = text.splitlines(keepends=True) + lines = text.splitlines(keepends=False) self.load_lines(lines) def load_lines(self, lines: list[str]) -> None: @@ -392,7 +392,7 @@ def cursor_at_start_of_row(self) -> bool: @property def cursor_at_end_of_row(self) -> bool: cursor_row, cursor_column = self.cursor_position - row_length = cell_len(self.document_lines[cursor_row]) + row_length = len(self.document_lines[cursor_row]) cursor_at_end = cursor_column == row_length return cursor_at_end @@ -409,7 +409,7 @@ def cursor_to_line_end(self) -> None: cursor_row, cursor_column = self.cursor_position self.cursor_position = ( cursor_row, - cell_len(self.document_lines[cursor_row]), + len(self.document_lines[cursor_row]), ) def cursor_to_line_start(self) -> None: @@ -427,7 +427,7 @@ def action_cursor_left(self) -> None: return cursor_row, cursor_column = self.cursor_position - length_of_row_above = cell_len(self.document_lines[cursor_row - 1]) + length_of_row_above = len(self.document_lines[cursor_row - 1]) target_row = cursor_row if cursor_column != 0 else cursor_row - 1 target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above @@ -458,9 +458,7 @@ def action_cursor_down(self) -> None: target_row = min(len(self.document_lines) - 1, cursor_row + 1) # TODO: Fetch last active column on this row - target_column = clamp( - cursor_column, 0, cell_len(self.document_lines[target_row]) - 1 - ) + target_column = clamp(cursor_column, 0, len(self.document_lines[target_row])) self.cursor_position = (target_row, target_column) @@ -501,7 +499,7 @@ def insert_text(self, text: str) -> None: text_before_cursor = line[:cursor_column] text_after_cursor = line[cursor_column:] - replacement_lines = text.splitlines(keepends=True) + replacement_lines = text.splitlines(keepends=False) replacement_lines[0] = text_before_cursor + replacement_lines[0] end_column = cell_len(replacement_lines[-1]) replacement_lines[-1] += text_after_cursor @@ -535,7 +533,7 @@ def split_line(self): lines = ( lines[:cursor_row] - + [text_before_cursor + os.linesep, text_after_cursor] + + [text_before_cursor, text_after_cursor] + lines[cursor_row + 1 :] ) From 9ef5568dbaf6b5fb086594e0dddb134066b08b57 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 13:26:22 +0100 Subject: [PATCH 032/366] Styling and ensuring column calculated correctly after insert --- src/textual/widgets/_text_editor.py | 40 ++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index f09c2bf5e5..c2bce8e163 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -34,7 +34,19 @@ class Highlight(NamedTuple): class TextEditor(ScrollView, can_focus=True): DEFAULT_CSS = """\ TextEditor > .text-editor--active-line { - background: $success; + background: $panel-lighten-1; +} +TextEditor > .text-editor--active-line-gutter { + color: $text; + background: $panel-lighten-1; +} +TextEditor > .text-editor--gutter { + color: $text-muted; + background: $background; +} +TextEditor > .text-editor--cursor { + color: $text; + background: white 80%; } """ @@ -42,6 +54,7 @@ class TextEditor(ScrollView, can_focus=True): "text-editor--active-line", "text-editor--active-line-gutter", "text-editor--gutter", + "text-editor--cursor", } BINDINGS = [ @@ -190,26 +203,35 @@ def render_line(self, widget_y: int) -> Strip: line_text = Text(f"{line_string} ", end="", tab_size=4) line_text.set_length(self.virtual_size.width) - # Apply highlighting to the line if necessary. + # Apply highlighting if self._highlights: highlights = self._highlights[document_y] for start, end, node_type in highlights: node_style = self._get_node_style(node_type) line_text.stylize(node_style, start, end) - # Show the cursor if necessary + # Show the cursor cursor_row, cursor_column = self.cursor_position if cursor_row == document_y: - line_text.stylize( - Style(color="black", bgcolor="white"), cursor_column, cursor_column + 1 + cursor_style = self.get_component_rich_style("text-editor--cursor") + line_text.stylize(cursor_style, cursor_column, cursor_column + 1) + active_line_style = self.get_component_rich_style( + "text-editor--active-line" ) - line_text.stylize_before(Style(bgcolor="#363636")) + line_text.stylize_before(active_line_style) + # Show the gutter if self.show_line_numbers: - gutter_style = self.get_component_rich_style("text-editor--gutter") + if cursor_row == document_y: + gutter_style = self.get_component_rich_style( + "text-editor--active-line-gutter" + ) + else: + gutter_style = self.get_component_rich_style("text-editor--gutter") + gutter_width_no_margin = self.gutter_width - 2 gutter = Text( - f"{document_y + 1:>{gutter_width_no_margin}}│ ", + f"{document_y + 1:>{gutter_width_no_margin}} ", style=gutter_style, end="", ) @@ -501,7 +523,7 @@ def insert_text(self, text: str) -> None: replacement_lines = text.splitlines(keepends=False) replacement_lines[0] = text_before_cursor + replacement_lines[0] - end_column = cell_len(replacement_lines[-1]) + end_column = len(replacement_lines[-1]) replacement_lines[-1] += text_after_cursor lines[cursor_row : cursor_row + 1] = replacement_lines From c91d3dbc875412f9c755688633be4b4e3b10a88c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 13:28:12 +0100 Subject: [PATCH 033/366] Scroll to cursor width leeway --- src/textual/widgets/_text_editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index c2bce8e163..fc88105c01 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -393,7 +393,7 @@ def scroll_cursor_visible(self): text = self.active_line_text[:column] column_offset = cell_len(text) self.scroll_to_region( - Region(x=column_offset, y=row, width=1, height=1), + Region(x=column_offset, y=row, width=3, height=1), spacing=Spacing(right=self.gutter_width), animate=False, force=True, From de0864100142fae4ac8e692425f7ac77fd59f44f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 13:37:32 +0100 Subject: [PATCH 034/366] Fix cursor up going to the wrong column --- src/textual/widgets/_text_editor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index fc88105c01..a5702d59b6 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -41,8 +41,7 @@ class TextEditor(ScrollView, can_focus=True): background: $panel-lighten-1; } TextEditor > .text-editor--gutter { - color: $text-muted; - background: $background; + color: $text-muted 40%; } TextEditor > .text-editor--cursor { color: $text; @@ -494,7 +493,7 @@ def action_cursor_up(self) -> None: target_row = max(0, cursor_row - 1) # TODO: Fetch last active column on this row target_column = clamp( - cursor_column, 0, cell_len(self.document_lines[target_row]) - 1 + cursor_column, 0, len(self.document_lines[target_row]) - 1 ) self.cursor_position = (target_row, target_column) From 3767b9f715d6a789078b4b2abd490fc4c2b940cc Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 14:38:20 +0100 Subject: [PATCH 035/366] Mouse cursor support - click to move cursor --- src/textual/widgets/_text_editor.py | 38 ++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index a5702d59b6..3c90dc3a0d 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import ClassVar, Iterable, NamedTuple +from rich.cells import get_character_cell_size from rich.segment import Segment from rich.style import Style from rich.text import Text @@ -33,12 +34,14 @@ class Highlight(NamedTuple): class TextEditor(ScrollView, can_focus=True): DEFAULT_CSS = """\ +$editor-active-line-bg: white 8%; + TextEditor > .text-editor--active-line { - background: $panel-lighten-1; + background: $editor-active-line-bg; } TextEditor > .text-editor--active-line-gutter { color: $text; - background: $panel-lighten-1; + background: $editor-active-line-bg; } TextEditor > .text-editor--gutter { color: $text-muted 40%; @@ -95,15 +98,6 @@ def __init__( self._highlights: dict[int, list[Highlight]] = defaultdict(list) """Mapping line numbers to the set of cached highlights for that line.""" - # TODO - currently unused - self._line_cache: dict[int, list[Segment]] = defaultdict(list) - """Caches segments for lines. Note that a line may span multiple y-offsets - due to wrapping. These segments do NOT include the cursor highlighting. - A portion of the line cache will be updated when an edit operation occurs - or when a file is loaded for the first time. - Tree sitter will tell us the modified ranges of the AST and we update - the corresponding line ranges in this cache.""" - # --- Abstract syntax tree and related parsing machinery self._parser: Parser | None = None """The tree-sitter parser which extracts the syntax tree from the document.""" @@ -366,6 +360,28 @@ def _on_key(self, event: events.Key) -> None: elif key == "enter": self.split_line() + def _on_click(self, event: events.Click) -> None: + """Clicking the content body moves the cursor.""" + + offset = event.get_content_offset(self) + if offset is None: + return + + event.stop() + + target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0) + target_y = offset.y + int(self.scroll_y) + + line = self.document_lines[target_y] + cell_offset = 0 + for index, character in enumerate(line): + if cell_offset >= target_x: + self.cursor_position = (target_y, index) + break + cell_offset += get_character_cell_size(character) + else: + self.cursor_position = (target_y, len(line)) + def _on_paste(self, event: events.Paste) -> None: text = event.text if text: From 3ded43138f948ffc5e2b6a54e1e7964fb993e644 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 14:41:59 +0100 Subject: [PATCH 036/366] Fix off-by-one in cursor up --- src/textual/widgets/_text_editor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 3c90dc3a0d..3c83b9cc3b 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -508,9 +508,7 @@ def action_cursor_up(self) -> None: target_row = max(0, cursor_row - 1) # TODO: Fetch last active column on this row - target_column = clamp( - cursor_column, 0, len(self.document_lines[target_row]) - 1 - ) + target_column = clamp(cursor_column, 0, len(self.document_lines[target_row])) self.cursor_position = (target_row, target_column) From df8bcf6ff9e246bce7bd359256ce4b632cc65c06 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 4 Jul 2023 16:43:58 +0100 Subject: [PATCH 037/366] Cacheing tree query-based syntax highlgihts --- src/textual/widgets/_text_editor.py | 282 +++++++++++++++++----------- tree-sitter/highlights/python.scm | 126 +++++++++++++ 2 files changed, 299 insertions(+), 109 deletions(-) create mode 100644 tree-sitter/highlights/python.scm diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 3c83b9cc3b..67c2b49a2d 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -10,6 +10,7 @@ from rich.style import Style from rich.text import Text from tree_sitter import Language, Node, Parser, Tree +from tree_sitter.binding import Query from textual import events, log from textual._cells import cell_len @@ -19,9 +20,18 @@ from textual.scroll_view import ScrollView from textual.strip import Strip -LANGUAGES_PATH = ( - Path(__file__) / "../../../../tree-sitter-languages/textual-languages.so" -) +TREE_SITTER_PATH = Path(__file__) / "../../../../tree-sitter/" +LANGUAGES_PATH = TREE_SITTER_PATH / "textual-languages.so" + +# TODO - remove hardcoded python.scm highlight query file +HIGHLIGHTS_PATH = TREE_SITTER_PATH / "highlights/python.scm" + + +HIGHLIGHT_STYLES = { + "string": Style(color="green"), + "comment": Style(dim=True), + "keyword": Style(bgcolor="red"), +} class Highlight(NamedTuple): @@ -29,7 +39,7 @@ class Highlight(NamedTuple): start_column: int | None end_column: int | None - node: Node + highlight_name: str | None class TextEditor(ScrollView, can_focus=True): @@ -98,7 +108,11 @@ def __init__( self._highlights: dict[int, list[Highlight]] = defaultdict(list) """Mapping line numbers to the set of cached highlights for that line.""" + self._highlights_query: str | None = None + """The string containing the tree-sitter AST query used for syntax highlighting.""" + # --- Abstract syntax tree and related parsing machinery + self._language: Language | None = None self._parser: Parser | None = None """The tree-sitter parser which extracts the syntax tree from the document.""" self._ast: Tree | None = None @@ -111,11 +125,12 @@ def watch_language(self, new_language: str | None) -> None: from our tree-sitter library file. If the language reactive is set to None, then the no parser is used.""" if new_language: - language = Language(LANGUAGES_PATH.resolve(), new_language) + self._language = Language(LANGUAGES_PATH.resolve(), new_language) parser = Parser() self._parser = parser - self._parser.set_language(language) - self._ast = self._build_ast(parser, self.document_lines) + self._parser.set_language(self._language) + self._ast = self._build_ast(parser) + self._highlights_query = Path(HIGHLIGHTS_PATH.resolve()).read_text() else: self._ast = None @@ -129,28 +144,27 @@ def watch__document_size(self, size: Size) -> None: def _build_ast( self, parser: Parser, - document_lines: list[str], ) -> Tree | None: """Fully parse the document and build the abstract syntax tree for it. Returns None if there's no parser available (e.g. when no language is selected). """ - - def read_callable(byte_offset, point): - row, column = point - row_out_of_bounds = row >= len(document_lines) - column_out_of_bounds = not row_out_of_bounds and column >= len( - document_lines[row] - ) - if row_out_of_bounds or column_out_of_bounds: - return None - return document_lines[row][column:].encode("utf8") - if parser: - return parser.parse(read_callable) + return parser.parse(self._read_callable) else: return None + def _read_callable(self, byte_offset, point): + row, column = point + lines = self.document_lines + row_out_of_bounds = row >= len(lines) + column_out_of_bounds = not row_out_of_bounds and column > len(lines[row]) + if row_out_of_bounds or column_out_of_bounds: + return None + if column == len(lines[row]): + return "\n".encode("utf-8") + return self.document_lines[row][column:].encode("utf8") + def load_text(self, text: str) -> None: """Load text from a string into the editor.""" lines = text.splitlines(keepends=False) @@ -161,13 +175,12 @@ def load_lines(self, lines: list[str]) -> None: self.document_lines = lines # TODO Offer maximum line width and wrap if needed - print("setting vs in load_lines") self._document_size = self._get_document_size(lines) # TODO - clear caches if self._parser is not None: - self._ast = self._build_ast(self._parser, lines) - self._cache_highlights(self._ast.walk(), lines) + self._ast = self._build_ast(self._parser) + self._prepare_highlights() log.debug(f"loaded text. parser = {self._parser} ast = {self._ast}") @@ -197,10 +210,11 @@ def render_line(self, widget_y: int) -> Strip: line_text.set_length(self.virtual_size.width) # Apply highlighting + null_style = Style.null() if self._highlights: highlights = self._highlights[document_y] - for start, end, node_type in highlights: - node_style = self._get_node_style(node_type) + for start, end, highlight_name in highlights: + node_style = HIGHLIGHT_STYLES.get(highlight_name, null_style) line_text.stylize(node_style, start, end) # Show the cursor @@ -258,91 +272,119 @@ def gutter_width(self) -> int: ) return gutter_longest_number - def _get_node_style(self, node: Node) -> Style: - # Apply simple highlighting to the node based on its type. - if node.type == "identifier": - style = Style(color="cyan") - elif node.type == "string": - style = Style(color="green") - elif node.type == "import_from_statement": - style = Style(bgcolor="magenta") - else: - style = Style.null() - return style - - def _cache_highlights( - self, - cursor, - document: list[str], - line_range: tuple[int, int] | None = None, - ) -> None: - """Traverse the AST and highlight the document. - - Args: - cursor: The tree-sitter Tree cursor. - document: The document as a list of strings. - line_range: The start and end line index that is visible. If None, highlight the whole document. - """ - - # TODO: Instead of traversing the AST, use AST queries and the - # .scm files from tree-sitter GitHub org for highlighting. - - reached_root = False - - while not reached_root: - # The range of the document (line indices) that we want to highlight. - if line_range is not None: - window_start, window_end = line_range + # --- Syntax highlighting + def _prepare_highlights(self) -> None: + scroll_y = int(self.scroll_y) + visible_start = scroll_y + visible_end = scroll_y + self.size.height + 1 + visible_range = range(visible_start, visible_end) + + # TODO - pass in the visible range to Query.captures + # to ensure we only query for nodes which are currently relevant. + # See py-tree-sitter readme, the pattern-matching section. + query: Query = self._language.query(self._highlights_query) + captures = query.captures(self._ast.root_node) + + highlight_cache = self._highlights + for capture in captures: + print(capture) + node, highlight_name = capture + node_start_row, node_start_column = node.start_point + node_end_row, node_end_column = node.end_point + + if node_start_row == node_end_row: + highlight = Highlight( + node_start_column, node_end_column, highlight_name + ) + highlight_cache[node_start_row].append(highlight) else: - window_start = 0 - window_end = len(document) - 1 - - # Get the range of this node - node_start_row, node_start_column = cursor.node.start_point - node_end_row, node_end_column = cursor.node.end_point - - node_in_window = line_range is None or ( - window_start <= node_end_row and window_end >= node_start_row - ) - - # Cache the highlight data for this node if it's within the window range - # At this point we're not actually looking at the document at all, we're - # just storing data on the locations to highlight within the document. - # This data will be referenced only when we render. - if node_in_window: - highlight_cache = self._highlights - node = cursor.node - if node_start_row == node_end_row: - highlight = Highlight(node_start_column, node_end_column, node) - highlight_cache[node_start_row].append(highlight) - else: - # Add the first line - highlight_cache[node_start_row].append( - Highlight(node_start_column, None, node) - ) - # Add the middle lines - entire row of this node is highlighted - for node_row in range(node_start_row + 1, node_end_row): - highlight_cache[node_row].append(Highlight(0, None, node)) - - # Add the last line - highlight_cache[node_end_row].append( - Highlight(0, node_end_column, node) - ) - - if cursor.goto_first_child(): - continue - - if cursor.goto_next_sibling(): - continue + # Add the first line + highlight_cache[node_start_row].append( + Highlight(node_start_column, None, highlight_name) + ) + # Add the middle lines - entire row of this node is highlighted + for node_row in range(node_start_row + 1, node_end_row): + highlight_cache[node_row].append(Highlight(0, None, highlight_name)) - retracing = True - while retracing: - if not cursor.goto_parent(): - retracing = False - reached_root = True + # Add the last line + highlight_cache[node_end_row].append( + Highlight(0, node_end_column, highlight_name) + ) - if cursor.goto_next_sibling(): - retracing = False + # + # def _cache_highlights( + # self, + # cursor, + # document: list[str], + # line_range: tuple[int, int] | None = None, + # ) -> None: + # """Traverse the AST and highlight the document. + # + # Args: + # cursor: The tree-sitter Tree cursor. + # document: The document as a list of strings. + # line_range: The start and end line index that is visible. If None, highlight the whole document. + # """ + # + # # TODO: Instead of traversing the AST, use AST queries and the + # # .scm files from tree-sitter GitHub org for highlighting. + # + # reached_root = False + # + # while not reached_root: + # # The range of the document (line indices) that we want to highlight. + # if line_range is not None: + # window_start, window_end = line_range + # else: + # window_start = 0 + # window_end = len(document) - 1 + # + # # Get the range of this node + # node_start_row, node_start_column = cursor.node.start_point + # node_end_row, node_end_column = cursor.node.end_point + # + # node_in_window = line_range is None or ( + # window_start <= node_end_row and window_end >= node_start_row + # ) + # + # # Cache the highlight data for this node if it's within the window range + # # At this point we're not actually looking at the document at all, we're + # # just storing data on the locations to highlight within the document. + # # This data will be referenced only when we render. + # if node_in_window: + # highlight_cache = self._highlights + # node = cursor.node + # if node_start_row == node_end_row: + # highlight = Highlight(node_start_column, node_end_column, node) + # highlight_cache[node_start_row].append(highlight) + # else: + # # Add the first line + # highlight_cache[node_start_row].append( + # Highlight(node_start_column, None, node) + # ) + # # Add the middle lines - entire row of this node is highlighted + # for node_row in range(node_start_row + 1, node_end_row): + # highlight_cache[node_row].append(Highlight(0, None, node)) + # + # # Add the last line + # highlight_cache[node_end_row].append( + # Highlight(0, node_end_column, node) + # ) + # + # if cursor.goto_first_child(): + # continue + # + # if cursor.goto_next_sibling(): + # continue + # + # retracing = True + # while retracing: + # if not cursor.goto_parent(): + # retracing = False + # reached_root = True + # + # if cursor.goto_next_sibling(): + # retracing = False # --- Lower level event/key handling def _on_key(self, event: events.Key) -> None: @@ -526,6 +568,10 @@ def active_line_text(self) -> str: # --- Editor operations def insert_text(self, text: str) -> None: log.debug(f"insert {text!r} at {self.cursor_position!r}") + + start_byte = self._position_to_byte_offset(self.cursor_position) + start_point = self.cursor_position + cursor_row, cursor_column = self.cursor_position lines = self.document_lines @@ -554,9 +600,27 @@ def insert_text(self, text: str) -> None: self._document_size = Size(new_document_width, new_document_height) self.cursor_position = (cursor_row + len(replacement_lines) - 1, end_column) - print("final_insert = ", replacement_lines) + self._ast.edit( + start_byte=start_byte, + old_end_byte=start_byte, + new_end_byte=start_byte + len(text), + start_point=start_point, + old_end_point=start_point, + new_end_point=self.cursor_position, + ) + self._ast = self._parser.parse(self._read_callable, self._ast) + self._prepare_highlights() - # TODO: Need to update the AST to inform it of the edit operation + def _position_to_byte_offset(self, position: tuple[int, int]) -> int: + """Given a document coordinate, return the byte offset of that coordinate.""" + + # TODO - this assumes all line endings are a single byte `\n` + lines = self.document_lines + row, column = position + lines_above = lines[:row] + bytes_lines_above = sum(len(line) + 1 for line in lines_above) + bytes_this_line_left_of_cursor = len(lines[row][:column]) + return bytes_lines_above + bytes_this_line_left_of_cursor def split_line(self): cursor_row, cursor_column = self.cursor_position diff --git a/tree-sitter/highlights/python.scm b/tree-sitter/highlights/python.scm new file mode 100644 index 0000000000..90fcf1c4b7 --- /dev/null +++ b/tree-sitter/highlights/python.scm @@ -0,0 +1,126 @@ +; Identifier naming conventions + +((identifier) @constructor + (#match? @constructor "^[A-Z]")) + +((identifier) @constant + (#match? @constant "^[A-Z][A-Z_]*$")) + +; Builtin functions + +((call + function: (identifier) @function.builtin) + (#match? + @function.builtin + "^(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__)$")) + +; Function calls + +(decorator) @function + +(call + function: (attribute attribute: (identifier) @function.method)) +(call + function: (identifier) @function) + +; Function definitions + +(function_definition + name: (identifier) @function) + +(identifier) @variable +(attribute attribute: (identifier) @property) +(type (identifier) @type) + +; Literals + +[ + (none) + (true) + (false) +] @constant.builtin + +[ + (integer) + (float) +] @number + +(comment) @comment +(string) @string +(escape_sequence) @escape + +(interpolation + "{" @punctuation.special + "}" @punctuation.special) @embedded + +[ + "-" + "-=" + "!=" + "*" + "**" + "**=" + "*=" + "/" + "//" + "//=" + "/=" + "&" + "%" + "%=" + "^" + "+" + "->" + "+=" + "<" + "<<" + "<=" + "<>" + "=" + ":=" + "==" + ">" + ">=" + ">>" + "|" + "~" + "and" + "in" + "is" + "not" + "or" +] @operator + +[ + "as" + "assert" + "async" + "await" + "break" + "class" + "continue" + "def" + "del" + "elif" + "else" + "except" + "exec" + "finally" + "for" + "from" + "global" + "if" + "import" + "lambda" + "nonlocal" + "pass" + "print" + "raise" + "return" + "try" + "while" + "with" + "yield" + "match" + "case" +] @keyword From b022d8e9d50ce7e55cdcd4bda09258c81656461e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 5 Jul 2023 16:39:40 +0100 Subject: [PATCH 038/366] Syntax highlighting improvements --- src/textual/widgets/_text_editor.py | 342 ++++++++++++++++++---------- tree-sitter/highlights/python.scm | 335 ++++++++++++++++++++++----- 2 files changed, 494 insertions(+), 183 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 67c2b49a2d..fe3ab26cda 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -1,12 +1,11 @@ from __future__ import annotations -import os from collections import defaultdict +from dataclasses import dataclass from pathlib import Path from typing import ClassVar, Iterable, NamedTuple from rich.cells import get_character_cell_size -from rich.segment import Segment from rich.style import Style from rich.text import Text from tree_sitter import Language, Node, Parser, Tree @@ -15,7 +14,7 @@ from textual import events, log from textual._cells import cell_len from textual.binding import Binding -from textual.geometry import Region, Size, Spacing, clamp +from textual.geometry import Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView from textual.strip import Strip @@ -26,11 +25,27 @@ # TODO - remove hardcoded python.scm highlight query file HIGHLIGHTS_PATH = TREE_SITTER_PATH / "highlights/python.scm" - +# TODO - temporary proof of concept approach HIGHLIGHT_STYLES = { - "string": Style(color="green"), - "comment": Style(dim=True), - "keyword": Style(bgcolor="red"), + "string": Style(color="#E6DB74"), + "string.documentation": Style(color="yellow"), + "comment": Style(color="#75715E"), + "keyword": Style(color="#F92672"), + "include": Style(color="#F92672"), + "keyword.function": Style(color="#F92672"), + "keyword.return": Style(color="#F92672"), + "conditional": Style(color="#F92672"), + "number": Style(color="#AE81FF"), + "class": Style(color="#A6E22E"), + "function": Style(color="#A6E22E"), + "function.call": Style(color="#A6E22E"), + "method": Style(color="#A6E22E"), + "method.call": Style(color="#A6E22E"), + # "constant": Style(color="#AE81FF"), + "variable": Style(color="white"), + "parameter": Style(color="cyan"), + "type": Style(color="cyan"), + "escape": Style(bgcolor="magenta"), } @@ -273,21 +288,29 @@ def gutter_width(self) -> int: return gutter_longest_number # --- Syntax highlighting - def _prepare_highlights(self) -> None: - scroll_y = int(self.scroll_y) - visible_start = scroll_y - visible_end = scroll_y + self.size.height + 1 - visible_range = range(visible_start, visible_end) - - # TODO - pass in the visible range to Query.captures - # to ensure we only query for nodes which are currently relevant. - # See py-tree-sitter readme, the pattern-matching section. + def _prepare_highlights( + self, + start_point: tuple[int, int] | None = None, + end_point: tuple[int, int] = None, + ) -> None: + # TODO - we're ignoring get changed ranges for now. Either I'm misunderstanding + # it or I've made a mistake somewhere with AST editing. + + highlights = self._highlights query: Query = self._language.query(self._highlights_query) - captures = query.captures(self._ast.root_node) - highlight_cache = self._highlights + log.debug(f"capturing nodes in range {start_point!r} -> {end_point!r}") + + captures_kwargs = {} + if start_point is not None: + captures_kwargs["start_point"] = start_point + if end_point is not None: + captures_kwargs["end_point"] = end_point + + captures = query.captures(self._ast.root_node, **captures_kwargs) + + highlight_updates: dict[int, list[Highlight]] = defaultdict(list) for capture in captures: - print(capture) node, highlight_name = capture node_start_row, node_start_column = node.start_point node_end_row, node_end_column = node.end_point @@ -296,95 +319,25 @@ def _prepare_highlights(self) -> None: highlight = Highlight( node_start_column, node_end_column, highlight_name ) - highlight_cache[node_start_row].append(highlight) + highlight_updates[node_start_row].append(highlight) else: # Add the first line - highlight_cache[node_start_row].append( + highlight_updates[node_start_row].append( Highlight(node_start_column, None, highlight_name) ) # Add the middle lines - entire row of this node is highlighted for node_row in range(node_start_row + 1, node_end_row): - highlight_cache[node_row].append(Highlight(0, None, highlight_name)) + highlight_updates[node_row].append( + Highlight(0, None, highlight_name) + ) # Add the last line - highlight_cache[node_end_row].append( + highlight_updates[node_end_row].append( Highlight(0, node_end_column, highlight_name) ) - # - # def _cache_highlights( - # self, - # cursor, - # document: list[str], - # line_range: tuple[int, int] | None = None, - # ) -> None: - # """Traverse the AST and highlight the document. - # - # Args: - # cursor: The tree-sitter Tree cursor. - # document: The document as a list of strings. - # line_range: The start and end line index that is visible. If None, highlight the whole document. - # """ - # - # # TODO: Instead of traversing the AST, use AST queries and the - # # .scm files from tree-sitter GitHub org for highlighting. - # - # reached_root = False - # - # while not reached_root: - # # The range of the document (line indices) that we want to highlight. - # if line_range is not None: - # window_start, window_end = line_range - # else: - # window_start = 0 - # window_end = len(document) - 1 - # - # # Get the range of this node - # node_start_row, node_start_column = cursor.node.start_point - # node_end_row, node_end_column = cursor.node.end_point - # - # node_in_window = line_range is None or ( - # window_start <= node_end_row and window_end >= node_start_row - # ) - # - # # Cache the highlight data for this node if it's within the window range - # # At this point we're not actually looking at the document at all, we're - # # just storing data on the locations to highlight within the document. - # # This data will be referenced only when we render. - # if node_in_window: - # highlight_cache = self._highlights - # node = cursor.node - # if node_start_row == node_end_row: - # highlight = Highlight(node_start_column, node_end_column, node) - # highlight_cache[node_start_row].append(highlight) - # else: - # # Add the first line - # highlight_cache[node_start_row].append( - # Highlight(node_start_column, None, node) - # ) - # # Add the middle lines - entire row of this node is highlighted - # for node_row in range(node_start_row + 1, node_end_row): - # highlight_cache[node_row].append(Highlight(0, None, node)) - # - # # Add the last line - # highlight_cache[node_end_row].append( - # Highlight(0, node_end_column, node) - # ) - # - # if cursor.goto_first_child(): - # continue - # - # if cursor.goto_next_sibling(): - # continue - # - # retracing = True - # while retracing: - # if not cursor.goto_parent(): - # retracing = False - # reached_root = True - # - # if cursor.goto_next_sibling(): - # retracing = False + for line_index, updated_highlights in highlight_updates.items(): + highlights[line_index] = updated_highlights # --- Lower level event/key handling def _on_key(self, event: events.Key) -> None: @@ -591,7 +544,7 @@ def insert_text(self, text: str) -> None: document_width, document_height = self._document_size # The virtual width of the row is the cell length of the text in the row - # plus 1 to accommodate for a cursor potentially "resting" at the end of the row. + # plus 1 to accommodate for a cursor potentially "resting" at the end of the row insertion_width = longest_modified_line + 1 new_document_width = max(insertion_width, document_width) @@ -600,16 +553,39 @@ def insert_text(self, text: str) -> None: self._document_size = Size(new_document_width, new_document_height) self.cursor_position = (cursor_row + len(replacement_lines) - 1, end_column) - self._ast.edit( - start_byte=start_byte, - old_end_byte=start_byte, - new_end_byte=start_byte + len(text), - start_point=start_point, - old_end_point=start_point, - new_end_point=self.cursor_position, + edit_args = { + "start_byte": start_byte, + "old_end_byte": start_byte, + "new_end_byte": start_byte + len(text), # TODO - what about newlines? + "start_point": start_point, + "old_end_point": start_point, + "new_end_point": self.cursor_position, + } + log.debug(edit_args) + self._ast.edit(**edit_args) + + old_tree = self._ast + self._ast = self._parser.parse(self._read_callable, old_tree) + + # Limit the range, rather arbitrarily for now. + # Perhaps we do the incremental parsing within a window here, then have some + # heuristic for wider parsing inside on_idle? + scroll_y = max(0, int(self.scroll_y)) + + visible_start_line = scroll_y + height = self.region.height or len(self.document_lines) - 1 + visible_end_line = scroll_y + height + + highlight_window_leeway = 10 + start_point = (max(0, visible_start_line - highlight_window_leeway), 0) + + end_row_index = min( + len(self.document_lines) - 1, visible_end_line + highlight_window_leeway ) - self._ast = self._parser.parse(self._read_callable, self._ast) - self._prepare_highlights() + end_line = self.document_lines[end_row_index] + end_point = (end_row_index, len(end_line) - 1) + + self._prepare_highlights(start_point, end_point) def _position_to_byte_offset(self, position: tuple[int, int]) -> int: """Given a document coordinate, return the byte offset of that coordinate.""" @@ -676,24 +652,38 @@ def traverse(cursor) -> Iterable[Node]: def action_print_highlight_cache(self) -> None: log.debug(self._highlights) - def debug_state(self) -> str: - return f"""\ -cursor {self.cursor_position!r} -language {self.language!r} -document_size {self._document_size!r} -virtual_size {self.virtual_size!r} -scroll {(self.scroll_x, self.scroll_y)!r} - -[b]LINE INDEX {self.cursor_position[0]} (cell_len={cell_len(self.active_line_text)})[/] -{self.active_line_text!r} -""" - - def debug_highlights(self) -> str: - return f"""\ -highlight cache keys (rows) {len(self._highlights)} -highlight cache total size {sum(len(highlights) for key, highlights in self._highlights.items())} -current row highlight cache size {len(self._highlights[self.cursor_position[0]])} -""" + @dataclass + class EditorDebug: + cursor: tuple[int, int] + language: str + document_size: Size + virtual_size: Size + scroll: Offset + active_line_text: str + active_line_cell_len: int + highlight_cache_key_count: int + highlight_cache_total_size: int + highlight_cache_current_row_size: int + highlight_cache_current_row: list[Highlight] + + def debug_state(self) -> "EditorDebug": + return self.EditorDebug( + cursor=self.cursor_position, + language=self.language, + document_size=self._document_size, + virtual_size=self.virtual_size, + scroll=self.scroll_offset, + active_line_text=repr(self.active_line_text), + active_line_cell_len=cell_len(self.active_line_text), + highlight_cache_key_count=len(self._highlights), + highlight_cache_total_size=sum( + len(highlights) for key, highlights in self._highlights.items() + ), + highlight_cache_current_row_size=len( + self._highlights[self.cursor_position[0]] + ), + highlight_cache_current_row=self._highlights[self.cursor_position[0]], + ) if __name__ == "__main__": @@ -748,3 +738,105 @@ def read_callable(byte_offset, point): tree = parser.parse(bytes(CODE, "utf-8")) print(list(traverse_tree(tree.walk()))) + +# from pathlib import Path +# +# from rich.pretty import Pretty +# +# from textual.app import App, ComposeResult +# from textual.binding import Binding +# from textual.widgets import Footer, Static +# from textual.widgets._text_editor import TextEditor +# +# SAMPLE_TEXT = [ +# "Hello, world!", +# "", +# "你好,世界!", # Chinese characters, which are usually double-width +# "こんにちは、世界!", # Japanese characters, also usually double-width +# "안녕하세요, 세계!", # Korean characters, also usually double-width +# " This line has leading white space", +# "This line has trailing white space ", +# " This line has both leading and trailing white space ", +# " ", # Line with only spaces +# "こんにちは、world! 你好,world!", # Mixed script line +# "Hello, 🌍! Hello, 🌏! Hello, 🌎!", # Line with emoji (which are often double-width) +# "The quick brown 🦊 jumps over the lazy 🐶.", # Line with emoji interspersed in text +# "Special characters: ~!@#$%^&*()_+`-={}|[]\\:\";'<>?,./", +# # Line with special characters +# "Unicode example: Привет, мир!", # Russian text +# "Unicode example: Γειά σου Κόσμε!", # Greek text +# "Unicode example: مرحبا بك في", # Arabic text +# ] +# +# PYTHON_SNIPPET = """\ +# def render_line(self, y: int) -> Strip: +# '''Render a line of the widget. y is relative to the top of the widget.''' +# +# row_index = y // 4 # A checkerboard square consists of 4 rows +# +# if row_index >= 8: # Generate blank lines when we reach the end +# return Strip.blank(self.size.width) +# +# is_odd = row_index % 2 # Used to alternate the starting square on each row +# +# white = Style.parse("on white") # Get a style object for a white background +# black = Style.parse("on black") # Get a style object for a black background +# +# # Generate a list of segments with alternating black and white space characters +# segments = [ +# Segment(" " * 8, black if (column + is_odd) % 2 else white) +# for column in range(8) +# ] +# strip = Strip(segments, 8 * 8) +# return strip +# """ +# +# +# class TextEditorDemo(App): +# CSS = """\ +# TextEditor { +# height: 18; +# background: $panel; +# } +# +# #debug { +# border: wide $primary; +# padding: 1 2; +# } +# +# """ +# +# BINDINGS = [ +# Binding("ctrl+p", "load_python", "Load Python") +# ] +# +# def compose(self) -> ComposeResult: +# text_area = TextEditor() +# text_area.language = "python" +# code_path = Path( +# "/Users/darrenburns/Code/textual/src/textual/widgets/_data_table.py") +# text_area.load_text(code_path.read_text()) +# yield text_area +# yield Footer() +# Static.can_focus = True +# yield Static(id="debug") +# +# def on_mount(self): +# editor = self.query_one(TextEditor) +# self.watch(editor, "cursor_position", self.update_debug) +# self.watch(editor, "language", self.update_debug) +# +# def update_debug(self): +# editor = self.query_one(TextEditor) +# debug = self.query_one("#debug") +# debug.update(Pretty(editor.debug_state())) +# +# def key_d(self): +# editor = self.query_one(TextEditor) +# for item in list(editor._highlights.items())[:10]: +# print(item) +# +# +# app = TextEditorDemo() +# if __name__ == '__main__': +# app.run() diff --git a/tree-sitter/highlights/python.scm b/tree-sitter/highlights/python.scm index 90fcf1c4b7..c18b748674 100644 --- a/tree-sitter/highlights/python.scm +++ b/tree-sitter/highlights/python.scm @@ -1,61 +1,182 @@ -; Identifier naming conventions +;; From tree-sitter-python licensed under MIT License +; Copyright (c) 2016 Max Brunsfeld -((identifier) @constructor - (#match? @constructor "^[A-Z]")) +; Variables +(identifier) @variable + +; Reset highlighting in f-string interpolations +(interpolation) @none +;; Identifier naming conventions +((identifier) @type + (#lua-match? @type "^[A-Z].*[a-z]")) ((identifier) @constant - (#match? @constant "^[A-Z][A-Z_]*$")) + (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) -; Builtin functions +((identifier) @constant.builtin + (#lua-match? @constant.builtin "^__[a-zA-Z0-9_]*__$")) -((call - function: (identifier) @function.builtin) - (#match? - @function.builtin - "^(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__)$")) +((identifier) @constant.builtin + (#any-of? @constant.builtin + ;; https://docs.python.org/3/library/constants.html + "NotImplemented" + "Ellipsis" + "quit" + "exit" + "copyright" + "credits" + "license")) -; Function calls +((attribute + attribute: (identifier) @field) + (#lua-match? @field "^[%l_].*$")) + +((assignment + left: (identifier) @type.definition + (type (identifier) @_annotation)) + (#eq? @_annotation "TypeAlias")) -(decorator) @function +((assignment + left: (identifier) @type.definition + right: (call + function: (identifier) @_func)) + (#any-of? @_func "TypeVar" "NewType")) + +; Function calls (call - function: (attribute attribute: (identifier) @function.method)) + function: (identifier) @function.call) + (call - function: (identifier) @function) + function: (attribute + attribute: (identifier) @method.call)) + +((call + function: (identifier) @constructor) + (#lua-match? @constructor "^%u")) + +((call + function: (attribute + attribute: (identifier) @constructor)) + (#lua-match? @constructor "^%u")) -; Function definitions +;; Decorators + +((decorator "@" @attribute) + (#set! "priority" 101)) + +(decorator + (identifier) @attribute) +(decorator + (attribute + attribute: (identifier) @attribute)) +(decorator + (call (identifier) @attribute)) +(decorator + (call (attribute + attribute: (identifier) @attribute))) + +((decorator + (identifier) @attribute.builtin) + (#any-of? @attribute.builtin "classmethod" "property")) + +;; Builtin functions + +((call + function: (identifier) @function.builtin) + (#any-of? @function.builtin + "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__")) + +;; Function definitions (function_definition name: (identifier) @function) -(identifier) @variable -(attribute attribute: (identifier) @property) (type (identifier) @type) +(type + (subscript + (identifier) @type)) ; type subscript: Tuple[int] -; Literals +((call + function: (identifier) @_isinstance + arguments: (argument_list + (_) + (identifier) @type)) + (#eq? @_isinstance "isinstance")) -[ - (none) - (true) - (false) -] @constant.builtin +;; Normal parameters +(parameters + (identifier) @parameter) +;; Lambda parameters +(lambda_parameters + (identifier) @parameter) +(lambda_parameters + (tuple_pattern + (identifier) @parameter)) +; Default parameters +(keyword_argument + name: (identifier) @parameter) +; Naming parameters on call-site +(default_parameter + name: (identifier) @parameter) +(typed_parameter + (identifier) @parameter) +(typed_default_parameter + (identifier) @parameter) +; Variadic parameters *args, **kwargs +(parameters + (list_splat_pattern ; *args + (identifier) @parameter)) +(parameters + (dictionary_splat_pattern ; **kwargs + (identifier) @parameter)) -[ - (integer) - (float) -] @number -(comment) @comment +;; Literals + +(none) @constant.builtin +[(true) (false)] @boolean +((identifier) @variable.builtin + (#eq? @variable.builtin "self")) +((identifier) @variable.builtin + (#eq? @variable.builtin "cls")) + +(integer) @number +(float) @float + +(comment) @comment @spell + +((module . (comment) @preproc) + (#lua-match? @preproc "^#!/")) + (string) @string -(escape_sequence) @escape +(escape_sequence) @string.escape -(interpolation - "{" @punctuation.special - "}" @punctuation.special) @embedded +; doc-strings + +(module . (expression_statement (string) @string.documentation @spell)) + +(class_definition + body: + (block + . (expression_statement (string) @string.documentation @spell))) + +(function_definition + body: + (block + . (expression_statement (string) @string.documentation @spell))) + +; Tokens [ "-" "-=" + ":=" "!=" "*" "**" @@ -66,61 +187,159 @@ "//=" "/=" "&" + "&=" "%" "%=" "^" + "^=" "+" - "->" "+=" "<" "<<" + "<<=" "<=" "<>" "=" - ":=" "==" ">" ">=" ">>" + ">>=" + "@" + "@=" "|" + "|=" "~" + "->" +] @operator + +; Keywords +[ "and" "in" "is" "not" "or" -] @operator + "is not" + "not in" + + "del" +] @keyword.operator + +[ + "def" + "lambda" +] @keyword.function [ - "as" "assert" - "async" - "await" - "break" "class" - "continue" - "def" - "del" - "elif" - "else" - "except" "exec" - "finally" - "for" - "from" "global" - "if" - "import" - "lambda" "nonlocal" "pass" "print" - "raise" - "return" - "try" - "while" "with" - "yield" - "match" - "case" + "as" ] @keyword + +[ + "async" + "await" +] @keyword.coroutine + +[ + "return" + "yield" +] @keyword.return +(yield "from" @keyword.return) + +(future_import_statement + "from" @include + "__future__" @constant.builtin) +(import_from_statement "from" @include) +"import" @include + +(aliased_import "as" @include) + +["if" "elif" "else" "match" "case"] @conditional + +["for" "while" "break" "continue"] @repeat + +[ + "try" + "except" + "except*" + "raise" + "finally" +] @exception + +(raise_statement "from" @exception) + +(try_statement + (else_clause + "else" @exception)) + +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + +(interpolation + "{" @punctuation.special + "}" @punctuation.special) + +(type_conversion) @function.macro + +["," "." ":" ";" (ellipsis)] @punctuation.delimiter + +;; Class definitions + +(class_definition name: (identifier) @type) + +(class_definition + body: (block + (function_definition + name: (identifier) @method))) + +(class_definition + superclasses: (argument_list + (identifier) @type)) + +((class_definition + body: (block + (expression_statement + (assignment + left: (identifier) @field)))) + (#lua-match? @field "^%l.*$")) +((class_definition + body: (block + (expression_statement + (assignment + left: (_ + (identifier) @field))))) + (#lua-match? @field "^%l.*$")) + +((class_definition + (block + (function_definition + name: (identifier) @constructor))) + (#any-of? @constructor "__new__" "__init__")) + +((identifier) @type.builtin + (#any-of? @type.builtin + ;; https://docs.python.org/3/library/exceptions.html + "BaseException" "Exception" "ArithmeticError" "BufferError" "LookupError" "AssertionError" "AttributeError" + "EOFError" "FloatingPointError" "GeneratorExit" "ImportError" "ModuleNotFoundError" "IndexError" "KeyError" + "KeyboardInterrupt" "MemoryError" "NameError" "NotImplementedError" "OSError" "OverflowError" "RecursionError" + "ReferenceError" "RuntimeError" "StopIteration" "StopAsyncIteration" "SyntaxError" "IndentationError" "TabError" + "SystemError" "SystemExit" "TypeError" "UnboundLocalError" "UnicodeError" "UnicodeEncodeError" "UnicodeDecodeError" + "UnicodeTranslateError" "ValueError" "ZeroDivisionError" "EnvironmentError" "IOError" "WindowsError" + "BlockingIOError" "ChildProcessError" "ConnectionError" "BrokenPipeError" "ConnectionAbortedError" + "ConnectionRefusedError" "ConnectionResetError" "FileExistsError" "FileNotFoundError" "InterruptedError" + "IsADirectoryError" "NotADirectoryError" "PermissionError" "ProcessLookupError" "TimeoutError" "Warning" + "UserWarning" "DeprecationWarning" "PendingDeprecationWarning" "SyntaxWarning" "RuntimeWarning" + "FutureWarning" "ImportWarning" "UnicodeWarning" "BytesWarning" "ResourceWarning" + ;; https://docs.python.org/3/library/stdtypes.html + "bool" "int" "float" "complex" "list" "tuple" "range" "str" + "bytes" "bytearray" "memoryview" "set" "frozenset" "dict" "type" "object")) + +;; Error +(ERROR) @error From 8294d1249b8a5c8b161ac786e9eb68544c5a57c9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 6 Jul 2023 17:18:48 +0100 Subject: [PATCH 039/366] Pushing progress --- src/textual/widgets/_text_editor.py | 115 +++++++++++++++++++--------- 1 file changed, 78 insertions(+), 37 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index fe3ab26cda..9fffb7113a 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import ClassVar, Iterable, NamedTuple +import rich from rich.cells import get_character_cell_size from rich.style import Style from rich.text import Text @@ -130,7 +131,7 @@ def __init__( self._language: Language | None = None self._parser: Parser | None = None """The tree-sitter parser which extracts the syntax tree from the document.""" - self._ast: Tree | None = None + self._syntax_tree: Tree | None = None """The tree-sitter Tree (AST) built from the document.""" def watch_language(self, new_language: str | None) -> None: @@ -144,10 +145,10 @@ def watch_language(self, new_language: str | None) -> None: parser = Parser() self._parser = parser self._parser.set_language(self._language) - self._ast = self._build_ast(parser) + self._syntax_tree = self._build_ast(parser) self._highlights_query = Path(HIGHLIGHTS_PATH.resolve()).read_text() else: - self._ast = None + self._syntax_tree = None log.debug(f"parser set to {self._parser}") @@ -175,10 +176,13 @@ def _read_callable(self, byte_offset, point): row_out_of_bounds = row >= len(lines) column_out_of_bounds = not row_out_of_bounds and column > len(lines[row]) if row_out_of_bounds or column_out_of_bounds: - return None - if column == len(lines[row]): - return "\n".encode("utf-8") - return self.document_lines[row][column:].encode("utf8") + return_value = None + elif column == len(lines[row]) and row < len(lines): + return_value = "\n".encode("utf8") + else: + return_value = lines[row][column].encode("utf8") + print(f"(point={point!r}) (offset={byte_offset!r}) {return_value!r}") + return return_value def load_text(self, text: str) -> None: """Load text from a string into the editor.""" @@ -194,10 +198,10 @@ def load_lines(self, lines: list[str]) -> None: # TODO - clear caches if self._parser is not None: - self._ast = self._build_ast(self._parser) + self._syntax_tree = self._build_ast(self._parser) self._prepare_highlights() - log.debug(f"loaded text. parser = {self._parser} ast = {self._ast}") + log.debug(f"loaded text. parser = {self._parser} ast = {self._syntax_tree}") # --- Methods for measuring things (e.g. virtual sizes) def _get_document_size(self, document_lines: list[str]) -> Size: @@ -307,7 +311,7 @@ def _prepare_highlights( if end_point is not None: captures_kwargs["end_point"] = end_point - captures = query.captures(self._ast.root_node, **captures_kwargs) + captures = query.captures(self._syntax_tree.root_node, **captures_kwargs) highlight_updates: dict[int, list[Highlight]] = defaultdict(list) for capture in captures: @@ -365,7 +369,7 @@ def _on_click(self, event: events.Click) -> None: event.stop() target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0) - target_y = offset.y + int(self.scroll_y) + target_y = clamp(offset.y + int(self.scroll_y), 0, len(self.document_lines) - 1) line = self.document_lines[target_y] cell_offset = 0 @@ -538,7 +542,7 @@ def insert_text(self, text: str) -> None: end_column = len(replacement_lines[-1]) replacement_lines[-1] += text_after_cursor - lines[cursor_row : cursor_row + 1] = replacement_lines + self.document_lines[cursor_row : cursor_row + 1] = replacement_lines longest_modified_line = max(cell_len(line) for line in replacement_lines) document_width, document_height = self._document_size @@ -562,10 +566,16 @@ def insert_text(self, text: str) -> None: "new_end_point": self.cursor_position, } log.debug(edit_args) - self._ast.edit(**edit_args) - old_tree = self._ast - self._ast = self._parser.parse(self._read_callable, old_tree) + # Edit the tree in place + old_tree = self._syntax_tree + old_tree.edit(**edit_args) + new_tree = self._parser.parse(self._read_callable, old_tree) + + changed_ranges = old_tree.get_changed_ranges(new_tree) + + self._syntax_tree = new_tree + log.debug(f"changed = {changed_ranges!r}") # Limit the range, rather arbitrarily for now. # Perhaps we do the incremental parsing within a window here, then have some @@ -647,7 +657,7 @@ def traverse(cursor) -> Iterable[Node]: yield from traverse(cursor) cursor.goto_parent() - log.debug(list(traverse(self._ast.walk()))) + log.debug(list(traverse(self._syntax_tree.walk()))) def action_print_highlight_cache(self) -> None: log.debug(self._highlights) @@ -659,6 +669,7 @@ class EditorDebug: document_size: Size virtual_size: Size scroll: Offset + tree_sexp: str active_line_text: str active_line_cell_len: int highlight_cache_key_count: int @@ -673,6 +684,7 @@ def debug_state(self) -> "EditorDebug": document_size=self._document_size, virtual_size=self.virtual_size, scroll=self.scroll_offset, + tree_sexp=self._syntax_tree.root_node.sexp(), active_line_text=repr(self.active_line_text), active_line_cell_len=cell_len(self.active_line_text), highlight_cache_key_count=len(self._highlights), @@ -686,28 +698,28 @@ def debug_state(self) -> "EditorDebug": ) -if __name__ == "__main__": +def traverse_tree(cursor): + reached_root = False + while reached_root == False: + yield cursor.node - def traverse_tree(cursor): - reached_root = False - while reached_root == False: - yield cursor.node + if cursor.goto_first_child(): + continue - if cursor.goto_first_child(): - continue + if cursor.goto_next_sibling(): + continue - if cursor.goto_next_sibling(): - continue + retracing = True + while retracing: + if not cursor.goto_parent(): + retracing = False + reached_root = True - retracing = True - while retracing: - if not cursor.goto_parent(): - retracing = False - reached_root = True + if cursor.goto_next_sibling(): + retracing = False - if cursor.goto_next_sibling(): - retracing = False +if __name__ == "__main__": language = Language(LANGUAGES_PATH.resolve(), "python") parser = Parser() parser.set_language(language) @@ -739,6 +751,8 @@ def read_callable(byte_offset, point): print(list(traverse_tree(tree.walk()))) +# +# # from pathlib import Path # # from rich.pretty import Pretty @@ -807,7 +821,7 @@ def read_callable(byte_offset, point): # """ # # BINDINGS = [ -# Binding("ctrl+p", "load_python", "Load Python") +# Binding("ctrl+t", "traverse", "Traverse nodes") # ] # # def compose(self) -> ComposeResult: @@ -815,7 +829,13 @@ def read_callable(byte_offset, point): # text_area.language = "python" # code_path = Path( # "/Users/darrenburns/Code/textual/src/textual/widgets/_data_table.py") -# text_area.load_text(code_path.read_text()) +# +# short_code = """\ +# print("hello") +# print("world") +# """ +# text_area.load_text(short_code) +# # text_area.load_text(code_path.read_text()) # yield text_area # yield Footer() # Static.can_focus = True @@ -831,10 +851,31 @@ def read_callable(byte_offset, point): # debug = self.query_one("#debug") # debug.update(Pretty(editor.debug_state())) # -# def key_d(self): +# def action_traverse(self): +# def traverse_tree(cursor): +# reached_root = False +# while reached_root == False: +# yield cursor.node +# +# if cursor.goto_first_child(): +# continue +# +# if cursor.goto_next_sibling(): +# continue +# +# retracing = True +# while retracing: +# if not cursor.goto_parent(): +# retracing = False +# reached_root = True +# +# if cursor.goto_next_sibling(): +# retracing = False +# # editor = self.query_one(TextEditor) -# for item in list(editor._highlights.items())[:10]: -# print(item) +# nodes = list(traverse_tree(editor._syntax_tree.walk())) +# for node in nodes: +# print(node) # # # app = TextEditorDemo() From 46dd2375c45f4257cab9aa7cda37792c45e0fe89 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 12 Jul 2023 10:05:26 +0100 Subject: [PATCH 040/366] Dont pass in sexp to debug --- src/textual/widgets/_text_editor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 9fffb7113a..03721074c8 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -684,7 +684,8 @@ def debug_state(self) -> "EditorDebug": document_size=self._document_size, virtual_size=self.virtual_size, scroll=self.scroll_offset, - tree_sexp=self._syntax_tree.root_node.sexp(), + # tree_sexp=self._syntax_tree.root_node.sexp(), + tree_sexp="", active_line_text=repr(self.active_line_text), active_line_cell_len=cell_len(self.active_line_text), highlight_cache_key_count=len(self._highlights), From 104548d76ffa1206a26fe90504ea479bda325c09 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 12 Jul 2023 12:00:50 +0100 Subject: [PATCH 041/366] Tidying up, implementing backspace to delete left --- src/textual/widgets/__init__.py | 2 + src/textual/widgets/__init__.pyi | 1 + src/textual/widgets/_text_editor.py | 169 ++++++---------------------- src/textual/widgets/text_editor.py | 3 + 4 files changed, 39 insertions(+), 136 deletions(-) create mode 100644 src/textual/widgets/text_editor.py diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index a66c80935c..be59f4f5d4 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -36,6 +36,7 @@ from ._switch import Switch from ._tabbed_content import TabbedContent, TabPane from ._tabs import Tab, Tabs + from ._text_editor import TextEditor from ._text_log import TextLog from ._tooltip import Tooltip from ._tree import Tree @@ -72,6 +73,7 @@ "TabbedContent", "TabPane", "Tabs", + "TextEditor", "TextLog", "Tooltip", "Tree", diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi index c04d1f0d88..60535633dd 100644 --- a/src/textual/widgets/__init__.pyi +++ b/src/textual/widgets/__init__.pyi @@ -28,6 +28,7 @@ from ._tabbed_content import TabbedContent as TabbedContent from ._tabbed_content import TabPane as TabPane from ._tabs import Tab as Tab from ._tabs import Tabs as Tabs +from ._text_editor import TextEditor as TextEditor from ._text_log import TextLog as TextLog from ._tooltip import Tooltip as Tooltip from ._tree import Tree as Tree diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 03721074c8..b37038b682 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -181,7 +181,7 @@ def _read_callable(self, byte_offset, point): return_value = "\n".encode("utf8") else: return_value = lines[row][column].encode("utf8") - print(f"(point={point!r}) (offset={byte_offset!r}) {return_value!r}") + # print(f"(point={point!r}) (offset={byte_offset!r}) {return_value!r}") return return_value def load_text(self, text: str) -> None: @@ -358,6 +358,8 @@ def _on_key(self, event: events.Key) -> None: event.prevent_default() elif key == "enter": self.split_line() + elif key == "backspace": + self.delete_left() def _on_click(self, event: events.Click) -> None: """Clicking the content body moves the cursor.""" @@ -609,6 +611,14 @@ def _position_to_byte_offset(self, position: tuple[int, int]) -> int: return bytes_lines_above + bytes_this_line_left_of_cursor def split_line(self): + """ + Splits the current line at the cursor's position and updates the cursor position. + + This method splits the current line into two at the cursor's column position, + effectively inserting a newline character at the cursor's position. The part of the + line after the cursor becomes a new line below the current line. The cursor then + moves to the start of this new line. + """ cursor_row, cursor_column = self.cursor_position lines = self.document_lines @@ -628,21 +638,39 @@ def split_line(self): self.cursor_position = (cursor_row + 1, 0) def delete_left(self) -> None: + """ + Deletes the character to the left of the cursor and updates the cursor position. + + If the cursor is at the start of a line, it deletes the newline character that separates + the current line from the previous one, effectively merging the two lines. The cursor + then moves to the end of what was previously the line above. + + If the cursor is not at the start of a line, it deletes the character to the left of the + cursor within the current line. The cursor then moves one space to the left. + + If the cursor is at the start of the document, no action is taken. + """ log.debug(f"delete left at {self.cursor_position!r}") if self.cursor_at_start_of_document: return cursor_row, cursor_column = self.cursor_position + lines = self.document_lines # If the cursor is at the start of a row, then the deletion "merges" the rows # as it deletes the newline character that separates them. if self.cursor_at_start_of_row: - pass + previous_line = lines[cursor_row - 1] + current_line = lines[cursor_row] + lines[cursor_row - 1] = previous_line + current_line + del lines[cursor_row] + self.cursor_position = (cursor_row - 1, len(previous_line)) else: - old_text = self.document_lines[cursor_row] - - new_text = old_text[: cursor_column - 1] + current_line = lines[cursor_row] + new_line = current_line[: cursor_column - 1] + current_line[cursor_column:] + lines[cursor_row] = new_line + self.cursor_position = (cursor_row, cursor_column - 1) # --- Debug actions def action_print_line_cache(self) -> None: @@ -751,134 +779,3 @@ def read_callable(byte_offset, point): tree = parser.parse(bytes(CODE, "utf-8")) print(list(traverse_tree(tree.walk()))) - -# -# -# from pathlib import Path -# -# from rich.pretty import Pretty -# -# from textual.app import App, ComposeResult -# from textual.binding import Binding -# from textual.widgets import Footer, Static -# from textual.widgets._text_editor import TextEditor -# -# SAMPLE_TEXT = [ -# "Hello, world!", -# "", -# "你好,世界!", # Chinese characters, which are usually double-width -# "こんにちは、世界!", # Japanese characters, also usually double-width -# "안녕하세요, 세계!", # Korean characters, also usually double-width -# " This line has leading white space", -# "This line has trailing white space ", -# " This line has both leading and trailing white space ", -# " ", # Line with only spaces -# "こんにちは、world! 你好,world!", # Mixed script line -# "Hello, 🌍! Hello, 🌏! Hello, 🌎!", # Line with emoji (which are often double-width) -# "The quick brown 🦊 jumps over the lazy 🐶.", # Line with emoji interspersed in text -# "Special characters: ~!@#$%^&*()_+`-={}|[]\\:\";'<>?,./", -# # Line with special characters -# "Unicode example: Привет, мир!", # Russian text -# "Unicode example: Γειά σου Κόσμε!", # Greek text -# "Unicode example: مرحبا بك في", # Arabic text -# ] -# -# PYTHON_SNIPPET = """\ -# def render_line(self, y: int) -> Strip: -# '''Render a line of the widget. y is relative to the top of the widget.''' -# -# row_index = y // 4 # A checkerboard square consists of 4 rows -# -# if row_index >= 8: # Generate blank lines when we reach the end -# return Strip.blank(self.size.width) -# -# is_odd = row_index % 2 # Used to alternate the starting square on each row -# -# white = Style.parse("on white") # Get a style object for a white background -# black = Style.parse("on black") # Get a style object for a black background -# -# # Generate a list of segments with alternating black and white space characters -# segments = [ -# Segment(" " * 8, black if (column + is_odd) % 2 else white) -# for column in range(8) -# ] -# strip = Strip(segments, 8 * 8) -# return strip -# """ -# -# -# class TextEditorDemo(App): -# CSS = """\ -# TextEditor { -# height: 18; -# background: $panel; -# } -# -# #debug { -# border: wide $primary; -# padding: 1 2; -# } -# -# """ -# -# BINDINGS = [ -# Binding("ctrl+t", "traverse", "Traverse nodes") -# ] -# -# def compose(self) -> ComposeResult: -# text_area = TextEditor() -# text_area.language = "python" -# code_path = Path( -# "/Users/darrenburns/Code/textual/src/textual/widgets/_data_table.py") -# -# short_code = """\ -# print("hello") -# print("world") -# """ -# text_area.load_text(short_code) -# # text_area.load_text(code_path.read_text()) -# yield text_area -# yield Footer() -# Static.can_focus = True -# yield Static(id="debug") -# -# def on_mount(self): -# editor = self.query_one(TextEditor) -# self.watch(editor, "cursor_position", self.update_debug) -# self.watch(editor, "language", self.update_debug) -# -# def update_debug(self): -# editor = self.query_one(TextEditor) -# debug = self.query_one("#debug") -# debug.update(Pretty(editor.debug_state())) -# -# def action_traverse(self): -# def traverse_tree(cursor): -# reached_root = False -# while reached_root == False: -# yield cursor.node -# -# if cursor.goto_first_child(): -# continue -# -# if cursor.goto_next_sibling(): -# continue -# -# retracing = True -# while retracing: -# if not cursor.goto_parent(): -# retracing = False -# reached_root = True -# -# if cursor.goto_next_sibling(): -# retracing = False -# -# editor = self.query_one(TextEditor) -# nodes = list(traverse_tree(editor._syntax_tree.walk())) -# for node in nodes: -# print(node) -# -# -# app = TextEditorDemo() -# if __name__ == '__main__': -# app.run() diff --git a/src/textual/widgets/text_editor.py b/src/textual/widgets/text_editor.py new file mode 100644 index 0000000000..2b5caf6317 --- /dev/null +++ b/src/textual/widgets/text_editor.py @@ -0,0 +1,3 @@ +from ._text_editor import Highlight + +__all__ = ["Highlight"] From 5a865644075993dd334599d8d2dbfb12e4238bd3 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 12 Jul 2023 12:21:48 +0100 Subject: [PATCH 042/366] Add cursor word right and cursor word left actions --- src/textual/widgets/_text_editor.py | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index b37038b682..d3682b1eb7 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -1,5 +1,6 @@ from __future__ import annotations +import string from collections import defaultdict from dataclasses import dataclass from pathlib import Path @@ -90,7 +91,9 @@ class TextEditor(ScrollView, can_focus=True): Binding("up", "cursor_up", "cursor up", show=False), Binding("down", "cursor_down", "cursor down", show=False), Binding("left", "cursor_left", "cursor left", show=False), + Binding("ctrl+left", "cursor_left_word", "cursor left word", show=False), Binding("right", "cursor_right", "cursor right", show=False), + Binding("ctrl+right", "cursor_right_word", "cursor right word", show=False), Binding("home", "cursor_line_start", "cursor line start", show=False), Binding("end", "cursor_line_end", "cursor line end", show=False), # Debugging bindings @@ -519,6 +522,59 @@ def action_cursor_line_end(self) -> None: def action_cursor_line_start(self) -> None: self.cursor_to_line_start() + def action_cursor_left_word(self) -> None: + """Move the cursor left by a single word.""" + + # If we're at the start of the document, there's nowhere to go + if self.cursor_at_start_of_document: + return + + cursor_row, cursor_column = self.cursor_position + while True: + # If we're at the start of a row, move up to the previous row + if self.cursor_at_start_of_row: + cursor_row -= 1 + cursor_column = len(self.document_lines[cursor_row]) + else: + cursor_column -= 1 + + # Update the cursor position + self.cursor_position = (cursor_row, cursor_column) + + # If we've moved to a word boundary, stop + if ( + cursor_column == 0 + or self.document_lines[cursor_row][cursor_column - 1] + in string.whitespace + ): + break + + def action_cursor_right_word(self) -> None: + """Move the cursor right by a single word.""" + + # If we're at the end of the document, there's nowhere to go + if self.cursor_at_end_of_document: + return + + cursor_row, cursor_column = self.cursor_position + while True: + # If we're at the end of a row, move down to the next row + if self.cursor_at_end_of_row: + cursor_row += 1 + cursor_column = 0 + else: + cursor_column += 1 + + # Update the cursor position + self.cursor_position = (cursor_row, cursor_column) + + # If we've moved to a word boundary, stop + if ( + cursor_column == len(self.document_lines[cursor_row]) + or self.document_lines[cursor_row][cursor_column] in string.whitespace + ): + break + @property def active_line_text(self) -> str: # TODO - consider empty documents @@ -672,6 +728,8 @@ def delete_left(self) -> None: lines[cursor_row] = new_line self.cursor_position = (cursor_row, cursor_column - 1) + # TODO - update the syntax tree here. + # --- Debug actions def action_print_line_cache(self) -> None: log.debug(self._line_cache) From 46ab5101b672b72c0cd050bef7c8a31633b22d6c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 12 Jul 2023 17:19:17 +0100 Subject: [PATCH 043/366] Add a bunch of utilities for deleting --- src/textual/_ansi_sequences.py | 2 + src/textual/widgets/_text_editor.py | 237 +++++++++++++++++++++------- 2 files changed, 179 insertions(+), 60 deletions(-) diff --git a/src/textual/_ansi_sequences.py b/src/textual/_ansi_sequences.py index fc3b8de624..9d0c4a601b 100644 --- a/src/textual/_ansi_sequences.py +++ b/src/textual/_ansi_sequences.py @@ -221,6 +221,8 @@ "\x1b[1;5B": (Keys.ControlDown,), # Cursor Mode "\x1b[1;5C": (Keys.ControlRight,), # Cursor Mode "\x1b[1;5D": (Keys.ControlLeft,), # Cursor Mode + "\x1bf": (Keys.ControlRight,), # iTerm natural editing keys + "\x1bb": (Keys.ControlLeft,), # iTerm natural editing keys "\x1b[1;5F": (Keys.ControlEnd,), "\x1b[1;5H": (Keys.ControlHome,), # Tmux sends following keystrokes when control+arrow is pressed, but for diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index d3682b1eb7..df450aac0f 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re import string from collections import defaultdict from dataclasses import dataclass @@ -94,8 +95,15 @@ class TextEditor(ScrollView, can_focus=True): Binding("ctrl+left", "cursor_left_word", "cursor left word", show=False), Binding("right", "cursor_right", "cursor right", show=False), Binding("ctrl+right", "cursor_right_word", "cursor right word", show=False), - Binding("home", "cursor_line_start", "cursor line start", show=False), - Binding("end", "cursor_line_end", "cursor line end", show=False), + Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False), + Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False), + Binding("backspace", "delete_left", "delete left", show=False), + Binding("ctrl+d", "delete_right", "delete right", show=False), + Binding("ctrl+x", "delete_line", "delete line", show=False), + Binding( + "ctrl+u", "delete_to_start_of_line", "delete to line start", show=False + ), + Binding("ctrl+k", "delete_to_end_of_line", "delete to line end", show=False), # Debugging bindings Binding("ctrl+s", "print_highlight_cache", "[debug] Print highlight cache"), Binding("ctrl+l", "print_line_cache", "[debug] Print line cache"), @@ -130,6 +138,10 @@ def __init__( self._highlights_query: str | None = None """The string containing the tree-sitter AST query used for syntax highlighting.""" + self._last_intentional_column: int = 0 + """Tracks the last column the user explicitly navigated to so that we can reset + to it whenever possible.""" + # --- Abstract syntax tree and related parsing machinery self._language: Language | None = None self._parser: Parser | None = None @@ -190,6 +202,8 @@ def _read_callable(self, byte_offset, point): def load_text(self, text: str) -> None: """Load text from a string into the editor.""" lines = text.splitlines(keepends=False) + if text[-1] == "\n": + lines.append("") self.load_lines(lines) def load_lines(self, lines: list[str]) -> None: @@ -361,8 +375,8 @@ def _on_key(self, event: events.Key) -> None: event.prevent_default() elif key == "enter": self.split_line() - elif key == "backspace": - self.delete_left() + elif key == "shift+tab": + self.dedent_line() def _on_click(self, event: events.Click) -> None: """Clicking the content body moves the cursor.""" @@ -386,6 +400,9 @@ def _on_click(self, event: events.Click) -> None: else: self.cursor_position = (target_y, len(line)) + new_row, new_column = self.cursor_position + self._last_intentional_column = new_column + def _on_paste(self, event: events.Paste) -> None: text = event.text if text: @@ -399,7 +416,9 @@ def _on_paste(self, event: events.Paste) -> None: # clamped_column = clamp(new_column, 0, len(self.document_lines[clamped_row]) - 1) # return clamped_row, clamped_column - def watch_cursor_position(self, new_position: tuple[int, int]) -> None: + def watch_cursor_position( + self, old_position: tuple[int, int], new_position: tuple[int, int] + ) -> None: log.debug("scrolling cursor into view") self.scroll_cursor_visible() @@ -448,10 +467,9 @@ def cursor_at_end_of_document(self) -> bool: def cursor_to_line_end(self) -> None: cursor_row, cursor_column = self.cursor_position - self.cursor_position = ( - cursor_row, - len(self.document_lines[cursor_row]), - ) + target_column = len(self.document_lines[cursor_row]) + self.cursor_position = (cursor_row, target_column) + self._last_intentional_column = target_column def cursor_to_line_start(self) -> None: cursor_row, cursor_column = self.cursor_position @@ -474,6 +492,7 @@ def action_cursor_left(self) -> None: target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above self.cursor_position = (target_row, target_column) + self._last_intentional_column = target_column def action_cursor_right(self) -> None: """Move the cursor one position to the right. @@ -489,6 +508,7 @@ def action_cursor_right(self) -> None: target_column = 0 if self.cursor_at_end_of_row else cursor_column + 1 self.cursor_position = (target_row, target_column) + self._last_intentional_column = target_column def action_cursor_down(self) -> None: """Move the cursor down one cell.""" @@ -496,9 +516,9 @@ def action_cursor_down(self) -> None: self.cursor_to_line_end() cursor_row, cursor_column = self.cursor_position + cursor_column = max(self._last_intentional_column, cursor_column) target_row = min(len(self.document_lines) - 1, cursor_row + 1) - # TODO: Fetch last active column on this row target_column = clamp(cursor_column, 0, len(self.document_lines[target_row])) self.cursor_position = (target_row, target_column) @@ -509,9 +529,9 @@ def action_cursor_up(self) -> None: self.cursor_to_line_start() cursor_row, cursor_column = self.cursor_position + cursor_column = max(self._last_intentional_column, cursor_column) target_row = max(0, cursor_row - 1) - # TODO: Fetch last active column on this row target_column = clamp(cursor_column, 0, len(self.document_lines[target_row])) self.cursor_position = (target_row, target_column) @@ -523,57 +543,62 @@ def action_cursor_line_start(self) -> None: self.cursor_to_line_start() def action_cursor_left_word(self) -> None: - """Move the cursor left by a single word.""" + """Move the cursor left by a single word, skipping spaces.""" - # If we're at the start of the document, there's nowhere to go if self.cursor_at_start_of_document: return cursor_row, cursor_column = self.cursor_position - while True: - # If we're at the start of a row, move up to the previous row - if self.cursor_at_start_of_row: - cursor_row -= 1 - cursor_column = len(self.document_lines[cursor_row]) - else: - cursor_column -= 1 - # Update the cursor position - self.cursor_position = (cursor_row, cursor_column) + # Regular expression pattern for "word" boundaries + pattern = r"(?<=\W)(?=\w)|(?<=\w)(?=\W)" - # If we've moved to a word boundary, stop - if ( - cursor_column == 0 - or self.document_lines[cursor_row][cursor_column - 1] - in string.whitespace - ): - break + # Check the current line for a word boundary + line = self.document_lines[cursor_row][:cursor_column] + matches = list(re.finditer(pattern, line)) + + if matches: + # If a word boundary is found, move the cursor there + cursor_column = matches[-1].start() + elif cursor_row > 0: + # If no word boundary is found and we're not on the first line, move to the end of the previous line + cursor_row -= 1 + cursor_column = len(self.document_lines[cursor_row]) + else: + # If we're already on the first line and no word boundary is found, move to the start of the line + cursor_column = 0 + + self.cursor_position = (cursor_row, cursor_column) + self._last_intentional_column = cursor_column def action_cursor_right_word(self) -> None: - """Move the cursor right by a single word.""" + """Move the cursor right by a single word, skipping spaces.""" - # If we're at the end of the document, there's nowhere to go if self.cursor_at_end_of_document: return cursor_row, cursor_column = self.cursor_position - while True: - # If we're at the end of a row, move down to the next row - if self.cursor_at_end_of_row: - cursor_row += 1 - cursor_column = 0 - else: - cursor_column += 1 - # Update the cursor position - self.cursor_position = (cursor_row, cursor_column) + # Regular expression pattern for "word" boundaries + pattern = r"(?<=\W)(?=\w)|(?<=\w)(?=\W)" - # If we've moved to a word boundary, stop - if ( - cursor_column == len(self.document_lines[cursor_row]) - or self.document_lines[cursor_row][cursor_column] in string.whitespace - ): - break + # Check the current line for a word boundary + line = self.document_lines[cursor_row][cursor_column:] + matches = list(re.finditer(pattern, line)) + + if matches: + # If a word boundary is found, move the cursor there + cursor_column += matches[0].end() + elif cursor_row < len(self.document_lines) - 1: + # If no word boundary is found and we're not on the last line, move to the start of the next line + cursor_row += 1 + cursor_column = 0 + else: + # If we're already on the last line and no word boundary is found, move to the end of the line + cursor_column = len(self.document_lines[cursor_row]) + + self.cursor_position = (cursor_row, cursor_column) + self._last_intentional_column = cursor_column @property def active_line_text(self) -> str: @@ -676,24 +701,55 @@ def split_line(self): moves to the start of this new line. """ cursor_row, cursor_column = self.cursor_position - lines = self.document_lines - line = lines[cursor_row] - text_before_cursor = line[:cursor_column] - text_after_cursor = line[cursor_column:] + # Get the current line's indentation (leading whitespace) + current_line = self.document_lines[cursor_row] + indentation = len(current_line) - len(current_line.lstrip()) - lines = ( - lines[:cursor_row] - + [text_before_cursor, text_after_cursor] - + lines[cursor_row + 1 :] - ) + # Split the current line into two lines at the cursor position + line_before = current_line[:cursor_column] + line_after = current_line[cursor_column:] - self.document_lines = lines - width, height = self._document_size - self._document_size = Size(width, height + 1) - self.cursor_position = (cursor_row + 1, 0) + # If the line ends with ':' or '{', add additional indentation to the new line + additional_indent = " " # Four spaces + if line_before.rstrip().endswith((":", "{")): + indentation += len(additional_indent) + elif cursor_row < len(self.document_lines) - 1: + # If there is a line below, match its indentation + next_line = self.document_lines[cursor_row + 1] + next_line_indentation = len(next_line) - len(next_line.lstrip()) + indentation = next_line_indentation + + # Add the indentation to the start of the new line + line_after = " " * indentation + line_after + + # Update the lines in the document + self.document_lines[cursor_row] = line_before + self.document_lines.insert(cursor_row + 1, line_after) + + # Move the cursor to the start of the new line + self.cursor_position = (cursor_row + 1, indentation) + + def dedent_line(self) -> None: + """Reduces the indentation of the current line by one level.""" + + cursor_row, cursor_column = self.cursor_position + + # Define one level of indentation as four spaces + indent_level = " " * 4 - def delete_left(self) -> None: + current_line = self.document_lines[cursor_row] + + # If the line is indented, reduce the indentation + if current_line.startswith(indent_level): + self.document_lines[cursor_row] = current_line[len(indent_level) :] + + if cursor_column > len(current_line): + self.cursor_position = (cursor_row, len(current_line)) + + self.refresh() + + def action_delete_left(self) -> None: """ Deletes the character to the left of the cursor and updates the cursor position. @@ -730,6 +786,67 @@ def delete_left(self) -> None: # TODO - update the syntax tree here. + def action_delete_right(self) -> None: + """Deletes the character to the right of the cursor and keeps the cursor at + the same position.""" + + cursor_row, cursor_column = self.cursor_position + + # Check if the cursor is at the end of the document + if cursor_row == len(self.document_lines) - 1 and cursor_column == len( + self.document_lines[cursor_row] + ): + return # Nothing to delete + + current_line = self.document_lines[cursor_row] + + if self.cursor_at_end_of_row: + # If the cursor is at the end of the line, delete the newline character by joining this line and the next line + self.document_lines[cursor_row] = ( + current_line + self.document_lines[cursor_row + 1] + ) + del self.document_lines[cursor_row + 1] + else: + # If the cursor is not at the end of the line, delete the character to the right of the cursor + self.document_lines[cursor_row] = ( + current_line[:cursor_column] + current_line[cursor_column + 1 :] + ) + + self.refresh() + + def action_delete_line(self) -> None: + """Deletes the entire line that the cursor is currently on.""" + + cursor_row, _ = self.cursor_position + + # Remove the current line from the document + del self.document_lines[cursor_row] + + # If we deleted the last line of the document, move the cursor up a line + if cursor_row == len(self.document_lines): + cursor_row -= 1 + + # Move the cursor to the start of the new current line + self.cursor_position = (cursor_row, 0) + + def action_delete_to_start_of_line(self) -> None: + """Deletes from the cursor position to the start of the line.""" + + cursor_row, cursor_column = self.cursor_position + self.document_lines[cursor_row] = self.document_lines[cursor_row][ + cursor_column: + ] + self.cursor_position = (cursor_row, 0) + + def action_delete_to_end_of_line(self) -> None: + """Deletes from the cursor position to the end of the line.""" + cursor_row, cursor_column = self.cursor_position + self.document_lines[cursor_row] = self.document_lines[cursor_row][ + :cursor_column + ] + + self.refresh() + # --- Debug actions def action_print_line_cache(self) -> None: log.debug(self._line_cache) From b3f124707fc2d484526333322307dbdc5c9f45af Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Jul 2023 10:39:24 +0100 Subject: [PATCH 044/366] Update lock, ensure virtual size updates on split_line --- poetry.lock | 2468 +++++++++++++-------------- src/textual/widgets/_text_editor.py | 4 + 2 files changed, 1238 insertions(+), 1234 deletions(-) diff --git a/poetry.lock b/poetry.lock index ac3bb981f3..3daf410716 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,13 +1,1139 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +[[package]] +name = "aiohttp" +version = "3.8.4" +description = "Async http client/server framework (asyncio)" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" +asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""} +attrs = ">=17.3.0" +charset-normalizer = ">=2.0,<4.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "cchardet"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "anyio" +version = "3.7.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] + +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} + +[[package]] +name = "asynctest" +version = "0.13.0" +description = "Enhance the standard unittest package with features for testing asyncio libraries" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "23.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cached-property" +version = "1.5.2" +description = "A decorator for caching properties in classes." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "certifi" +version = "2023.5.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "charset-normalizer" +version = "3.2.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" +optional = false +python-versions = ">=3.7.0" + +[[package]] +name = "click" +version = "8.1.4" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" + +[[package]] +name = "colored" +version = "1.4.4" +description = "Simple library for color and formatting to terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "distlib" +version = "0.3.6" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "exceptiongroup" +version = "1.1.2" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.12.2" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "frozenlist" +version = "1.3.3" +description = "A list-like structure which implements collections.abc.MutableSequence" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "gitdb" +version = "4.0.10" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.32" +description = "GitPython is a Python library used to interact with Git repositories" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +gitdb = ">=4.0.1,<5" +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} + +[[package]] +name = "griffe" +version = "0.30.1" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cached-property = {version = "*", markers = "python_version < \"3.8\""} +colorama = ">=0.4" + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "httpcore" +version = "0.16.3" +description = "A minimal low-level HTTP client." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.23.3" +description = "The next generation HTTP client." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.17.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "identify" +version = "2.5.24" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "linkify-it-py" +version = "2.0.2" +description = "Links recognition library with FULL unicode support." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +uc-micro-py = "*" + +[package.extras] +benchmark = ["pytest", "pytest-benchmark"] +dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] +doc = ["myst-parser", "sphinx", "sphinx-book-theme"] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "markdown" +version = "3.3.7" +description = "Python implementation of Markdown." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-it-py" +version = "2.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} +mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""} +mdurl = ">=0.1,<1.0" +typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mdit-py-plugins" +version = "0.3.5" +description = "Collection of plugins for markdown-it-py" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +markdown-it-py = ">=1.0.0,<3.0.0" + +[package.extras] +code-style = ["pre-commit"] +rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mkdocs" +version = "1.4.3" +description = "Project documentation with Markdown." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.2.1,<3.4" +mergedeep = ">=1.3.4" +packaging = ">=20.5" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "0.4.1" +description = "Automatically link across pages in MkDocs." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Markdown = ">=3.3" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-exclude" +version = "1.0.2" +description = "A mkdocs plugin that lets you exclude files or trees." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +mkdocs = "*" + +[[package]] +name = "mkdocs-material" +version = "9.1.18" +description = "Documentation that simply works" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = ">=0.4" +jinja2 = ">=3.0" +markdown = ">=3.2" +mkdocs = ">=1.4.2" +mkdocs-material-extensions = ">=1.1" +pygments = ">=2.14" +pymdown-extensions = ">=9.9.1" +regex = ">=2022.4.24" +requests = ">=2.26" + +[[package]] +name = "mkdocs-material-extensions" +version = "1.1.1" +description = "Extension pack for Python Markdown and MkDocs Material." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mkdocs-rss-plugin" +version = "1.5.0" +description = "MkDocs plugin which generates a static RSS feed using git log and page.meta." +category = "dev" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +GitPython = ">=3.1,<3.2" +mkdocs = ">=1.1,<2" +pytz = {version = ">=2022.0.0,<2023.0.0", markers = "python_version < \"3.9\""} +tzdata = {version = ">=2022.0.0,<2023.0.0", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""} + +[package.extras] +dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (>=4.0.0,<4.1.0)", "validator-collection (>=1.5,<1.6)"] +doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"] + +[[package]] +name = "mkdocstrings" +version = "0.20.0" +description = "Automatic documentation from sources, for MkDocs." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Jinja2 = ">=2.11.1" +Markdown = ">=3.3" +MarkupSafe = ">=1.1" +mkdocs = ">=1.2" +mkdocs-autorefs = ">=0.3.1" +mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "0.10.1" +description = "A Python handler for mkdocstrings." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +griffe = ">=0.24" +mkdocstrings = ">=0.20" + +[[package]] +name = "msgpack" +version = "1.0.5" +description = "MessagePack serializer" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "multidict" +version = "6.0.4" +description = "multidict implementation" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mypy" +version = "1.4.1" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "platformdirs" +version = "3.8.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pygments" +version = "2.15.1" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pymdown-extensions" +version = "10.0.1" +description = "Extension pack for Python Markdown." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +markdown = ">=3.2" +pyyaml = "*" + +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-aiohttp" +version = "1.0.4" +description = "Pytest plugin for aiohttp support" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +aiohttp = ">=3.8.1" +pytest = ">=6.1.0" +pytest-asyncio = ">=0.17.2" + +[package.extras] +testing = ["coverage (==6.2)", "mypy (==0.931)"] + +[[package]] +name = "pytest-asyncio" +version = "0.21.1" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=7.0.0" +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-textual-snapshot" +version = "0.2.0" +description = "Snapshot testing for Textual apps" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +jinja2 = ">=3.0.0" +pytest = ">=7.0.0" +rich = ">=12.0.0" +syrupy = ">=3.0.0" +textual = ">=0.28.0" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2022.7.1" +description = "World timezone definitions, modern and historical" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "regex" +version = "2023.6.3" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "13.4.2" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.7.0" + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "syrupy" +version = "3.0.6" +description = "Pytest Snapshot Test Utility" +category = "dev" +optional = false +python-versions = ">=3.7,<4" + +[package.dependencies] +colored = ">=1.3.92,<2.0.0" +pytest = ">=5.1.0,<8.0.0" + +[[package]] +name = "textual-dev" +version = "1.0.1" +description = "Development tools for working with Textual" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +aiohttp = ">=3.8.1" +click = ">=8.1.2" +msgpack = ">=1.0.3" +textual = ">=0.29.0" +typing-extensions = ">=4.4.0,<5.0.0" + +[[package]] +name = "time-machine" +version = "2.10.0" +description = "Travel through time in your tests." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +python-dateutil = "*" [[package]] -name = "aiohttp" -version = "3.8.4" -description = "Async http client/server framework (asyncio)" +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tree-sitter" +version = "0.20.1" +description = "Python bindings to the Tree-sitter parsing library" +category = "main" +optional = false +python-versions = ">=3.3" + +[[package]] +name = "typed-ast" +version = "1.5.5" +description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false python-versions = ">=3.6" -files = [ + +[[package]] +name = "types-setuptools" +version = "67.8.0.0" +description = "Typing stubs for setuptools" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tzdata" +version = "2022.7" +description = "Provider of IANA time zone data" +category = "dev" +optional = false +python-versions = ">=2" + +[[package]] +name = "uc-micro-py" +version = "1.0.2" +description = "Micro subset of unicode data files for linkify-it-py projects." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "urllib3" +version = "2.0.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.23.1" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.12,<4" +importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} +platformdirs = ">=3.5.1,<4" + +[package.extras] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"] + +[[package]] +name = "watchdog" +version = "3.0.0" +description = "Filesystem events monitoring" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "yarl" +version = "1.9.2" +description = "Yet another URL library" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "811fccd3413e15a412c4e4de3ff954851f797a6828dc90fd260dcdab5db88ada" + +[metadata.files] +aiohttp = [ {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1"}, {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a"}, {file = "aiohttp-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b"}, @@ -96,116 +1222,27 @@ files = [ {file = "aiohttp-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04"}, {file = "aiohttp-3.8.4.tar.gz", hash = "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"}, ] - -[package.dependencies] -aiosignal = ">=1.1.2" -async-timeout = ">=4.0.0a3,<5.0" -asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""} -attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<4.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns", "cchardet"] - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +aiosignal = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, ] - -[package.dependencies] -frozenlist = ">=1.1.0" - -[[package]] -name = "anyio" -version = "3.7.1" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +anyio = [ {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, ] - -[package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] - -[[package]] -name = "async-timeout" -version = "4.0.2" -description = "Timeout context manager for asyncio programs" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] - -[package.dependencies] -typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} - -[[package]] -name = "asynctest" -version = "0.13.0" -description = "Enhance the standard unittest package with features for testing asyncio libraries" -category = "dev" -optional = false -python-versions = ">=3.5" -files = [ +asynctest = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, ] - -[[package]] -name = "attrs" -version = "23.1.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +attrs = [ {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, ] - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] - -[[package]] -name = "black" -version = "23.3.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +black = [ {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, @@ -228,71 +1265,23 @@ files = [ {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "cached-property" -version = "1.5.2" -description = "A decorator for caching properties in classes." -category = "dev" -optional = false -python-versions = "*" -files = [ + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, +] +cached-property = [ {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, ] - -[[package]] -name = "certifi" -version = "2023.5.7" -description = "Python package for providing Mozilla's CA Bundle." -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +certifi = [ {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, ] - -[[package]] -name = "cfgv" -version = "3.3.1" -description = "Validate configuration and produce human readable error messages." -category = "dev" -optional = false -python-versions = ">=3.6.1" -files = [ +cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] - -[[package]] -name = "charset-normalizer" -version = "3.2.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "dev" -optional = false -python-versions = ">=3.7.0" -files = [ +charset-normalizer = [ {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, @@ -369,54 +1358,18 @@ files = [ {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, ] - -[[package]] -name = "click" -version = "8.1.4" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +click = [ {file = "click-8.1.4-py3-none-any.whl", hash = "sha256:2739815aaa5d2c986a88f1e9230c55e17f0caad3d958a5e13ad0797c166db9e3"}, {file = "click-8.1.4.tar.gz", hash = "sha256:b97d0c74955da062a7d4ef92fadb583806a585b2ea81958a81bd72726cbb8e37"}, ] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ +colorama = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] - -[[package]] -name = "colored" -version = "1.4.4" -description = "Simple library for color and formatting to terminal" -category = "dev" -optional = false -python-versions = "*" -files = [ +colored = [ {file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"}, ] - -[[package]] -name = "coverage" -version = "7.2.7" -description = "Code coverage measurement for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +coverage = [ {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, @@ -478,61 +1431,19 @@ files = [ {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, ] - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "distlib" -version = "0.3.6" -description = "Distribution utilities" -category = "dev" -optional = false -python-versions = "*" -files = [ +distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] - -[[package]] -name = "exceptiongroup" -version = "1.1.2" -description = "Backport of PEP 654 (exception groups)" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +exceptiongroup = [ {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, ] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "filelock" -version = "3.12.2" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +filelock = [ {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, ] - -[package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "frozenlist" -version = "1.3.3" -description = "A list-like structure which implements collections.abc.MutableSequence" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +frozenlist = [ {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, @@ -608,286 +1519,67 @@ files = [ {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, ] - -[[package]] -name = "ghp-import" -version = "2.1.0" -description = "Copy your docs directly to the gh-pages branch." -category = "dev" -optional = false -python-versions = "*" -files = [ +ghp-import = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] - -[package.dependencies] -python-dateutil = ">=2.8.1" - -[package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] - -[[package]] -name = "gitdb" -version = "4.0.10" -description = "Git Object Database" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +gitdb = [ {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, ] - -[package.dependencies] -smmap = ">=3.0.1,<6" - -[[package]] -name = "gitpython" -version = "3.1.32" -description = "GitPython is a Python library used to interact with Git repositories" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +gitpython = [ {file = "GitPython-3.1.32-py3-none-any.whl", hash = "sha256:e3d59b1c2c6ebb9dfa7a184daf3b6dd4914237e7488a1730a6d8f6f5d0b4187f"}, {file = "GitPython-3.1.32.tar.gz", hash = "sha256:8d9b8cb1e80b9735e8717c9362079d3ce4c6e5ddeebedd0361b228c3a67a62f6"}, ] - -[package.dependencies] -gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} - -[[package]] -name = "griffe" -version = "0.30.1" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +griffe = [ {file = "griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c"}, {file = "griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109"}, ] - -[package.dependencies] -cached-property = {version = "*", markers = "python_version < \"3.8\""} -colorama = ">=0.4" - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +h11 = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "httpcore" -version = "0.16.3" -description = "A minimal low-level HTTP client." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +httpcore = [ {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, ] - -[package.dependencies] -anyio = ">=3.0,<5.0" -certifi = "*" -h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" - -[package.extras] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - -[[package]] -name = "httpx" -version = "0.23.3" -description = "The next generation HTTP client." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +httpx = [ {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, ] - -[package.dependencies] -certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - -[[package]] -name = "identify" -version = "2.5.24" -description = "File identification library for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +identify = [ {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, ] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "dev" -optional = false -python-versions = ">=3.5" -files = [ +idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] - -[[package]] -name = "importlib-metadata" -version = "6.7.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +importlib-metadata = [ {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, ] - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +iniconfig = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "linkify-it-py" -version = "2.0.2" -description = "Links recognition library with FULL unicode support." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +linkify-it-py = [ {file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"}, {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"}, ] - -[package.dependencies] -uc-micro-py = "*" - -[package.extras] -benchmark = ["pytest", "pytest-benchmark"] -dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] -doc = ["myst-parser", "sphinx", "sphinx-book-theme"] -test = ["coverage", "pytest", "pytest-cov"] - -[[package]] -name = "markdown" -version = "3.3.7" -description = "Python implementation of Markdown." -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +markdown = [ {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, ] - -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - -[package.extras] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markdown-it-py" -version = "2.2.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +markdown-it-py = [ {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, ] - -[package.dependencies] -linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} -mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""} -mdurl = ">=0.1,<1.0" -typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "2.1.3" -description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +markupsafe = [ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, @@ -939,218 +1631,50 @@ files = [ {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] - -[[package]] -name = "mdit-py-plugins" -version = "0.3.5" -description = "Collection of plugins for markdown-it-py" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +mdit-py-plugins = [ {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, ] - -[package.dependencies] -markdown-it-py = ">=1.0.0,<3.0.0" - -[package.extras] -code-style = ["pre-commit"] -rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +mdurl = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +mergedeep = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] - -[[package]] -name = "mkdocs" -version = "1.4.3" -description = "Project documentation with Markdown." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mkdocs = [ {file = "mkdocs-1.4.3-py3-none-any.whl", hash = "sha256:6ee46d309bda331aac915cd24aab882c179a933bd9e77b80ce7d2eaaa3f689dd"}, {file = "mkdocs-1.4.3.tar.gz", hash = "sha256:5955093bbd4dd2e9403c5afaf57324ad8b04f16886512a3ee6ef828956481c57"}, ] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} -ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} -jinja2 = ">=2.11.1" -markdown = ">=3.2.1,<3.4" -mergedeep = ">=1.3.4" -packaging = ">=20.5" -pyyaml = ">=5.1" -pyyaml-env-tag = ">=0.1" -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} -watchdog = ">=2.0" - -[package.extras] -i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] - -[[package]] -name = "mkdocs-autorefs" -version = "0.4.1" -description = "Automatically link across pages in MkDocs." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mkdocs-autorefs = [ {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, ] - -[package.dependencies] -Markdown = ">=3.3" -mkdocs = ">=1.1" - -[[package]] -name = "mkdocs-exclude" -version = "1.0.2" -description = "A mkdocs plugin that lets you exclude files or trees." -category = "dev" -optional = false -python-versions = "*" -files = [ +mkdocs-exclude = [ {file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"}, ] - -[package.dependencies] -mkdocs = "*" - -[[package]] -name = "mkdocs-material" -version = "9.1.18" -description = "Documentation that simply works" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mkdocs-material = [ {file = "mkdocs_material-9.1.18-py3-none-any.whl", hash = "sha256:5bcf8fb79ac2f253c0ffe93fa181cba87718c6438f459dc4180ac7418cc9a450"}, {file = "mkdocs_material-9.1.18.tar.gz", hash = "sha256:981dd39979723d4cda7cfc77bbbe5e54922d5761a7af23fb8ba9edb52f114b13"}, ] - -[package.dependencies] -colorama = ">=0.4" -jinja2 = ">=3.0" -markdown = ">=3.2" -mkdocs = ">=1.4.2" -mkdocs-material-extensions = ">=1.1" -pygments = ">=2.14" -pymdown-extensions = ">=9.9.1" -regex = ">=2022.4.24" -requests = ">=2.26" - -[[package]] -name = "mkdocs-material-extensions" -version = "1.1.1" -description = "Extension pack for Python Markdown and MkDocs Material." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mkdocs-material-extensions = [ {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"}, ] - -[[package]] -name = "mkdocs-rss-plugin" -version = "1.5.0" -description = "MkDocs plugin which generates a static RSS feed using git log and page.meta." -category = "dev" -optional = false -python-versions = ">=3.7, <4" -files = [ +mkdocs-rss-plugin = [ {file = "mkdocs-rss-plugin-1.5.0.tar.gz", hash = "sha256:4178b3830dcbad9b53b12459e315b1aad6b37d1e7e5c56c686866a10f99878a4"}, {file = "mkdocs_rss_plugin-1.5.0-py2.py3-none-any.whl", hash = "sha256:2ab14c20bf6b7983acbe50181e7e4a0778731d9c2d5c38107ca7047a7abd2165"}, ] - -[package.dependencies] -GitPython = ">=3.1,<3.2" -mkdocs = ">=1.1,<2" -pytz = {version = ">=2022.0.0,<2023.0.0", markers = "python_version < \"3.9\""} -tzdata = {version = ">=2022.0.0,<2023.0.0", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""} - -[package.extras] -dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (>=4.0.0,<4.1.0)", "validator-collection (>=1.5,<1.6)"] -doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"] - -[[package]] -name = "mkdocstrings" -version = "0.20.0" -description = "Automatic documentation from sources, for MkDocs." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mkdocstrings = [ {file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"}, {file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"}, ] - -[package.dependencies] -Jinja2 = ">=2.11.1" -Markdown = ">=3.3" -MarkupSafe = ">=1.1" -mkdocs = ">=1.2" -mkdocs-autorefs = ">=0.3.1" -mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} -pymdown-extensions = ">=6.3" - -[package.extras] -crystal = ["mkdocstrings-crystal (>=0.3.4)"] -python = ["mkdocstrings-python (>=0.5.2)"] -python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] - -[[package]] -name = "mkdocstrings-python" -version = "0.10.1" -description = "A Python handler for mkdocstrings." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +mkdocstrings-python = [ {file = "mkdocstrings_python-0.10.1-py3-none-any.whl", hash = "sha256:ef239cee2c688e2b949a0a47e42a141d744dd12b7007311b3309dc70e3bafc5c"}, {file = "mkdocstrings_python-0.10.1.tar.gz", hash = "sha256:b72301fff739070ec517b5b36bf2f7c49d1360a275896a64efb97fc17d3f3968"}, ] - -[package.dependencies] -griffe = ">=0.24" -mkdocstrings = ">=0.20" - -[[package]] -name = "msgpack" -version = "1.0.5" -description = "MessagePack serializer" -category = "dev" -optional = false -python-versions = "*" -files = [ +msgpack = [ {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, @@ -1215,15 +1739,7 @@ files = [ {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, ] - -[[package]] -name = "multidict" -version = "6.0.4" -description = "multidict implementation" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +multidict = [ {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, @@ -1296,18 +1812,10 @@ files = [ {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, -] - -[[package]] -name = "mypy" -version = "1.4.1" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] +mypy = [ {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, @@ -1335,297 +1843,71 @@ files = [ {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, ] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" -optional = false -python-versions = ">=3.5" -files = [ +mypy-extensions = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] - -[[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" -category = "dev" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ +nodeenv = [ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, ] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "packaging" -version = "23.1" -description = "Core utilities for Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +packaging = [ {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] - -[[package]] -name = "pathspec" -version = "0.11.1" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pathspec = [ {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, ] - -[[package]] -name = "platformdirs" -version = "3.8.1" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +platformdirs = [ {file = "platformdirs-3.8.1-py3-none-any.whl", hash = "sha256:cec7b889196b9144d088e4c57d9ceef7374f6c39694ad1577a0aab50d27ea28c"}, {file = "platformdirs-3.8.1.tar.gz", hash = "sha256:f87ca4fcff7d2b0f81c6a748a77973d7af0f4d526f98f308477c3c436c74d528"}, ] - -[package.dependencies] -typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""} - -[package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] - -[[package]] -name = "pluggy" -version = "1.2.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pluggy = [ {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, ] - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "2.21.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pre-commit = [ {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, ] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pygments" -version = "2.15.1" -description = "Pygments is a syntax highlighting package written in Python." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +pygments = [ {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, ] - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pymdown-extensions" -version = "10.0.1" -description = "Extension pack for Python Markdown." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pymdown-extensions = [ {file = "pymdown_extensions-10.0.1-py3-none-any.whl", hash = "sha256:ae66d84013c5d027ce055693e09a4628b67e9dec5bce05727e45b0918e36f274"}, {file = "pymdown_extensions-10.0.1.tar.gz", hash = "sha256:b44e1093a43b8a975eae17b03c3a77aad4681b3b56fce60ce746dbef1944c8cb"}, ] - -[package.dependencies] -markdown = ">=3.2" -pyyaml = "*" - -[[package]] -name = "pytest" -version = "7.4.0" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pytest = [ {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-aiohttp" -version = "1.0.4" -description = "Pytest plugin for aiohttp support" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pytest-aiohttp = [ {file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"}, {file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"}, ] - -[package.dependencies] -aiohttp = ">=3.8.1" -pytest = ">=6.1.0" -pytest-asyncio = ">=0.17.2" - -[package.extras] -testing = ["coverage (==6.2)", "mypy (==0.931)"] - -[[package]] -name = "pytest-asyncio" -version = "0.21.0" -description = "Pytest support for asyncio" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-asyncio-0.21.0.tar.gz", hash = "sha256:2b38a496aef56f56b0e87557ec313e11e1ab9276fc3863f6a7be0f1d0e415e1b"}, - {file = "pytest_asyncio-0.21.0-py3-none-any.whl", hash = "sha256:f2b3366b7cd501a4056858bd39349d5af19742aed2d81660b7998b6341c7eb9c"}, +pytest-asyncio = [ + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, ] - -[package.dependencies] -pytest = ">=7.0.0" -typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] - -[[package]] -name = "pytest-cov" -version = "2.12.1" -description = "Pytest plugin for measuring coverage." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ +pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] - -[package.dependencies] -coverage = ">=5.2.1" -pytest = ">=4.6" -toml = "*" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "pytest-textual-snapshot" -version = "0.1.0" -description = "Snapshot testing for Textual apps" -category = "dev" -optional = false -python-versions = ">=3.6,<4.0" -files = [ - {file = "pytest_textual_snapshot-0.1.0-py3-none-any.whl", hash = "sha256:7310002ed152ce6cc654fff7f83ec88eecc36116c5bf7995decc0a6424809da3"}, - {file = "pytest_textual_snapshot-0.1.0.tar.gz", hash = "sha256:5e20629f2413a3689a485117e709e6d4010b7b5b558c0070414899b9768697f0"}, +pytest-textual-snapshot = [ + {file = "pytest_textual_snapshot-0.2.0-py3-none-any.whl", hash = "sha256:663fe07bf62181ec0c63139daaeaf50eb8088164037eb30d721f028adc9edc8c"}, + {file = "pytest_textual_snapshot-0.2.0.tar.gz", hash = "sha256:5e9f8c4b1b011bdae67d4f1129530afd6611f3f8bcf03cf06699402179bc12cf"}, ] - -[package.dependencies] -jinja2 = ">=3.0.0" -pytest = ">=7.0.0" -rich = ">=12.0.0" -syrupy = ">=3.0.0" -textual = ">=0.28.0" - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ +python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2022.7.1" -description = "World timezone definitions, modern and historical" -category = "dev" -optional = false -python-versions = "*" -files = [ +pytz = [ {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, ] - -[[package]] -name = "pyyaml" -version = "6.0" -description = "YAML parser and emitter for Python" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -1667,30 +1949,11 @@ files = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +pyyaml-env-tag = [ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, ] - -[package.dependencies] -pyyaml = "*" - -[[package]] -name = "regex" -version = "2023.6.3" -description = "Alternative regular expression module, to replace re." -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +regex = [ {file = "regex-2023.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:824bf3ac11001849aec3fa1d69abcb67aac3e150a933963fb12bda5151fe1bfd"}, {file = "regex-2023.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05ed27acdf4465c95826962528f9e8d41dbf9b1aa8531a387dee6ed215a3e9ef"}, {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b49c764f88a79160fa64f9a7b425620e87c9f46095ef9c9920542ab2495c8bc"}, @@ -1778,165 +2041,45 @@ files = [ {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e5087a3c59eef624a4591ef9eaa6e9a8d8a94c779dade95d27c0bc24650261cd"}, {file = "regex-2023.6.3-cp39-cp39-win32.whl", hash = "sha256:20326216cc2afe69b6e98528160b225d72f85ab080cbdf0b11528cbbaba2248f"}, {file = "regex-2023.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:bdff5eab10e59cf26bc479f565e25ed71a7d041d1ded04ccf9aee1d9f208487a"}, - {file = "regex-2023.6.3.tar.gz", hash = "sha256:72d1a25bf36d2050ceb35b517afe13864865268dfb45910e2e17a84be6cbfeb0"}, -] - -[[package]] -name = "requests" -version = "2.31.0" -description = "Python HTTP for Humans." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ + {file = "regex-2023.6.3.tar.gz", hash = "sha256:72d1a25bf36d2050ceb35b517afe13864865268dfb45910e2e17a84be6cbfeb0"}, +] +requests = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "dev" -optional = false -python-versions = "*" -files = [ +rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - -[[package]] -name = "rich" -version = "13.4.2" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" -optional = false -python-versions = ">=3.7.0" -files = [ +rich = [ {file = "rich-13.4.2-py3-none-any.whl", hash = "sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec"}, {file = "rich-13.4.2.tar.gz", hash = "sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898"}, ] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "setuptools" -version = "68.0.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +setuptools = [ {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ +six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] - -[[package]] -name = "smmap" -version = "5.0.0" -description = "A pure Python implementation of a sliding window memory map manager" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +smmap = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, ] - -[[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +sniffio = [ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] - -[[package]] -name = "syrupy" -version = "3.0.6" -description = "Pytest Snapshot Test Utility" -category = "dev" -optional = false -python-versions = ">=3.7,<4" -files = [ +syrupy = [ {file = "syrupy-3.0.6-py3-none-any.whl", hash = "sha256:9c18e22264026b34239bcc87ab7cc8d893eb17236ea7dae634217ea4f22a848d"}, {file = "syrupy-3.0.6.tar.gz", hash = "sha256:583aa5ca691305c27902c3e29a1ce9da50ff9ab5f184c54b1dc124a16e4a6cf4"}, ] - -[package.dependencies] -colored = ">=1.3.92,<2.0.0" -pytest = ">=5.1.0,<8.0.0" - -[[package]] -name = "textual-dev" -version = "1.0.1" -description = "Development tools for working with Textual" -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" -files = [ +textual-dev = [ {file = "textual_dev-1.0.1-py3-none-any.whl", hash = "sha256:419fc426c120f04f89ab0cb1aa88f7873dd7cdb9c21618e709175c8eaff6b566"}, {file = "textual_dev-1.0.1.tar.gz", hash = "sha256:9f4c40655cbb56af7ee92805ef14fa24ae98ff8b0ae778c59de7222f1caa7281"}, ] - -[package.dependencies] -aiohttp = ">=3.8.1" -click = ">=8.1.2" -msgpack = ">=1.0.3" -textual = ">=0.29.0" -typing-extensions = ">=4.4.0,<5.0.0" - -[[package]] -name = "time-machine" -version = "2.10.0" -description = "Travel through time in your tests." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +time-machine = [ {file = "time_machine-2.10.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d5e93c14b935d802a310c1d4694a9fe894b48a733ebd641c9a570d6f9e1f667"}, {file = "time_machine-2.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c0dda6b132c0180941944ede357109016d161d840384c2fb1096a3a2ef619f4"}, {file = "time_machine-2.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:900517e4a4121bf88527343d6aea2b5c99df134815bb8271ef589ec792502a71"}, @@ -1992,54 +2135,19 @@ files = [ {file = "time_machine-2.10.0-cp39-cp39-win_arm64.whl", hash = "sha256:c1775a949dd830579d1af5a271ec53d920dc01657035ad305f55c5a1ac9b9f1e"}, {file = "time_machine-2.10.0.tar.gz", hash = "sha256:64fd89678cf589fc5554c311417128b2782222dd65f703bf248ef41541761da0"}, ] - -[package.dependencies] -python-dateutil = "*" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ +toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] - -[[package]] -name = "tree-sitter" -version = "0.20.1" -description = "Python bindings to the Tree-sitter parsing library" -category = "main" -optional = false -python-versions = ">=3.3" -files = [ +tree-sitter = [ {file = "tree_sitter-0.20.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:6f11a1fd909dcf569e7b1d98861a837436799e757bbbc5cd5280989050929e12"}, {file = "tree_sitter-0.20.1.tar.gz", hash = "sha256:e93f082c545d6649bcfb5d681ed255eb004a6ce22988971a128f40692feec60d"}, ] - -[[package]] -name = "typed-ast" -version = "1.5.5" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +typed-ast = [ {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, @@ -2082,106 +2190,31 @@ files = [ {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, ] - -[[package]] -name = "types-setuptools" -version = "67.8.0.0" -description = "Typing stubs for setuptools" -category = "dev" -optional = false -python-versions = "*" -files = [ +types-setuptools = [ {file = "types-setuptools-67.8.0.0.tar.gz", hash = "sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f"}, {file = "types_setuptools-67.8.0.0-py3-none-any.whl", hash = "sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff"}, ] - -[[package]] -name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +typing-extensions = [ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] - -[[package]] -name = "tzdata" -version = "2022.7" -description = "Provider of IANA time zone data" -category = "dev" -optional = false -python-versions = ">=2" -files = [ +tzdata = [ {file = "tzdata-2022.7-py2.py3-none-any.whl", hash = "sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d"}, {file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"}, ] - -[[package]] -name = "uc-micro-py" -version = "1.0.2" -description = "Micro subset of unicode data files for linkify-it-py projects." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +uc-micro-py = [ {file = "uc-micro-py-1.0.2.tar.gz", hash = "sha256:30ae2ac9c49f39ac6dce743bd187fcd2b574b16ca095fa74cd9396795c954c54"}, {file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"}, ] - -[package.extras] -test = ["coverage", "pytest", "pytest-cov"] - -[[package]] -name = "urllib3" -version = "2.0.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +urllib3 = [ {file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"}, {file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"}, ] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "virtualenv" -version = "20.23.1" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +virtualenv = [ {file = "virtualenv-20.23.1-py3-none-any.whl", hash = "sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419"}, {file = "virtualenv-20.23.1.tar.gz", hash = "sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1"}, ] - -[package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.12,<4" -importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} -platformdirs = ">=3.5.1,<4" - -[package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"] - -[[package]] -name = "watchdog" -version = "3.0.0" -description = "Filesystem events monitoring" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +watchdog = [ {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, @@ -2210,18 +2243,7 @@ files = [ {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, ] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "yarl" -version = "1.9.2" -description = "Yet another URL library" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +yarl = [ {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, @@ -2297,29 +2319,7 @@ files = [ {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, ] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} - -[[package]] -name = "zipp" -version = "3.15.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +zipp = [ {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, ] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.7" -content-hash = "811fccd3413e15a412c4e4de3ff954851f797a6828dc90fd260dcdab5db88ada" diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index df450aac0f..c337ea8ba1 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -727,9 +727,13 @@ def split_line(self): self.document_lines[cursor_row] = line_before self.document_lines.insert(cursor_row + 1, line_after) + self._document_size = self._get_document_size(self.document_lines) + # Move the cursor to the start of the new line self.cursor_position = (cursor_row + 1, indentation) + self.refresh(layout=True) + def dedent_line(self) -> None: """Reduces the indentation of the current line by one level.""" From 6a120bcee27b7638dbddd80487af69f8b3634497 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Jul 2023 11:10:24 +0100 Subject: [PATCH 045/366] Fix refreshing --- src/textual/widgets/_text_editor.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index c337ea8ba1..a2686d3863 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -233,6 +233,9 @@ def _get_document_size(self, document_lines: list[str]) -> Size: # a line doesn't currently exist. return Size(text_width + 1, height) + def _refresh_size(self) -> None: + self._document_size = self._get_document_size(self.document_lines) + def render_line(self, widget_y: int) -> Strip: document_lines = self.document_lines @@ -377,6 +380,7 @@ def _on_key(self, event: events.Key) -> None: self.split_line() elif key == "shift+tab": self.dedent_line() + event.stop() def _on_click(self, event: events.Click) -> None: """Clicking the content body moves the cursor.""" @@ -634,10 +638,7 @@ def insert_text(self, text: str) -> None: # plus 1 to accommodate for a cursor potentially "resting" at the end of the row insertion_width = longest_modified_line + 1 - new_document_width = max(insertion_width, document_width) - new_document_height = len(lines) - - self._document_size = Size(new_document_width, new_document_height) + self._refresh_size() self.cursor_position = (cursor_row + len(replacement_lines) - 1, end_column) edit_args = { @@ -727,13 +728,10 @@ def split_line(self): self.document_lines[cursor_row] = line_before self.document_lines.insert(cursor_row + 1, line_after) - self._document_size = self._get_document_size(self.document_lines) - + self._refresh_size() # Move the cursor to the start of the new line self.cursor_position = (cursor_row + 1, indentation) - self.refresh(layout=True) - def dedent_line(self) -> None: """Reduces the indentation of the current line by one level.""" @@ -751,6 +749,7 @@ def dedent_line(self) -> None: if cursor_column > len(current_line): self.cursor_position = (cursor_row, len(current_line)) + self._refresh_size() self.refresh() def action_delete_left(self) -> None: @@ -848,7 +847,6 @@ def action_delete_to_end_of_line(self) -> None: self.document_lines[cursor_row] = self.document_lines[cursor_row][ :cursor_column ] - self.refresh() # --- Debug actions From ec08c637d243dc28f4e3eeafbe7b995c01cb2c9f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Jul 2023 13:21:57 +0100 Subject: [PATCH 046/366] Move delete_left and delete_right to command architecture --- src/textual/_types.py | 2 +- src/textual/widgets/_text_editor.py | 198 ++++++++++++++++++++-------- 2 files changed, 145 insertions(+), 55 deletions(-) diff --git a/src/textual/_types.py b/src/textual/_types.py index 85eb27c421..41d08dae5b 100644 --- a/src/textual/_types.py +++ b/src/textual/_types.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Union -from typing_extensions import Protocol +from typing_extensions import Protocol, runtime_checkable if TYPE_CHECKING: from rich.segment import Segment diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index a2686d3863..3f899e2bcc 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -1,13 +1,11 @@ from __future__ import annotations import re -import string from collections import defaultdict from dataclasses import dataclass from pathlib import Path from typing import ClassVar, Iterable, NamedTuple -import rich from rich.cells import get_character_cell_size from rich.style import Style from rich.text import Text @@ -16,6 +14,7 @@ from textual import events, log from textual._cells import cell_len +from textual._types import Protocol, runtime_checkable from textual.binding import Binding from textual.geometry import Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive @@ -60,6 +59,44 @@ class Highlight(NamedTuple): highlight_name: str | None +@runtime_checkable +class Edit(Protocol): + """Protocol for actions performed in the text editor that can be done and undone.""" + + def do(self, editor: TextEditor) -> None: + """Do the action.""" + + def undo(self, editor: TextEditor) -> None: + """Undo the action.""" + + +class Insert(NamedTuple): + """Implements the Edit protocol for inserting text at some position.""" + + text: str + position: tuple[int, int] + move_cursor: bool = True + + def do(self, editor: TextEditor) -> None: + editor._insert_text(self.text, self.position, self.move_cursor) + + def undo(self, editor: TextEditor) -> None: + """Undo the action.""" + + +@dataclass +class Delete: + from_position: tuple[int, int] + to_position: tuple[int, int] + + def do(self, editor: TextEditor) -> None: + """Do the action.""" + self.deleted_text = editor._delete_range(self.from_position, self.to_position) + + def undo(self, editor: TextEditor) -> None: + """Undo the action.""" + + class TextEditor(ScrollView, can_focus=True): DEFAULT_CSS = """\ $editor-active-line-bg: white 8%; @@ -142,6 +179,12 @@ def __init__( """Tracks the last column the user explicitly navigated to so that we can reset to it whenever possible.""" + self._word_pattern = re.compile(r"(?<=\W)(?=\w)|(?<=\w)(?=\W)") + """Compiled regular expression for what we consider to be a 'word'.""" + + self._undo_stack: list[Edit] = [] + """A stack (the end of the list is the top of the stack) for tracking edits.""" + # --- Abstract syntax tree and related parsing machinery self._language: Language | None = None self._parser: Parser | None = None @@ -363,6 +406,19 @@ def _prepare_highlights( for line_index, updated_highlights in highlight_updates.items(): highlights[line_index] = updated_highlights + def perform_edit(self, edit: Edit) -> None: + log.debug(f"performing edit {edit!r}") + edit.do(self) + self._undo_stack.append(edit) + + # TODO: Think about this... + self._undo_stack = self._undo_stack[-20:] + + def undo(self) -> None: + if self._undo_stack: + action = self._undo_stack.pop() + action.undo(self) + # --- Lower level event/key handling def _on_key(self, event: events.Key) -> None: log.debug(f"{event!r}") @@ -374,7 +430,8 @@ def _on_key(self, event: events.Key) -> None: insert = event.character event.stop() assert event.character is not None - self.insert_text(insert) + + self.insert_text(insert, self.cursor_position) event.prevent_default() elif key == "enter": self.split_line() @@ -410,7 +467,7 @@ def _on_click(self, event: events.Click) -> None: def _on_paste(self, event: events.Paste) -> None: text = event.text if text: - self.insert_text(text) + self._insert_text(text, self.cursor_position) event.stop() # --- Reactive watchers and validators @@ -554,12 +611,9 @@ def action_cursor_left_word(self) -> None: cursor_row, cursor_column = self.cursor_position - # Regular expression pattern for "word" boundaries - pattern = r"(?<=\W)(?=\w)|(?<=\w)(?=\W)" - # Check the current line for a word boundary line = self.document_lines[cursor_row][:cursor_column] - matches = list(re.finditer(pattern, line)) + matches = list(re.finditer(self._word_pattern, line)) if matches: # If a word boundary is found, move the cursor there @@ -583,12 +637,9 @@ def action_cursor_right_word(self) -> None: cursor_row, cursor_column = self.cursor_position - # Regular expression pattern for "word" boundaries - pattern = r"(?<=\W)(?=\w)|(?<=\w)(?=\W)" - # Check the current line for a word boundary line = self.document_lines[cursor_row][cursor_column:] - matches = list(re.finditer(pattern, line)) + matches = list(re.finditer(self._word_pattern, line)) if matches: # If a word boundary is found, move the cursor there @@ -610,26 +661,33 @@ def active_line_text(self) -> str: return self.document_lines[self.cursor_position[0]] # --- Editor operations - def insert_text(self, text: str) -> None: + def insert_text( + self, text: str, position: tuple[int, int], move_cursor: bool = True + ) -> None: + self.perform_edit(Insert(text, position, move_cursor)) + + def _insert_text( + self, text: str, position: tuple[int, int], move_cursor: bool = True + ) -> None: log.debug(f"insert {text!r} at {self.cursor_position!r}") start_byte = self._position_to_byte_offset(self.cursor_position) - start_point = self.cursor_position + start_point = position - cursor_row, cursor_column = self.cursor_position + target_row, target_column = position lines = self.document_lines - line = lines[cursor_row] - text_before_cursor = line[:cursor_column] - text_after_cursor = line[cursor_column:] + line = lines[target_row] + text_before_cursor = line[:target_column] + text_after_cursor = line[target_column:] replacement_lines = text.splitlines(keepends=False) replacement_lines[0] = text_before_cursor + replacement_lines[0] end_column = len(replacement_lines[-1]) replacement_lines[-1] += text_after_cursor - self.document_lines[cursor_row : cursor_row + 1] = replacement_lines + self.document_lines[target_row : target_row + 1] = replacement_lines longest_modified_line = max(cell_len(line) for line in replacement_lines) document_width, document_height = self._document_size @@ -639,7 +697,9 @@ def insert_text(self, text: str) -> None: insertion_width = longest_modified_line + 1 self._refresh_size() - self.cursor_position = (cursor_row + len(replacement_lines) - 1, end_column) + + if move_cursor: + self.cursor_position = (target_row + len(replacement_lines) - 1, end_column) edit_args = { "start_byte": start_byte, @@ -752,6 +812,58 @@ def dedent_line(self) -> None: self._refresh_size() self.refresh() + def _delete_range( + self, from_position: tuple[int, int], to_position: tuple[int, int] + ) -> str: + """Delete text between `from_position` and `to_position`. + + Returns: + A string containing the deleted text. + """ + + from_row, from_column = from_position + to_row, to_column = to_position + + lines = self.document_lines + + # Ensure that from_position is before to_position + if from_position > to_position: + from_row, from_column, to_row, to_column = ( + to_row, + to_column, + from_row, + from_column, + ) + + # If the range is within a single line + if from_row == to_row: + line = lines[from_row] + deleted_text = line[from_column:to_column] + lines[from_row] = line[:from_column] + line[to_column:] + else: + # The range spans multiple lines + start_line = lines[from_row] + end_line = lines[to_row] + + # Add the deleted segments from the start and end lines to the deleted text + deleted_text = ( + start_line[from_column:] + + "\n" + + "\n".join(self.document_lines[from_row + 1 : to_row]) + + "\n" + + end_line[:to_column] + ) + + # Update the lines at the start and end of the range + lines[from_row] = start_line[:from_column] + end_line[to_column:] + + # Delete the lines in between + del lines[from_row + 1 : to_row + 1] + + # Move the cursor to the start of the deleted range + self.cursor_position = (from_row, from_column) + return deleted_text + def action_delete_left(self) -> None: """ Deletes the character to the left of the cursor and updates the cursor position. @@ -765,57 +877,33 @@ def action_delete_left(self) -> None: If the cursor is at the start of the document, no action is taken. """ - log.debug(f"delete left at {self.cursor_position!r}") - if self.cursor_at_start_of_document: return cursor_row, cursor_column = self.cursor_position lines = self.document_lines - - # If the cursor is at the start of a row, then the deletion "merges" the rows - # as it deletes the newline character that separates them. + from_position = self.cursor_position if self.cursor_at_start_of_row: - previous_line = lines[cursor_row - 1] - current_line = lines[cursor_row] - lines[cursor_row - 1] = previous_line + current_line - del lines[cursor_row] - self.cursor_position = (cursor_row - 1, len(previous_line)) + to_position = (cursor_row - 1, len(lines[cursor_row - 1])) else: - current_line = lines[cursor_row] - new_line = current_line[: cursor_column - 1] + current_line[cursor_column:] - lines[cursor_row] = new_line - self.cursor_position = (cursor_row, cursor_column - 1) + to_position = (cursor_row, cursor_column - 1) - # TODO - update the syntax tree here. + self.perform_edit(Delete(from_position, to_position)) def action_delete_right(self) -> None: """Deletes the character to the right of the cursor and keeps the cursor at the same position.""" + if self.cursor_at_end_of_document: + return cursor_row, cursor_column = self.cursor_position - - # Check if the cursor is at the end of the document - if cursor_row == len(self.document_lines) - 1 and cursor_column == len( - self.document_lines[cursor_row] - ): - return # Nothing to delete - - current_line = self.document_lines[cursor_row] - + from_position = self.cursor_position if self.cursor_at_end_of_row: - # If the cursor is at the end of the line, delete the newline character by joining this line and the next line - self.document_lines[cursor_row] = ( - current_line + self.document_lines[cursor_row + 1] - ) - del self.document_lines[cursor_row + 1] + to_position = (cursor_row + 1, 0) else: - # If the cursor is not at the end of the line, delete the character to the right of the cursor - self.document_lines[cursor_row] = ( - current_line[:cursor_column] + current_line[cursor_column + 1 :] - ) + to_position = (cursor_row, cursor_column + 1) - self.refresh() + self.perform_edit(Delete(from_position, to_position)) def action_delete_line(self) -> None: """Deletes the entire line that the cursor is currently on.""" @@ -874,6 +962,7 @@ class EditorDebug: document_size: Size virtual_size: Size scroll: Offset + undo_stack: list[Edit] tree_sexp: str active_line_text: str active_line_cell_len: int @@ -889,6 +978,7 @@ def debug_state(self) -> "EditorDebug": document_size=self._document_size, virtual_size=self.virtual_size, scroll=self.scroll_offset, + undo_stack=self._undo_stack, # tree_sexp=self._syntax_tree.root_node.sexp(), tree_sexp="", active_line_text=repr(self.active_line_text), From 9b18185c84664cf40361194239d670e6f8fc6f09 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Jul 2023 13:48:01 +0100 Subject: [PATCH 047/366] Update delete_line to Edit protocol --- src/textual/widgets/_text_editor.py | 31 +++++++++++++++++------------ 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 3f899e2bcc..9bf0bec30f 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -88,10 +88,13 @@ def undo(self, editor: TextEditor) -> None: class Delete: from_position: tuple[int, int] to_position: tuple[int, int] + cursor_destination: tuple[int, int] | None = None def do(self, editor: TextEditor) -> None: """Do the action.""" - self.deleted_text = editor._delete_range(self.from_position, self.to_position) + self.deleted_text = editor._delete_range( + self.from_position, self.to_position, self.cursor_destination + ) def undo(self, editor: TextEditor) -> None: """Undo the action.""" @@ -813,7 +816,10 @@ def dedent_line(self) -> None: self.refresh() def _delete_range( - self, from_position: tuple[int, int], to_position: tuple[int, int] + self, + from_position: tuple[int, int], + to_position: tuple[int, int], + cursor_destination: tuple[int, int] | None, ) -> str: """Delete text between `from_position` and `to_position`. @@ -860,8 +866,12 @@ def _delete_range( # Delete the lines in between del lines[from_row + 1 : to_row + 1] - # Move the cursor to the start of the deleted range - self.cursor_position = (from_row, from_column) + if cursor_destination is not None: + self.cursor_position = cursor_destination + else: + # Move the cursor to the start of the deleted range + self.cursor_position = (from_row, from_column) + return deleted_text def action_delete_left(self) -> None: @@ -907,23 +917,18 @@ def action_delete_right(self) -> None: def action_delete_line(self) -> None: """Deletes the entire line that the cursor is currently on.""" - cursor_row, _ = self.cursor_position - # Remove the current line from the document - del self.document_lines[cursor_row] + from_position = (cursor_row, 0) + to_position = (cursor_row + 1, 0) - # If we deleted the last line of the document, move the cursor up a line - if cursor_row == len(self.document_lines): - cursor_row -= 1 - - # Move the cursor to the start of the new current line - self.cursor_position = (cursor_row, 0) + self.perform_edit(Delete(from_position, to_position)) def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor position to the start of the line.""" cursor_row, cursor_column = self.cursor_position + self.document_lines[cursor_row] = self.document_lines[cursor_row][ cursor_column: ] From 46a8a17a9cdf83240ab5a26dec53d4938ef683d1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Jul 2023 14:09:18 +0100 Subject: [PATCH 048/366] Delete to end/start of line are now Edit protocol based --- src/textual/widgets/_text_editor.py | 36 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 9bf0bec30f..a7bb34bd2e 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -99,6 +99,12 @@ def do(self, editor: TextEditor) -> None: def undo(self, editor: TextEditor) -> None: """Undo the action.""" + def __rich_repr__(self): + yield "from_position", self.from_position + yield "to_position", self.to_position + if hasattr(self, "deleted_text"): + yield "deleted_text", self.deleted_text + class TextEditor(ScrollView, can_focus=True): DEFAULT_CSS = """\ @@ -796,7 +802,11 @@ def split_line(self): self.cursor_position = (cursor_row + 1, indentation) def dedent_line(self) -> None: - """Reduces the indentation of the current line by one level.""" + """Reduces the indentation of the current line by one level. + + A dedent is simply a Delete operation on some amount of whitespace + which may exist at the start of a line. + """ cursor_row, cursor_column = self.cursor_position @@ -851,6 +861,8 @@ def _delete_range( start_line = lines[from_row] end_line = lines[to_row] + # TODO - I think this might be slightly off. + # When you delete a line, it records the deleted text with two newlines at the end instead of 1. # Add the deleted segments from the start and end lines to the deleted text deleted_text = ( start_line[from_column:] @@ -916,31 +928,25 @@ def action_delete_right(self) -> None: self.perform_edit(Delete(from_position, to_position)) def action_delete_line(self) -> None: - """Deletes the entire line that the cursor is currently on.""" + """Deletes the line the cursor is on.""" cursor_row, _ = self.cursor_position - from_position = (cursor_row, 0) to_position = (cursor_row + 1, 0) - self.perform_edit(Delete(from_position, to_position)) def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor position to the start of the line.""" - cursor_row, cursor_column = self.cursor_position - - self.document_lines[cursor_row] = self.document_lines[cursor_row][ - cursor_column: - ] - self.cursor_position = (cursor_row, 0) + from_position = self.cursor_position + to_position = (cursor_row, 0) + self.perform_edit(Delete(from_position, to_position)) def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor position to the end of the line.""" cursor_row, cursor_column = self.cursor_position - self.document_lines[cursor_row] = self.document_lines[cursor_row][ - :cursor_column - ] - self.refresh() + from_position = self.cursor_position + to_position = (cursor_row, len(self.document_lines[cursor_row])) + self.perform_edit(Delete(from_position, to_position)) # --- Debug actions def action_print_line_cache(self) -> None: @@ -983,7 +989,7 @@ def debug_state(self) -> "EditorDebug": document_size=self._document_size, virtual_size=self.virtual_size, scroll=self.scroll_offset, - undo_stack=self._undo_stack, + undo_stack=list(reversed(self._undo_stack)), # tree_sexp=self._syntax_tree.root_node.sexp(), tree_sexp="", active_line_text=repr(self.active_line_text), From 6f55f343efdba4884eb6781ff31c2465d50a6165 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Jul 2023 16:18:24 +0100 Subject: [PATCH 049/366] Begin moving insert to work on ranges --- src/textual/widgets/_text_editor.py | 51 ++++++++++--------- tests/text_area/test_text_area_tree_sitter.py | 0 tests/text_editor/test_text_editor_insert.py | 11 ++++ 3 files changed, 38 insertions(+), 24 deletions(-) delete mode 100644 tests/text_area/test_text_area_tree_sitter.py create mode 100644 tests/text_editor/test_text_editor_insert.py diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index a7bb34bd2e..7038c27716 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import re from collections import defaultdict from dataclasses import dataclass @@ -415,7 +416,7 @@ def _prepare_highlights( for line_index, updated_highlights in highlight_updates.items(): highlights[line_index] = updated_highlights - def perform_edit(self, edit: Edit) -> None: + def edit(self, edit: Edit) -> None: log.debug(f"performing edit {edit!r}") edit.do(self) self._undo_stack.append(edit) @@ -673,7 +674,7 @@ def active_line_text(self) -> str: def insert_text( self, text: str, position: tuple[int, int], move_cursor: bool = True ) -> None: - self.perform_edit(Insert(text, position, move_cursor)) + self.edit(Insert(text, position, move_cursor)) def _insert_text( self, text: str, position: tuple[int, int], move_cursor: bool = True @@ -750,6 +751,20 @@ def _insert_text( self._prepare_highlights(start_point, end_point) + def insert_text_range( + self, text: str, from_position: tuple[int, int], to_position: tuple[int, int] + ) -> None: + """Insert text at a given range and move the cursor to the end of the inserted text.""" + + # If we're inserting a single newline character, this is just a split. + # Delete the range first + self._delete_range(from_position, to_position, None) + if text == os.linesep: + self.split_line(from_position) + + # Split the inserted text into lines + lines = text.splitlines() + def _position_to_byte_offset(self, position: tuple[int, int]) -> int: """Given a document coordinate, return the byte offset of that coordinate.""" @@ -761,7 +776,7 @@ def _position_to_byte_offset(self, position: tuple[int, int]) -> int: bytes_this_line_left_of_cursor = len(lines[row][:column]) return bytes_lines_above + bytes_this_line_left_of_cursor - def split_line(self): + def split_line(self, position: tuple[int, int]) -> None: """ Splits the current line at the cursor's position and updates the cursor position. @@ -770,7 +785,7 @@ def split_line(self): line after the cursor becomes a new line below the current line. The cursor then moves to the start of this new line. """ - cursor_row, cursor_column = self.cursor_position + cursor_row, cursor_column = position # Get the current line's indentation (leading whitespace) current_line = self.document_lines[cursor_row] @@ -807,7 +822,6 @@ def dedent_line(self) -> None: A dedent is simply a Delete operation on some amount of whitespace which may exist at the start of a line. """ - cursor_row, cursor_column = self.cursor_position # Define one level of indentation as four spaces @@ -884,21 +898,11 @@ def _delete_range( # Move the cursor to the start of the deleted range self.cursor_position = (from_row, from_column) + self._refresh_size() return deleted_text def action_delete_left(self) -> None: - """ - Deletes the character to the left of the cursor and updates the cursor position. - - If the cursor is at the start of a line, it deletes the newline character that separates - the current line from the previous one, effectively merging the two lines. The cursor - then moves to the end of what was previously the line above. - - If the cursor is not at the start of a line, it deletes the character to the left of the - cursor within the current line. The cursor then moves one space to the left. - - If the cursor is at the start of the document, no action is taken. - """ + """Deletes the character to the left of the cursor and updates the cursor position.""" if self.cursor_at_start_of_document: return @@ -910,11 +914,10 @@ def action_delete_left(self) -> None: else: to_position = (cursor_row, cursor_column - 1) - self.perform_edit(Delete(from_position, to_position)) + self.edit(Delete(from_position, to_position)) def action_delete_right(self) -> None: - """Deletes the character to the right of the cursor and keeps the cursor at - the same position.""" + """Deletes the character to the right of the cursor and keeps the cursor at the same position.""" if self.cursor_at_end_of_document: return @@ -925,28 +928,28 @@ def action_delete_right(self) -> None: else: to_position = (cursor_row, cursor_column + 1) - self.perform_edit(Delete(from_position, to_position)) + self.edit(Delete(from_position, to_position)) def action_delete_line(self) -> None: """Deletes the line the cursor is on.""" cursor_row, _ = self.cursor_position from_position = (cursor_row, 0) to_position = (cursor_row + 1, 0) - self.perform_edit(Delete(from_position, to_position)) + self.edit(Delete(from_position, to_position)) def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor position to the start of the line.""" cursor_row, cursor_column = self.cursor_position from_position = self.cursor_position to_position = (cursor_row, 0) - self.perform_edit(Delete(from_position, to_position)) + self.edit(Delete(from_position, to_position)) def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor position to the end of the line.""" cursor_row, cursor_column = self.cursor_position from_position = self.cursor_position to_position = (cursor_row, len(self.document_lines[cursor_row])) - self.perform_edit(Delete(from_position, to_position)) + self.edit(Delete(from_position, to_position)) # --- Debug actions def action_print_line_cache(self) -> None: diff --git a/tests/text_area/test_text_area_tree_sitter.py b/tests/text_area/test_text_area_tree_sitter.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/text_editor/test_text_editor_insert.py b/tests/text_editor/test_text_editor_insert.py new file mode 100644 index 0000000000..1f18dadbce --- /dev/null +++ b/tests/text_editor/test_text_editor_insert.py @@ -0,0 +1,11 @@ +from textual.widgets import TextEditor + +TEXT = """I must not fear. +Fear is the mind-killer.""" + + +def test_insert_text_range() -> None: + editor = TextEditor() + editor.load_text(TEXT) + editor.insert_text_range("\n", (0, 0), (0, 0)) + assert editor.document_lines == ["", "I must not fear.", "Fear is the mind-killer."] From c08f8371516fe9843472d10f1724de55a74e11e2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 17 Jul 2023 16:12:00 +0100 Subject: [PATCH 050/366] Insertion improvements --- src/textual/widgets/_text_editor.py | 93 +++++++++++++++----- tests/text_editor/test_text_editor_insert.py | 9 +- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 7038c27716..72b5aa8eb2 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -75,11 +75,14 @@ class Insert(NamedTuple): """Implements the Edit protocol for inserting text at some position.""" text: str - position: tuple[int, int] + from_position: tuple[int, int] + to_position: tuple[int, int] move_cursor: bool = True def do(self, editor: TextEditor) -> None: - editor._insert_text(self.text, self.position, self.move_cursor) + editor._insert_text_range( + self.text, self.from_position, self.to_position, self.move_cursor + ) def undo(self, editor: TextEditor) -> None: """Undo the action.""" @@ -433,18 +436,18 @@ def undo(self) -> None: def _on_key(self, event: events.Key) -> None: log.debug(f"{event!r}") key = event.key - if event.is_printable or key == "tab": + if event.is_printable or key == "tab" or key == "enter": if key == "tab": insert = " " + elif key == "enter": + insert = "\n" else: insert = event.character event.stop() assert event.character is not None - - self.insert_text(insert, self.cursor_position) + cursor_position = self.cursor_position + self.insert_text_range(insert, cursor_position, cursor_position) event.prevent_default() - elif key == "enter": - self.split_line() elif key == "shift+tab": self.dedent_line() event.stop() @@ -477,7 +480,7 @@ def _on_click(self, event: events.Click) -> None: def _on_paste(self, event: events.Paste) -> None: text = event.text if text: - self._insert_text(text, self.cursor_position) + self.insert_text(text, self.cursor_position) event.stop() # --- Reactive watchers and validators @@ -674,7 +677,16 @@ def active_line_text(self) -> str: def insert_text( self, text: str, position: tuple[int, int], move_cursor: bool = True ) -> None: - self.edit(Insert(text, position, move_cursor)) + self.edit(Insert(text, position, position, move_cursor)) + + def insert_text_range( + self, + text: str, + from_position: tuple[int, int], + to_position: tuple[int, int], + move_cursor: bool = True, + ): + self.edit(Insert(text, from_position, to_position, move_cursor)) def _insert_text( self, text: str, position: tuple[int, int], move_cursor: bool = True @@ -751,19 +763,59 @@ def _insert_text( self._prepare_highlights(start_point, end_point) - def insert_text_range( - self, text: str, from_position: tuple[int, int], to_position: tuple[int, int] + def _insert_text_range( + self, + text: str, + from_position: tuple[int, int], + to_position: tuple[int, int], + move_cursor: bool = True, ) -> None: """Insert text at a given range and move the cursor to the end of the inserted text.""" - # If we're inserting a single newline character, this is just a split. - # Delete the range first - self._delete_range(from_position, to_position, None) - if text == os.linesep: - self.split_line(from_position) + inserted_text = text + lines = self.document_lines - # Split the inserted text into lines - lines = text.splitlines() + from_row, from_column = from_position + to_row, to_column = to_position + + if from_position > to_position: + from_row, from_column, to_row, to_column = ( + to_row, + to_column, + from_row, + from_column, + ) + + insert_lines = inserted_text.splitlines() + if inserted_text.endswith("\n"): + # Special case where a single newline character is inserted. + insert_lines.append("") + + before_selection = lines[from_row][:from_column] + after_selection = lines[to_row][to_column:] + + insert_lines[0] = before_selection + insert_lines[0] + destination_column = len(insert_lines[-1]) + insert_lines[-1] = insert_lines[-1] + after_selection + lines[from_row : to_row + 1] = insert_lines + destination_row = from_row + len(insert_lines) - 1 + + cursor_destination = (destination_row, destination_column) + + start_byte = self._position_to_byte_offset(from_position) + self._syntax_tree.edit( + start_byte=start_byte, + old_end_byte=self._position_to_byte_offset(to_position), + new_end_byte=start_byte + len(inserted_text), + start_point=from_position, + old_end_point=to_position, + new_end_point=cursor_destination, + ) + self._syntax_tree = self._parser.parse(self._read_callable, self._syntax_tree) + self._prepare_highlights() + self._refresh_size() + if move_cursor: + self.cursor_position = cursor_destination def _position_to_byte_offset(self, position: tuple[int, int]) -> int: """Given a document coordinate, return the byte offset of that coordinate.""" @@ -875,7 +927,8 @@ def _delete_range( start_line = lines[from_row] end_line = lines[to_row] - # TODO - I think this might be slightly off. + # TODO - I think this might be slightly off - merging lines will + # result in two newline characters. # When you delete a line, it records the deleted text with two newlines at the end instead of 1. # Add the deleted segments from the start and end lines to the deleted text deleted_text = ( @@ -892,13 +945,13 @@ def _delete_range( # Delete the lines in between del lines[from_row + 1 : to_row + 1] + self._refresh_size() if cursor_destination is not None: self.cursor_position = cursor_destination else: # Move the cursor to the start of the deleted range self.cursor_position = (from_row, from_column) - self._refresh_size() return deleted_text def action_delete_left(self) -> None: diff --git a/tests/text_editor/test_text_editor_insert.py b/tests/text_editor/test_text_editor_insert.py index 1f18dadbce..513d1db71f 100644 --- a/tests/text_editor/test_text_editor_insert.py +++ b/tests/text_editor/test_text_editor_insert.py @@ -4,8 +4,15 @@ Fear is the mind-killer.""" -def test_insert_text_range() -> None: +def test_insert_text_range_newline_file_start() -> None: editor = TextEditor() editor.load_text(TEXT) editor.insert_text_range("\n", (0, 0), (0, 0)) assert editor.document_lines == ["", "I must not fear.", "Fear is the mind-killer."] + + +def test_insert_text_range_newline_splits_line() -> None: + editor = TextEditor() + editor.load_text(TEXT) + editor.insert_text_range("\n", (0, 1), (0, 1)) + assert editor.document_lines == ["I", " must not fear.", "Fear is the mind-killer."] From c0eb2e61a45e597a4d22d76f12951893986d1097 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 17 Jul 2023 16:27:34 +0100 Subject: [PATCH 051/366] Update the syntax tree during deletion --- src/textual/widgets/_text_editor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 72b5aa8eb2..ce353c8178 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -945,6 +945,17 @@ def _delete_range( # Delete the lines in between del lines[from_row + 1 : to_row + 1] + start_byte = self._position_to_byte_offset(from_position) + self._syntax_tree.edit( + start_byte=start_byte, + old_end_byte=self._position_to_byte_offset(to_position), + new_end_byte=start_byte, + start_point=from_position, + old_end_point=to_position, + new_end_point=from_position, + ) + self._syntax_tree = self._parser.parse(self._read_callable, self._syntax_tree) + self._prepare_highlights() self._refresh_size() if cursor_destination is not None: self.cursor_position = cursor_destination From 53a4bb5005d79fdc1e975a95d772049c425ea749 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 17 Jul 2023 16:31:44 +0100 Subject: [PATCH 052/366] Remove old methods for splitting lines and inserting text --- src/textual/widgets/_text_editor.py | 115 ---------------------------- 1 file changed, 115 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index ce353c8178..95c6d7bae9 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -688,81 +688,6 @@ def insert_text_range( ): self.edit(Insert(text, from_position, to_position, move_cursor)) - def _insert_text( - self, text: str, position: tuple[int, int], move_cursor: bool = True - ) -> None: - log.debug(f"insert {text!r} at {self.cursor_position!r}") - - start_byte = self._position_to_byte_offset(self.cursor_position) - start_point = position - - target_row, target_column = position - - lines = self.document_lines - - line = lines[target_row] - text_before_cursor = line[:target_column] - text_after_cursor = line[target_column:] - - replacement_lines = text.splitlines(keepends=False) - replacement_lines[0] = text_before_cursor + replacement_lines[0] - end_column = len(replacement_lines[-1]) - replacement_lines[-1] += text_after_cursor - - self.document_lines[target_row : target_row + 1] = replacement_lines - - longest_modified_line = max(cell_len(line) for line in replacement_lines) - document_width, document_height = self._document_size - - # The virtual width of the row is the cell length of the text in the row - # plus 1 to accommodate for a cursor potentially "resting" at the end of the row - insertion_width = longest_modified_line + 1 - - self._refresh_size() - - if move_cursor: - self.cursor_position = (target_row + len(replacement_lines) - 1, end_column) - - edit_args = { - "start_byte": start_byte, - "old_end_byte": start_byte, - "new_end_byte": start_byte + len(text), # TODO - what about newlines? - "start_point": start_point, - "old_end_point": start_point, - "new_end_point": self.cursor_position, - } - log.debug(edit_args) - - # Edit the tree in place - old_tree = self._syntax_tree - old_tree.edit(**edit_args) - new_tree = self._parser.parse(self._read_callable, old_tree) - - changed_ranges = old_tree.get_changed_ranges(new_tree) - - self._syntax_tree = new_tree - log.debug(f"changed = {changed_ranges!r}") - - # Limit the range, rather arbitrarily for now. - # Perhaps we do the incremental parsing within a window here, then have some - # heuristic for wider parsing inside on_idle? - scroll_y = max(0, int(self.scroll_y)) - - visible_start_line = scroll_y - height = self.region.height or len(self.document_lines) - 1 - visible_end_line = scroll_y + height - - highlight_window_leeway = 10 - start_point = (max(0, visible_start_line - highlight_window_leeway), 0) - - end_row_index = min( - len(self.document_lines) - 1, visible_end_line + highlight_window_leeway - ) - end_line = self.document_lines[end_row_index] - end_point = (end_row_index, len(end_line) - 1) - - self._prepare_highlights(start_point, end_point) - def _insert_text_range( self, text: str, @@ -828,46 +753,6 @@ def _position_to_byte_offset(self, position: tuple[int, int]) -> int: bytes_this_line_left_of_cursor = len(lines[row][:column]) return bytes_lines_above + bytes_this_line_left_of_cursor - def split_line(self, position: tuple[int, int]) -> None: - """ - Splits the current line at the cursor's position and updates the cursor position. - - This method splits the current line into two at the cursor's column position, - effectively inserting a newline character at the cursor's position. The part of the - line after the cursor becomes a new line below the current line. The cursor then - moves to the start of this new line. - """ - cursor_row, cursor_column = position - - # Get the current line's indentation (leading whitespace) - current_line = self.document_lines[cursor_row] - indentation = len(current_line) - len(current_line.lstrip()) - - # Split the current line into two lines at the cursor position - line_before = current_line[:cursor_column] - line_after = current_line[cursor_column:] - - # If the line ends with ':' or '{', add additional indentation to the new line - additional_indent = " " # Four spaces - if line_before.rstrip().endswith((":", "{")): - indentation += len(additional_indent) - elif cursor_row < len(self.document_lines) - 1: - # If there is a line below, match its indentation - next_line = self.document_lines[cursor_row + 1] - next_line_indentation = len(next_line) - len(next_line.lstrip()) - indentation = next_line_indentation - - # Add the indentation to the start of the new line - line_after = " " * indentation + line_after - - # Update the lines in the document - self.document_lines[cursor_row] = line_before - self.document_lines.insert(cursor_row + 1, line_after) - - self._refresh_size() - # Move the cursor to the start of the new line - self.cursor_position = (cursor_row + 1, indentation) - def dedent_line(self) -> None: """Reduces the indentation of the current line by one level. From bb77414bf10d84f01f7d12e50d6ba41c9f4e0dff Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 17 Jul 2023 16:51:10 +0100 Subject: [PATCH 053/366] Only edit syntax tree when it is not None. Add tests for inserting multiple lines --- src/textual/widgets/_text_editor.py | 49 +++++++++++--------- tests/text_editor/test_text_editor_insert.py | 33 +++++++++++-- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 95c6d7bae9..b663fd7468 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -728,16 +728,19 @@ def _insert_text_range( cursor_destination = (destination_row, destination_column) start_byte = self._position_to_byte_offset(from_position) - self._syntax_tree.edit( - start_byte=start_byte, - old_end_byte=self._position_to_byte_offset(to_position), - new_end_byte=start_byte + len(inserted_text), - start_point=from_position, - old_end_point=to_position, - new_end_point=cursor_destination, - ) - self._syntax_tree = self._parser.parse(self._read_callable, self._syntax_tree) - self._prepare_highlights() + if self._syntax_tree is not None: + self._syntax_tree.edit( + start_byte=start_byte, + old_end_byte=self._position_to_byte_offset(to_position), + new_end_byte=start_byte + len(inserted_text), + start_point=from_position, + old_end_point=to_position, + new_end_point=cursor_destination, + ) + self._syntax_tree = self._parser.parse( + self._read_callable, self._syntax_tree + ) + self._prepare_highlights() self._refresh_size() if move_cursor: self.cursor_position = cursor_destination @@ -830,17 +833,21 @@ def _delete_range( # Delete the lines in between del lines[from_row + 1 : to_row + 1] - start_byte = self._position_to_byte_offset(from_position) - self._syntax_tree.edit( - start_byte=start_byte, - old_end_byte=self._position_to_byte_offset(to_position), - new_end_byte=start_byte, - start_point=from_position, - old_end_point=to_position, - new_end_point=from_position, - ) - self._syntax_tree = self._parser.parse(self._read_callable, self._syntax_tree) - self._prepare_highlights() + if self._syntax_tree is not None: + start_byte = self._position_to_byte_offset(from_position) + self._syntax_tree.edit( + start_byte=start_byte, + old_end_byte=self._position_to_byte_offset(to_position), + new_end_byte=start_byte, + start_point=from_position, + old_end_point=to_position, + new_end_point=from_position, + ) + self._syntax_tree = self._parser.parse( + self._read_callable, self._syntax_tree + ) + self._prepare_highlights() + self._refresh_size() if cursor_destination is not None: self.cursor_position = cursor_destination diff --git a/tests/text_editor/test_text_editor_insert.py b/tests/text_editor/test_text_editor_insert.py index 513d1db71f..6347ba75ff 100644 --- a/tests/text_editor/test_text_editor_insert.py +++ b/tests/text_editor/test_text_editor_insert.py @@ -4,15 +4,40 @@ Fear is the mind-killer.""" -def test_insert_text_range_newline_file_start() -> None: +def test_insert_text_range_newline_file_start(): editor = TextEditor() editor.load_text(TEXT) - editor.insert_text_range("\n", (0, 0), (0, 0)) + editor.insert_text("\n", (0, 0)) assert editor.document_lines == ["", "I must not fear.", "Fear is the mind-killer."] -def test_insert_text_range_newline_splits_line() -> None: +def test_insert_text_newline_splits_line(): editor = TextEditor() editor.load_text(TEXT) - editor.insert_text_range("\n", (0, 1), (0, 1)) + editor.insert_text("\n", (0, 1)) assert editor.document_lines == ["I", " must not fear.", "Fear is the mind-killer."] + + +def test_insert_text_multiple_lines_ends_with_newline(): + editor = TextEditor() + editor.load_text(TEXT) + editor.insert_text("Hello,\nworld!\n", (0, 1)) + assert editor.document_lines == [ + "IHello,", + "world!", + " must not fear.", + "Fear is the mind-killer.", + ] + + +def test_insert_text_multiple_lines_starts_with_newline(): + editor = TextEditor() + editor.load_text(TEXT) + editor.insert_text("\nHello,\nworld!\n", (0, 1)) + assert editor.document_lines == [ + "I", + "Hello,", + "world!", + " must not fear.", + "Fear is the mind-killer.", + ] From aecc03c577d4968aa3aacd61c9bbf1c40969ffa9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 17 Jul 2023 17:02:13 +0100 Subject: [PATCH 054/366] Some more initial tests for text insertion in the text editor --- src/textual/widgets/_text_editor.py | 7 +-- tests/text_editor/test_text_editor_insert.py | 47 ++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index b663fd7468..2c4325bbff 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -80,9 +80,10 @@ class Insert(NamedTuple): move_cursor: bool = True def do(self, editor: TextEditor) -> None: - editor._insert_text_range( - self.text, self.from_position, self.to_position, self.move_cursor - ) + if self.text: + editor._insert_text_range( + self.text, self.from_position, self.to_position, self.move_cursor + ) def undo(self, editor: TextEditor) -> None: """Undo the action.""" diff --git a/tests/text_editor/test_text_editor_insert.py b/tests/text_editor/test_text_editor_insert.py index 6347ba75ff..31c68c851a 100644 --- a/tests/text_editor/test_text_editor_insert.py +++ b/tests/text_editor/test_text_editor_insert.py @@ -4,6 +4,40 @@ Fear is the mind-killer.""" +def test_insert_text_no_newlines(): + editor = TextEditor() + editor.load_text(TEXT) + editor.insert_text(" really", (0, 1)) + assert editor.document_lines == [ + "I really must not fear.", + "Fear is the mind-killer.", + ] + + +def test_insert_text_empty_string(): + editor = TextEditor() + editor.load_text(TEXT) + editor.insert_text("", (0, 1)) + assert editor.document_lines == ["I must not fear.", "Fear is the mind-killer."] + + +def test_insert_text_invalid_column(): + # TODO - what is the correct behaviour here? + # right now it appends to the end of the line if the column is too large. + editor = TextEditor() + editor.load_text(TEXT) + editor.insert_text(" really", (0, 999)) + assert editor.document_lines == ["I must not fear.", "Fear is the mind-killer."] + + +def test_insert_text_invalid_row(): + # TODO - this raises an IndexError for list index out of range + editor = TextEditor() + editor.load_text(TEXT) + editor.insert_text(" really", (999, 0)) + assert editor.document_lines == ["I must not fear.", "Fear is the mind-killer."] + + def test_insert_text_range_newline_file_start(): editor = TextEditor() editor.load_text(TEXT) @@ -41,3 +75,16 @@ def test_insert_text_multiple_lines_starts_with_newline(): " must not fear.", "Fear is the mind-killer.", ] + + +def test_insert_range_text_no_newlines(): + editor = TextEditor() + editor.load_text(TEXT) + editor.insert_text_range("REALLY", (0, 2), (0, 8)) + + # TODO - this is failing - I think we're not properly attaching the right + # side of the range from the end position of the selection. + assert editor.document_lines == [ + "I REALLY must not fear.", + "Fear is the mind-killer.", + ] From e5a651e8a0713a6c285ce294bce320db676a5b78 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 17 Jul 2023 17:05:55 +0100 Subject: [PATCH 055/366] Add a todo --- src/textual/widgets/_text_editor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 2c4325bbff..eb7a4d36f0 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -720,6 +720,9 @@ def _insert_text_range( before_selection = lines[from_row][:from_column] after_selection = lines[to_row][to_column:] + # TODO - there;s a failing test suggesting I'm taking the wrong + # slice here somewhere - look at the output of `test_insert_range_text_no_newlines`. + insert_lines[0] = before_selection + insert_lines[0] destination_column = len(insert_lines[-1]) insert_lines[-1] = insert_lines[-1] + after_selection From db813f9ea19503484b86ef8fe6876997ad67ab30 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 18 Jul 2023 13:49:53 +0100 Subject: [PATCH 056/366] Test fixes --- src/textual/widgets/_text_editor.py | 3 --- tests/text_editor/test_text_editor_delete.py | 0 tests/text_editor/test_text_editor_insert.py | 12 +++++++----- 3 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 tests/text_editor/test_text_editor_delete.py diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index eb7a4d36f0..2c4325bbff 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -720,9 +720,6 @@ def _insert_text_range( before_selection = lines[from_row][:from_column] after_selection = lines[to_row][to_column:] - # TODO - there;s a failing test suggesting I'm taking the wrong - # slice here somewhere - look at the output of `test_insert_range_text_no_newlines`. - insert_lines[0] = before_selection + insert_lines[0] destination_column = len(insert_lines[-1]) insert_lines[-1] = insert_lines[-1] + after_selection diff --git a/tests/text_editor/test_text_editor_delete.py b/tests/text_editor/test_text_editor_delete.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/text_editor/test_text_editor_insert.py b/tests/text_editor/test_text_editor_insert.py index 31c68c851a..fe98e226ce 100644 --- a/tests/text_editor/test_text_editor_insert.py +++ b/tests/text_editor/test_text_editor_insert.py @@ -1,3 +1,5 @@ +import pytest + from textual.widgets import TextEditor TEXT = """I must not fear. @@ -21,6 +23,7 @@ def test_insert_text_empty_string(): assert editor.document_lines == ["I must not fear.", "Fear is the mind-killer."] +@pytest.mark.xfail(reason="undecided on behaviour") def test_insert_text_invalid_column(): # TODO - what is the correct behaviour here? # right now it appends to the end of the line if the column is too large. @@ -30,6 +33,7 @@ def test_insert_text_invalid_column(): assert editor.document_lines == ["I must not fear.", "Fear is the mind-killer."] +@pytest.mark.xfail(reason="undecided on behaviour") def test_insert_text_invalid_row(): # TODO - this raises an IndexError for list index out of range editor = TextEditor() @@ -78,13 +82,11 @@ def test_insert_text_multiple_lines_starts_with_newline(): def test_insert_range_text_no_newlines(): + """Ensuring we can do a simple replacement of text.""" editor = TextEditor() editor.load_text(TEXT) - editor.insert_text_range("REALLY", (0, 2), (0, 8)) - - # TODO - this is failing - I think we're not properly attaching the right - # side of the range from the end position of the selection. + editor.insert_text_range("MUST", (0, 2), (0, 6)) assert editor.document_lines == [ - "I REALLY must not fear.", + "I MUST not fear.", "Fear is the mind-killer.", ] From a5fb2c337860322ab96559864d8faaa68ed4b1fb Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 18 Jul 2023 15:15:13 +0100 Subject: [PATCH 057/366] Public delete method and tests --- src/textual/widgets/_text_editor.py | 27 +++- tests/text_editor/test_text_editor_delete.py | 0 .../test_text_editor_document_delete.py | 117 ++++++++++++++++++ ...py => test_text_editor_document_insert.py} | 0 4 files changed, 138 insertions(+), 6 deletions(-) delete mode 100644 tests/text_editor/test_text_editor_delete.py create mode 100644 tests/text_editor/test_text_editor_document_delete.py rename tests/text_editor/{test_text_editor_insert.py => test_text_editor_document_insert.py} (100%) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 2c4325bbff..c6ab2a3f65 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -64,10 +64,10 @@ class Highlight(NamedTuple): class Edit(Protocol): """Protocol for actions performed in the text editor that can be done and undone.""" - def do(self, editor: TextEditor) -> None: + def do(self, editor: TextEditor) -> object | None: """Do the action.""" - def undo(self, editor: TextEditor) -> None: + def undo(self, editor: TextEditor) -> object | None: """Undo the action.""" @@ -100,6 +100,7 @@ def do(self, editor: TextEditor) -> None: self.deleted_text = editor._delete_range( self.from_position, self.to_position, self.cursor_destination ) + return self.deleted_text def undo(self, editor: TextEditor) -> None: """Undo the action.""" @@ -264,7 +265,9 @@ def load_text(self, text: str) -> None: self.load_lines(lines) def load_lines(self, lines: list[str]) -> None: - """Load text from a list of lines into the editor.""" + """Load text from a list of lines into the editor. + + This will replace any previously loaded lines.""" self.document_lines = lines # TODO Offer maximum line width and wrap if needed @@ -277,6 +280,9 @@ def load_lines(self, lines: list[str]) -> None: log.debug(f"loaded text. parser = {self._parser} ast = {self._syntax_tree}") + def clear(self) -> None: + self.load_text("") + # --- Methods for measuring things (e.g. virtual sizes) def _get_document_size(self, document_lines: list[str]) -> Size: """Return the virtual size of the document - the document only @@ -420,14 +426,16 @@ def _prepare_highlights( for line_index, updated_highlights in highlight_updates.items(): highlights[line_index] = updated_highlights - def edit(self, edit: Edit) -> None: + def edit(self, edit: Edit) -> object | None: log.debug(f"performing edit {edit!r}") - edit.do(self) + result = edit.do(self) self._undo_stack.append(edit) # TODO: Think about this... self._undo_stack = self._undo_stack[-20:] + return result + def undo(self) -> None: if self._undo_stack: action = self._undo_stack.pop() @@ -494,7 +502,6 @@ def _on_paste(self, event: events.Paste) -> None: def watch_cursor_position( self, old_position: tuple[int, int], new_position: tuple[int, int] ) -> None: - log.debug("scrolling cursor into view") self.scroll_cursor_visible() def watch_virtual_size(self, vs): @@ -780,6 +787,14 @@ def dedent_line(self) -> None: self._refresh_size() self.refresh() + def delete_range( + self, + from_position: tuple[int, int], + to_position: tuple[int, int], + cursor_destination: tuple[int, int] | None = None, + ) -> str: + return self.edit(Delete(from_position, to_position, cursor_destination)) + def _delete_range( self, from_position: tuple[int, int], diff --git a/tests/text_editor/test_text_editor_delete.py b/tests/text_editor/test_text_editor_delete.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/text_editor/test_text_editor_document_delete.py b/tests/text_editor/test_text_editor_document_delete.py new file mode 100644 index 0000000000..df8418cfb5 --- /dev/null +++ b/tests/text_editor/test_text_editor_document_delete.py @@ -0,0 +1,117 @@ +import pytest + +from textual.widgets import TextEditor + +TEXT = """I must not fear. +Fear is the mind-killer. +I forgot the rest of the quote. +Sorry Will.""" + + +@pytest.fixture +def editor(): + editor = TextEditor() + editor.load_text(TEXT) + return editor + + +def test_delete_range_single_character(editor): + deleted_text = editor.delete_range((0, 0), (0, 1)) + assert deleted_text == "I" + assert editor.document_lines == [ + " must not fear.", + "Fear is the mind-killer.", + "I forgot the rest of the quote.", + "Sorry Will.", + ] + + +@pytest.mark.xfail(reason="deleted text incorrect returns 2 newlines") +def test_delete_range_single_newline(editor): + """Testing deleting newline from right to left""" + deleted_text = editor.delete_range((1, 0), (0, 16)) + assert deleted_text == "\n" + assert editor.document_lines == [ + "I must not fear.Fear is the mind-killer.", + "I forgot the rest of the quote.", + "Sorry Will.", + ] + + +def test_delete_range_single_character_end_of_document_newline(editor): + """Check deleting the newline character at the end of the document""" + deleted_text = editor.delete_range((1, 0), (0, 16)) + assert deleted_text == "\n" + assert editor.document_lines == [ + "I must not fear.", + "Fear is the mind-killer.", + "I forgot the rest of the quote.", + "Sorry Will.", + ] + + +def test_delete_range_multiple_characters_on_one_line(editor): + deleted_text = editor.delete_range((0, 2), (0, 7)) + assert deleted_text == "must " + assert editor.document_lines == [ + "I not fear.", + "Fear is the mind-killer.", + "I forgot the rest of the quote.", + "Sorry Will.", + ] + + +def test_delete_range_multiple_lines_partially_spanned(editor): + """Deleting a selection that partially spans the first and final lines of the selection.""" + deleted_text = editor.delete_range((0, 2), (2, 2)) + assert deleted_text == "must not fear.\nFear is the mind-killer.\nI " + assert editor.document_lines == [ + "I forgot the rest of the quote.", + "Sorry Will.", + ] + + +@pytest.mark.xfail(reason="deleted text incorrect returns 2 newlines") +def test_delete_range_end_of_line(editor): + """Testing deleting newline from left to right""" + deleted_text = editor.delete_range((0, 16), (1, 0)) + assert deleted_text == "\n" + assert editor.document_lines == [ + "I must not fear.Fear is the mind-killer.", + "I forgot the rest of the quote.", + "Sorry Will.", + ] + + +def test_delete_range_single_line_excluding_newline(editor): + """Delete from the start to the end of the line.""" + deleted_text = editor.delete_range((2, 0), (2, 31)) + assert deleted_text == "I forgot the rest of the quote." + assert editor.document_lines == [ + "I must not fear.", + "Fear is the mind-killer.", + "", + "Sorry Will.", + ] + + +@pytest.mark.xfail(reason="double newline issue again") +def test_delete_range_single_line_including_newline(editor): + """Delete from the start of a line to the start of the line below.""" + deleted_text = editor.delete_range((2, 0), (3, 0)) + assert deleted_text == "I forgot the rest of the quote.\n" + assert editor.document_lines == [ + "I must not fear.", + "Fear is the mind-killer.", + "Sorry Will.", + ] + + +def test_delete_range_single_character_start_of_document(): + """Check deletion of the first character in the document""" + pass + + +def test_delete_range_single_character_end_of_document_newline(): + """Check deleting the newline character at the end of the document""" + pass diff --git a/tests/text_editor/test_text_editor_insert.py b/tests/text_editor/test_text_editor_document_insert.py similarity index 100% rename from tests/text_editor/test_text_editor_insert.py rename to tests/text_editor/test_text_editor_document_insert.py From 34a14a52628e2c1ae541dc69142d568f71012e73 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 18 Jul 2023 16:01:34 +0100 Subject: [PATCH 058/366] Fix deleted text newlines being incorrect, remove xfails --- src/textual/widgets/_text_editor.py | 14 +++++++------- .../test_text_editor_document_delete.py | 3 --- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index c6ab2a3f65..ad9c1812d1 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -835,13 +835,13 @@ def _delete_range( # result in two newline characters. # When you delete a line, it records the deleted text with two newlines at the end instead of 1. # Add the deleted segments from the start and end lines to the deleted text - deleted_text = ( - start_line[from_column:] - + "\n" - + "\n".join(self.document_lines[from_row + 1 : to_row]) - + "\n" - + end_line[:to_column] - ) + deleted_text = start_line[from_column:] + "\n" + for row in range(from_row + 1, to_row): + deleted_text += lines[row] + "\n" + + deleted_text += end_line[:to_column] + if to_column == len(end_line): + deleted_text += "\n" # Update the lines at the start and end of the range lines[from_row] = start_line[:from_column] + end_line[to_column:] diff --git a/tests/text_editor/test_text_editor_document_delete.py b/tests/text_editor/test_text_editor_document_delete.py index df8418cfb5..809b882b79 100644 --- a/tests/text_editor/test_text_editor_document_delete.py +++ b/tests/text_editor/test_text_editor_document_delete.py @@ -26,7 +26,6 @@ def test_delete_range_single_character(editor): ] -@pytest.mark.xfail(reason="deleted text incorrect returns 2 newlines") def test_delete_range_single_newline(editor): """Testing deleting newline from right to left""" deleted_text = editor.delete_range((1, 0), (0, 16)) @@ -71,7 +70,6 @@ def test_delete_range_multiple_lines_partially_spanned(editor): ] -@pytest.mark.xfail(reason="deleted text incorrect returns 2 newlines") def test_delete_range_end_of_line(editor): """Testing deleting newline from left to right""" deleted_text = editor.delete_range((0, 16), (1, 0)) @@ -95,7 +93,6 @@ def test_delete_range_single_line_excluding_newline(editor): ] -@pytest.mark.xfail(reason="double newline issue again") def test_delete_range_single_line_including_newline(editor): """Delete from the start of a line to the start of the line below.""" deleted_text = editor.delete_range((2, 0), (3, 0)) From dbbfcf2d51a50aade43f1f37e8d1a7b4a491e384 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 18 Jul 2023 16:11:24 +0100 Subject: [PATCH 059/366] Adding a todo --- src/textual/widgets/_text_editor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index ad9c1812d1..bd00c4f5e4 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -778,6 +778,7 @@ def dedent_line(self) -> None: current_line = self.document_lines[cursor_row] # If the line is indented, reduce the indentation + # TODO - if the line is less than the indent level we should just dedent as far as possible. if current_line.startswith(indent_level): self.document_lines[cursor_row] = current_line[len(indent_level) :] From 6cec3b88ee1ab7c2ca12697b89868777ecb35aed Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 18 Jul 2023 17:38:18 +0100 Subject: [PATCH 060/366] Fix styling --- src/textual/widgets/_text_editor.py | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index bd00c4f5e4..73bd3ca857 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -115,7 +115,9 @@ def __rich_repr__(self): class TextEditor(ScrollView, can_focus=True): DEFAULT_CSS = """\ $editor-active-line-bg: white 8%; - +TextEditor { + background: $panel; +} TextEditor > .text-editor--active-line { background: $editor-active-line-bg; } @@ -156,9 +158,6 @@ class TextEditor(ScrollView, can_focus=True): "ctrl+u", "delete_to_start_of_line", "delete to line start", show=False ), Binding("ctrl+k", "delete_to_end_of_line", "delete to line end", show=False), - # Debugging bindings - Binding("ctrl+s", "print_highlight_cache", "[debug] Print highlight cache"), - Binding("ctrl+l", "print_line_cache", "[debug] Print line cache"), ] language: Reactive[str | None] = reactive(None) @@ -924,24 +923,7 @@ def action_delete_to_end_of_line(self) -> None: to_position = (cursor_row, len(self.document_lines[cursor_row])) self.edit(Delete(from_position, to_position)) - # --- Debug actions - def action_print_line_cache(self) -> None: - log.debug(self._line_cache) - - def traverse(cursor) -> Iterable[Node]: - yield cursor.node - - if cursor.goto_first_child(): - yield from traverse(cursor) - while cursor.goto_next_sibling(): - yield from traverse(cursor) - cursor.goto_parent() - - log.debug(list(traverse(self._syntax_tree.walk()))) - - def action_print_highlight_cache(self) -> None: - log.debug(self._highlights) - + # --- Debugging @dataclass class EditorDebug: cursor: tuple[int, int] From aa697451fac3f4d1cd0f7be16ba280f9fe372641 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 18 Jul 2023 17:53:07 +0100 Subject: [PATCH 061/366] Optimise imports --- src/textual/widgets/_text_editor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 73bd3ca857..1469b06078 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -1,16 +1,15 @@ from __future__ import annotations -import os import re from collections import defaultdict from dataclasses import dataclass from pathlib import Path -from typing import ClassVar, Iterable, NamedTuple +from typing import ClassVar, NamedTuple from rich.cells import get_character_cell_size from rich.style import Style from rich.text import Text -from tree_sitter import Language, Node, Parser, Tree +from tree_sitter import Language, Parser, Tree from tree_sitter.binding import Query from textual import events, log From c0619328c84592921b842d8185972b22547e0937 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 18 Jul 2023 18:46:16 +0100 Subject: [PATCH 062/366] Delete word left and right --- src/textual/widgets/_text_editor.py | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 1469b06078..a5aee3a397 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -151,7 +151,13 @@ class TextEditor(ScrollView, can_focus=True): Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False), Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False), Binding("backspace", "delete_left", "delete left", show=False), + Binding( + "ctrl+w", "delete_word_left", "delete left to start of word", show=False + ), Binding("ctrl+d", "delete_right", "delete right", show=False), + Binding( + "ctrl+f", "delete_word_right", "delete right to start of word", show=False + ), Binding("ctrl+x", "delete_line", "delete line", show=False), Binding( "ctrl+u", "delete_to_start_of_line", "delete to line start", show=False @@ -922,6 +928,52 @@ def action_delete_to_end_of_line(self) -> None: to_position = (cursor_row, len(self.document_lines[cursor_row])) self.edit(Delete(from_position, to_position)) + def action_delete_word_left(self) -> None: + """Deletes the word to the left of the cursor and updates the cursor position.""" + if self.cursor_at_start_of_document: + return + + cursor_row, cursor_column = self.cursor_position + + # Check the current line for a word boundary + line = self.document_lines[cursor_row][:cursor_column] + matches = list(re.finditer(self._word_pattern, line)) + + if matches: + # If a word boundary is found, delete the word + from_position = (cursor_row, matches[-1].start()) + elif cursor_row > 0: + # If no word boundary is found and we're not on the first line, delete to the end of the previous line + from_position = (cursor_row - 1, len(self.document_lines[cursor_row - 1])) + else: + # If we're already on the first line and no word boundary is found, delete to the start of the line + from_position = (cursor_row, 0) + + self.edit(Delete(from_position, self.cursor_position)) + + def action_delete_word_right(self) -> None: + """Deletes the word to the right of the cursor and keeps the cursor at the same position.""" + if self.cursor_at_end_of_document: + return + + cursor_row, cursor_column = self.cursor_position + + # Check the current line for a word boundary + line = self.document_lines[cursor_row][cursor_column:] + matches = list(re.finditer(self._word_pattern, line)) + + if matches: + # If a word boundary is found, delete the word + to_position = (cursor_row, cursor_column + matches[0].end()) + elif cursor_row < len(self.document_lines) - 1: + # If no word boundary is found and we're not on the last line, delete to the start of the next line + to_position = (cursor_row + 1, 0) + else: + # If we're already on the last line and no word boundary is found, delete to the end of the line + to_position = (cursor_row, len(self.document_lines[cursor_row])) + + self.edit(Delete(self.cursor_position, to_position)) + # --- Debugging @dataclass class EditorDebug: From dcf475866a17338989cccb5709293f20b1c4c6da Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 19 Jul 2023 11:28:53 +0100 Subject: [PATCH 063/366] Improve cursor/up down alignment when double width characters present --- src/textual/widgets/_text_editor.py | 63 +++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index a5aee3a397..4c80bfdd66 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -194,8 +194,9 @@ def __init__( self._highlights_query: str | None = None """The string containing the tree-sitter AST query used for syntax highlighting.""" - self._last_intentional_column: int = 0 - """Tracks the last column the user explicitly navigated to so that we can reset + self._last_intentional_cell_width: int = 0 + """Tracks the last column (measured in terms of cell length, since we care here about where + the cursor visually moves more than the logical characters) the user explicitly navigated to so that we can reset to it whenever possible.""" self._word_pattern = re.compile(r"(?<=\W)(?=\w)|(?<=\w)(?=\W)") @@ -487,8 +488,7 @@ def _on_click(self, event: events.Click) -> None: else: self.cursor_position = (target_y, len(line)) - new_row, new_column = self.cursor_position - self._last_intentional_column = new_column + self._record_last_intentional_cell_width() def _on_paste(self, event: events.Paste) -> None: text = event.text @@ -496,6 +496,18 @@ def _on_paste(self, event: events.Paste) -> None: self.insert_text(text, self.cursor_position) event.stop() + def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: + """Given a row index and a cell width, return the column that the cell width + corresponds to.""" + total_cell_offset = 0 + line = self.document_lines[row_index] + for column_index, character in enumerate(line): + if total_cell_offset >= cell_width: + log(f"cell width {cell_width} -> column_index {column_index}") + return column_index + total_cell_offset += cell_len(character) + return len(line) + # --- Reactive watchers and validators # def validate_cursor_position(self, new_position: tuple[int, int]) -> tuple[int, int]: # new_row, new_column = new_position @@ -555,7 +567,7 @@ def cursor_to_line_end(self) -> None: cursor_row, cursor_column = self.cursor_position target_column = len(self.document_lines[cursor_row]) self.cursor_position = (cursor_row, target_column) - self._last_intentional_column = target_column + self._record_last_intentional_cell_width() def cursor_to_line_start(self) -> None: cursor_row, cursor_column = self.cursor_position @@ -578,7 +590,7 @@ def action_cursor_left(self) -> None: target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above self.cursor_position = (target_row, target_column) - self._last_intentional_column = target_column + self._record_last_intentional_cell_width() def action_cursor_right(self) -> None: """Move the cursor one position to the right. @@ -594,7 +606,7 @@ def action_cursor_right(self) -> None: target_column = 0 if self.cursor_at_end_of_row else cursor_column + 1 self.cursor_position = (target_row, target_column) - self._last_intentional_column = target_column + self._record_last_intentional_cell_width() def action_cursor_down(self) -> None: """Move the cursor down one cell.""" @@ -602,10 +614,14 @@ def action_cursor_down(self) -> None: self.cursor_to_line_end() cursor_row, cursor_column = self.cursor_position - cursor_column = max(self._last_intentional_column, cursor_column) target_row = min(len(self.document_lines) - 1, cursor_row + 1) - target_column = clamp(cursor_column, 0, len(self.document_lines[target_row])) + + # Attempt to snap last intentional cell length + target_column = self.cell_width_to_column_index( + self._last_intentional_cell_width, target_row + ) + target_column = clamp(target_column, 0, len(self.document_lines[target_row])) self.cursor_position = (target_row, target_column) @@ -615,10 +631,14 @@ def action_cursor_up(self) -> None: self.cursor_to_line_start() cursor_row, cursor_column = self.cursor_position - cursor_column = max(self._last_intentional_column, cursor_column) target_row = max(0, cursor_row - 1) - target_column = clamp(cursor_column, 0, len(self.document_lines[target_row])) + + # Attempt to snap last intentional cell length + target_column = self.cell_width_to_column_index( + self._last_intentional_cell_width, target_row + ) + target_column = clamp(target_column, 0, len(self.document_lines[target_row])) self.cursor_position = (target_row, target_column) @@ -652,7 +672,7 @@ def action_cursor_left_word(self) -> None: cursor_column = 0 self.cursor_position = (cursor_row, cursor_column) - self._last_intentional_column = cursor_column + self._record_last_intentional_cell_width() def action_cursor_right_word(self) -> None: """Move the cursor right by a single word, skipping spaces.""" @@ -678,13 +698,26 @@ def action_cursor_right_word(self) -> None: cursor_column = len(self.document_lines[cursor_row]) self.cursor_position = (cursor_row, cursor_column) - self._last_intentional_column = cursor_column + self._record_last_intentional_cell_width() @property def active_line_text(self) -> str: # TODO - consider empty documents return self.document_lines[self.cursor_position[0]] + def get_column_cell_width(self, row: int, column: int) -> int: + """Given a row and column index within the editor, return the cell offset + of the column from the start of the row (the left edge of the editor content area). + """ + line = self.document_lines[row] + return cell_len(line[:column]) + + def _record_last_intentional_cell_width(self) -> None: + row, column = self.cursor_position + column_cell_length = self.get_column_cell_width(row, column) + log(f"last intentional cell width = {column_cell_length}") + self._last_intentional_cell_width = column_cell_length + # --- Editor operations def insert_text( self, text: str, position: tuple[int, int], move_cursor: bool = True @@ -836,10 +869,6 @@ def _delete_range( start_line = lines[from_row] end_line = lines[to_row] - # TODO - I think this might be slightly off - merging lines will - # result in two newline characters. - # When you delete a line, it records the deleted text with two newlines at the end instead of 1. - # Add the deleted segments from the start and end lines to the deleted text deleted_text = start_line[from_column:] + "\n" for row in range(from_row + 1, to_row): deleted_text += lines[row] + "\n" From 2e1ad5cce4666fe110b62f991175efe473a3f7d9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 19 Jul 2023 11:35:06 +0100 Subject: [PATCH 064/366] Add a todo --- src/textual/widgets/_text_editor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 4c80bfdd66..1952a4ffc0 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -499,6 +499,9 @@ def _on_paste(self, event: events.Paste) -> None: def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: """Given a row index and a cell width, return the column that the cell width corresponds to.""" + + # TODO - this code can be reused in on_click. I think it might actually be slightly + # off, so double check it when writing tests. total_cell_offset = 0 line = self.document_lines[row_index] for column_index, character in enumerate(line): From 06e42a984be9a8ff3bd8e48ce314a28f0437072b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 19 Jul 2023 11:40:59 +0100 Subject: [PATCH 065/366] Reuse cell to column code in on_click, fix an off-by-one --- src/textual/widgets/_text_editor.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 1952a4ffc0..97805b5967 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -476,18 +476,11 @@ def _on_click(self, event: events.Click) -> None: event.stop() target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0) - target_y = clamp(offset.y + int(self.scroll_y), 0, len(self.document_lines) - 1) - - line = self.document_lines[target_y] - cell_offset = 0 - for index, character in enumerate(line): - if cell_offset >= target_x: - self.cursor_position = (target_y, index) - break - cell_offset += get_character_cell_size(character) - else: - self.cursor_position = (target_y, len(line)) - + target_row_index = clamp( + offset.y + int(self.scroll_y), 0, len(self.document_lines) - 1 + ) + target_column = self.cell_width_to_column_index(target_x, target_row_index) + self.cursor_position = (target_row_index, target_column) self._record_last_intentional_cell_width() def _on_paste(self, event: events.Paste) -> None: @@ -505,10 +498,10 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: total_cell_offset = 0 line = self.document_lines[row_index] for column_index, character in enumerate(line): - if total_cell_offset >= cell_width: + total_cell_offset += cell_len(character) + if total_cell_offset >= cell_width + 1: log(f"cell width {cell_width} -> column_index {column_index}") return column_index - total_cell_offset += cell_len(character) return len(line) # --- Reactive watchers and validators From d175448dc9f79defd6dedeb4556a43660a44abeb Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 19 Jul 2023 14:40:35 +0100 Subject: [PATCH 066/366] Make the cursor a selection --- src/textual/widgets/_text_editor.py | 179 +++++++++++------- .../test_input_key_modification_actions.py | 32 ++-- .../input/test_input_key_movement_actions.py | 28 +-- tests/input/test_input_mouse.py | 8 +- 4 files changed, 143 insertions(+), 104 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 97805b5967..77b6f08976 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -59,6 +59,26 @@ class Highlight(NamedTuple): highlight_name: str | None +class Selection(NamedTuple): + """A range of characters within a document from a start point to the end point. + The position of the cursor is always considered to be the `end` point of the selection. + """ + + start: tuple[int, int] = (0, 0) + end: tuple[int, int] = (0, 0) + + @classmethod + def cursor(cls, position: tuple[int, int]) -> "Selection": + """Create a Selection with the same start and end point.""" + return cls(position, position) + + @property + def is_cursor(self) -> bool: + """Return True if the selection has 0 width, i.e. it's just a cursor.""" + start, end = self + return start == end + + @runtime_checkable class Edit(Protocol): """Protocol for actions performed in the text editor that can be done and undone.""" @@ -167,7 +187,7 @@ class TextEditor(ScrollView, can_focus=True): language: Reactive[str | None] = reactive(None) """The language to use for syntax highlighting (via tree-sitter).""" - cursor_position: Reactive[tuple[int, int]] = reactive((0, 0), always_update=True) + selection: Reactive[Selection] = reactive(Selection(), always_update=True) """The cursor position (zero-based line_index, offset).""" show_line_numbers: Reactive[bool] = reactive(True) """True to show line number gutter, otherwise False.""" @@ -324,11 +344,13 @@ def render_line(self, widget_y: int) -> Strip: node_style = HIGHLIGHT_STYLES.get(highlight_name, null_style) line_text.stylize(node_style, start, end) - # Show the cursor - cursor_row, cursor_column = self.cursor_position - if cursor_row == document_y: + # Show the selection + start, end = self.selection + start_row, start_column = start + end_row, end_column = end + if end_row == document_y: cursor_style = self.get_component_rich_style("text-editor--cursor") - line_text.stylize(cursor_style, cursor_column, cursor_column + 1) + line_text.stylize(cursor_style, end_column, end_column + 1) active_line_style = self.get_component_rich_style( "text-editor--active-line" ) @@ -336,7 +358,7 @@ def render_line(self, widget_y: int) -> Strip: # Show the gutter if self.show_line_numbers: - if cursor_row == document_y: + if end_row == document_y: gutter_style = self.get_component_rich_style( "text-editor--active-line-gutter" ) @@ -459,8 +481,8 @@ def _on_key(self, event: events.Key) -> None: insert = event.character event.stop() assert event.character is not None - cursor_position = self.cursor_position - self.insert_text_range(insert, cursor_position, cursor_position) + start, end = self.selection + self.insert_text_range(insert, start, end) event.prevent_default() elif key == "shift+tab": self.dedent_line() @@ -476,17 +498,17 @@ def _on_click(self, event: events.Click) -> None: event.stop() target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0) - target_row_index = clamp( + target_row = clamp( offset.y + int(self.scroll_y), 0, len(self.document_lines) - 1 ) - target_column = self.cell_width_to_column_index(target_x, target_row_index) - self.cursor_position = (target_row_index, target_column) + target_column = self.cell_width_to_column_index(target_x, target_row) + self.selection = Selection.cursor((target_row, target_column)) self._record_last_intentional_cell_width() def _on_paste(self, event: events.Paste) -> None: text = event.text if text: - self.insert_text(text, self.cursor_position) + self.insert_text(text, self.selection) event.stop() def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: @@ -511,9 +533,7 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: # clamped_column = clamp(new_column, 0, len(self.document_lines[clamped_row]) - 1) # return clamped_row, clamped_column - def watch_cursor_position( - self, old_position: tuple[int, int], new_position: tuple[int, int] - ) -> None: + def watch_selection(self) -> None: self.scroll_cursor_visible() def watch_virtual_size(self, vs): @@ -521,7 +541,9 @@ def watch_virtual_size(self, vs): # --- Cursor utilities def scroll_cursor_visible(self): - row, column = self.cursor_position + # The end of the selection is always considered to be position of the cursor + # ... this is a constraint we need to enforce in code. + row, column = self.selection.end text = self.active_line_text[:column] column_offset = cell_len(text) self.scroll_to_region( @@ -533,19 +555,19 @@ def scroll_cursor_visible(self): @property def cursor_at_first_row(self) -> bool: - return self.cursor_position[0] == 0 + return self.selection.end[0] == 0 @property def cursor_at_last_row(self) -> bool: - return self.cursor_position[0] == len(self.document_lines) - 1 + return self.selection.end[0] == len(self.document_lines) - 1 @property def cursor_at_start_of_row(self) -> bool: - return self.cursor_position[1] == 0 + return self.selection.end[1] == 0 @property def cursor_at_end_of_row(self) -> bool: - cursor_row, cursor_column = self.cursor_position + cursor_row, cursor_column = self.selection.end row_length = len(self.document_lines[cursor_row]) cursor_at_end = cursor_column == row_length return cursor_at_end @@ -560,14 +582,14 @@ def cursor_at_end_of_document(self) -> bool: return self.cursor_at_last_row and self.cursor_at_end_of_row def cursor_to_line_end(self) -> None: - cursor_row, cursor_column = self.cursor_position + cursor_row, cursor_column = self.selection.end target_column = len(self.document_lines[cursor_row]) - self.cursor_position = (cursor_row, target_column) + self.selection = Selection.cursor((cursor_row, target_column)) self._record_last_intentional_cell_width() def cursor_to_line_start(self) -> None: - cursor_row, cursor_column = self.cursor_position - self.cursor_position = (cursor_row, 0) + cursor_row, cursor_column = self.selection.end + self.selection = Selection.cursor((cursor_row, 0)) # ------ Cursor movement actions def action_cursor_left(self) -> None: @@ -579,13 +601,13 @@ def action_cursor_left(self) -> None: if self.cursor_at_start_of_document: return - cursor_row, cursor_column = self.cursor_position + cursor_row, cursor_column = self.selection.end length_of_row_above = len(self.document_lines[cursor_row - 1]) target_row = cursor_row if cursor_column != 0 else cursor_row - 1 target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above - self.cursor_position = (target_row, target_column) + self.selection = Selection.cursor((target_row, target_column)) self._record_last_intentional_cell_width() def action_cursor_right(self) -> None: @@ -596,12 +618,12 @@ def action_cursor_right(self) -> None: if self.cursor_at_end_of_document: return - cursor_row, cursor_column = self.cursor_position + cursor_row, cursor_column = self.selection.end target_row = cursor_row + 1 if self.cursor_at_end_of_row else cursor_row target_column = 0 if self.cursor_at_end_of_row else cursor_column + 1 - self.cursor_position = (target_row, target_column) + self.selection = Selection.cursor((target_row, target_column)) self._record_last_intentional_cell_width() def action_cursor_down(self) -> None: @@ -609,7 +631,7 @@ def action_cursor_down(self) -> None: if self.cursor_at_last_row: self.cursor_to_line_end() - cursor_row, cursor_column = self.cursor_position + cursor_row, cursor_column = self.selection.end target_row = min(len(self.document_lines) - 1, cursor_row + 1) @@ -619,14 +641,14 @@ def action_cursor_down(self) -> None: ) target_column = clamp(target_column, 0, len(self.document_lines[target_row])) - self.cursor_position = (target_row, target_column) + self.selection = Selection.cursor((target_row, target_column)) def action_cursor_up(self) -> None: """Move the cursor up one cell.""" if self.cursor_at_first_row: self.cursor_to_line_start() - cursor_row, cursor_column = self.cursor_position + cursor_row, cursor_column = self.selection.end target_row = max(0, cursor_row - 1) @@ -636,7 +658,7 @@ def action_cursor_up(self) -> None: ) target_column = clamp(target_column, 0, len(self.document_lines[target_row])) - self.cursor_position = (target_row, target_column) + self.selection = Selection.cursor((target_row, target_column)) def action_cursor_line_end(self) -> None: self.cursor_to_line_end() @@ -650,7 +672,7 @@ def action_cursor_left_word(self) -> None: if self.cursor_at_start_of_document: return - cursor_row, cursor_column = self.cursor_position + cursor_row, cursor_column = self.selection.end # Check the current line for a word boundary line = self.document_lines[cursor_row][:cursor_column] @@ -667,7 +689,7 @@ def action_cursor_left_word(self) -> None: # If we're already on the first line and no word boundary is found, move to the start of the line cursor_column = 0 - self.cursor_position = (cursor_row, cursor_column) + self.selection = Selection.cursor((cursor_row, cursor_column)) self._record_last_intentional_cell_width() def action_cursor_right_word(self) -> None: @@ -676,7 +698,7 @@ def action_cursor_right_word(self) -> None: if self.cursor_at_end_of_document: return - cursor_row, cursor_column = self.cursor_position + cursor_row, cursor_column = self.selection.end # Check the current line for a word boundary line = self.document_lines[cursor_row][cursor_column:] @@ -693,13 +715,13 @@ def action_cursor_right_word(self) -> None: # If we're already on the last line and no word boundary is found, move to the end of the line cursor_column = len(self.document_lines[cursor_row]) - self.cursor_position = (cursor_row, cursor_column) + self.selection = Selection.cursor((cursor_row, cursor_column)) self._record_last_intentional_cell_width() @property def active_line_text(self) -> str: # TODO - consider empty documents - return self.document_lines[self.cursor_position[0]] + return self.document_lines[self.selection.end[0]] def get_column_cell_width(self, row: int, column: int) -> int: """Given a row and column index within the editor, return the cell offset @@ -709,7 +731,7 @@ def get_column_cell_width(self, row: int, column: int) -> int: return cell_len(line[:column]) def _record_last_intentional_cell_width(self) -> None: - row, column = self.cursor_position + row, column = self.selection.end column_cell_length = self.get_column_cell_width(row, column) log(f"last intentional cell width = {column_cell_length}") self._last_intentional_cell_width = column_cell_length @@ -784,7 +806,7 @@ def _insert_text_range( self._prepare_highlights() self._refresh_size() if move_cursor: - self.cursor_position = cursor_destination + self.selection = Selection.cursor(cursor_destination) def _position_to_byte_offset(self, position: tuple[int, int]) -> int: """Given a document coordinate, return the byte offset of that coordinate.""" @@ -803,7 +825,7 @@ def dedent_line(self) -> None: A dedent is simply a Delete operation on some amount of whitespace which may exist at the start of a line. """ - cursor_row, cursor_column = self.cursor_position + cursor_row, cursor_column = self.selection.end # Define one level of indentation as four spaces indent_level = " " * 4 @@ -816,7 +838,7 @@ def dedent_line(self) -> None: self.document_lines[cursor_row] = current_line[len(indent_level) :] if cursor_column > len(current_line): - self.cursor_position = (cursor_row, len(current_line)) + self.selection = Selection.cursor((cursor_row, len(current_line))) self._refresh_size() self.refresh() @@ -896,10 +918,10 @@ def _delete_range( self._refresh_size() if cursor_destination is not None: - self.cursor_position = cursor_destination + self.selection = Selection.cursor(cursor_destination) else: # Move the cursor to the start of the deleted range - self.cursor_position = (from_row, from_column) + self.selection = Selection.cursor((from_row, from_column)) return deleted_text @@ -908,48 +930,55 @@ def action_delete_left(self) -> None: if self.cursor_at_start_of_document: return - cursor_row, cursor_column = self.cursor_position lines = self.document_lines - from_position = self.cursor_position + + start, end = self.selection + end_row, end_column = end + if self.cursor_at_start_of_row: - to_position = (cursor_row - 1, len(lines[cursor_row - 1])) + to_position = (end_row - 1, len(lines[end_row - 1])) else: - to_position = (cursor_row, cursor_column - 1) + to_position = (end_row, end_column - 1) - self.edit(Delete(from_position, to_position)) + self.edit(Delete(start, to_position)) def action_delete_right(self) -> None: """Deletes the character to the right of the cursor and keeps the cursor at the same position.""" if self.cursor_at_end_of_document: return - cursor_row, cursor_column = self.cursor_position - from_position = self.cursor_position + start, end = self.selection + end_row, end_column = end + if self.cursor_at_end_of_row: - to_position = (cursor_row + 1, 0) + to_position = (end_row + 1, 0) else: - to_position = (cursor_row, cursor_column + 1) + to_position = (end_row, end_column + 1) - self.edit(Delete(from_position, to_position)) + self.edit(Delete(start, to_position)) def action_delete_line(self) -> None: - """Deletes the line the cursor is on.""" - cursor_row, _ = self.cursor_position - from_position = (cursor_row, 0) - to_position = (cursor_row + 1, 0) + """Deletes the lines which intersect with the selection.""" + start, end = self.selection + start_row, start_column = start + end_row, end_column = end + + from_position = (start_row, 0) + to_position = (end_row + 1, 0) + self.edit(Delete(from_position, to_position)) def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor position to the start of the line.""" - cursor_row, cursor_column = self.cursor_position - from_position = self.cursor_position + from_position = self.selection.end + cursor_row, cursor_column = from_position to_position = (cursor_row, 0) self.edit(Delete(from_position, to_position)) def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor position to the end of the line.""" - cursor_row, cursor_column = self.cursor_position - from_position = self.cursor_position + from_position = self.selection.end + cursor_row, cursor_column = from_position to_position = (cursor_row, len(self.document_lines[cursor_row])) self.edit(Delete(from_position, to_position)) @@ -958,7 +987,13 @@ def action_delete_word_left(self) -> None: if self.cursor_at_start_of_document: return - cursor_row, cursor_column = self.cursor_position + # If there's a non-zero selection, then "delete word left" typically only + # deletes the characters within the selection range, ignoring word boundaries. + start, end = self.selection + if start != end: + self.edit(Delete(start, end)) + + cursor_row, cursor_column = end # Check the current line for a word boundary line = self.document_lines[cursor_row][:cursor_column] @@ -968,20 +1003,24 @@ def action_delete_word_left(self) -> None: # If a word boundary is found, delete the word from_position = (cursor_row, matches[-1].start()) elif cursor_row > 0: - # If no word boundary is found and we're not on the first line, delete to the end of the previous line + # If no word boundary is found, and we're not on the first line, delete to the end of the previous line from_position = (cursor_row - 1, len(self.document_lines[cursor_row - 1])) else: # If we're already on the first line and no word boundary is found, delete to the start of the line from_position = (cursor_row, 0) - self.edit(Delete(from_position, self.cursor_position)) + self.edit(Delete(from_position, self.selection.end)) def action_delete_word_right(self) -> None: """Deletes the word to the right of the cursor and keeps the cursor at the same position.""" if self.cursor_at_end_of_document: return - cursor_row, cursor_column = self.cursor_position + start, end = self.selection + if start != end: + self.edit(Delete(start, end)) + + cursor_row, cursor_column = end # Check the current line for a word boundary line = self.document_lines[cursor_row][cursor_column:] @@ -991,13 +1030,13 @@ def action_delete_word_right(self) -> None: # If a word boundary is found, delete the word to_position = (cursor_row, cursor_column + matches[0].end()) elif cursor_row < len(self.document_lines) - 1: - # If no word boundary is found and we're not on the last line, delete to the start of the next line + # If no word boundary is found, and we're not on the last line, delete to the start of the next line to_position = (cursor_row + 1, 0) else: # If we're already on the last line and no word boundary is found, delete to the end of the line to_position = (cursor_row, len(self.document_lines[cursor_row])) - self.edit(Delete(self.cursor_position, to_position)) + self.edit(Delete(end, to_position)) # --- Debugging @dataclass @@ -1018,7 +1057,7 @@ class EditorDebug: def debug_state(self) -> "EditorDebug": return self.EditorDebug( - cursor=self.cursor_position, + cursor=self.selection, language=self.language, document_size=self._document_size, virtual_size=self.virtual_size, @@ -1033,9 +1072,9 @@ def debug_state(self) -> "EditorDebug": len(highlights) for key, highlights in self._highlights.items() ), highlight_cache_current_row_size=len( - self._highlights[self.cursor_position[0]] + self._highlights[self.selection.end[0]] ), - highlight_cache_current_row=self._highlights[self.cursor_position[0]], + highlight_cache_current_row=self._highlights[self.selection.end[0]], ) diff --git a/tests/input/test_input_key_modification_actions.py b/tests/input/test_input_key_modification_actions.py index bfd935fd9d..25ddb3f810 100644 --- a/tests/input/test_input_key_modification_actions.py +++ b/tests/input/test_input_key_modification_actions.py @@ -26,7 +26,7 @@ async def test_delete_left_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_delete_left() - assert input.cursor_position == 0 + assert input.selection == 0 assert input.value == TEST_INPUTS[input.id] @@ -36,7 +36,7 @@ async def test_delete_left_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_left() - assert input.cursor_position == len(input.value) + assert input.selection == len(input.value) assert input.value == TEST_INPUTS[input.id][:-1] @@ -45,16 +45,16 @@ async def test_delete_left_word_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_delete_left_word() - assert input.cursor_position == 0 + assert input.selection == 0 assert input.value == TEST_INPUTS[input.id] async def test_delete_left_word_from_inside_first_word() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): - input.cursor_position = 1 + input.selection = 1 input.action_delete_left_word() - assert input.cursor_position == 0 + assert input.selection == 0 assert input.value == TEST_INPUTS[input.id][1:] @@ -70,7 +70,7 @@ async def test_delete_left_word_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_left_word() - assert input.cursor_position == len(input.value) + assert input.selection == len(input.value) assert input.value == expected[input.id] @@ -81,7 +81,7 @@ async def test_password_delete_left_word_from_end() -> None: input.action_end() input.password = True input.action_delete_left_word() - assert input.cursor_position == 0 + assert input.selection == 0 assert input.value == "" @@ -90,7 +90,7 @@ async def test_delete_left_all_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_delete_left_all() - assert input.cursor_position == 0 + assert input.selection == 0 assert input.value == TEST_INPUTS[input.id] @@ -100,7 +100,7 @@ async def test_delete_left_all_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_left_all() - assert input.cursor_position == 0 + assert input.selection == 0 assert input.value == "" @@ -109,7 +109,7 @@ async def test_delete_right_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_delete_right() - assert input.cursor_position == 0 + assert input.selection == 0 assert input.value == TEST_INPUTS[input.id][1:] @@ -119,7 +119,7 @@ async def test_delete_right_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_right() - assert input.cursor_position == len(input.value) + assert input.selection == len(input.value) assert input.value == TEST_INPUTS[input.id] @@ -134,7 +134,7 @@ async def test_delete_right_word_from_home() -> None: } for input in pilot.app.query(Input): input.action_delete_right_word() - assert input.cursor_position == 0 + assert input.selection == 0 assert input.value == expected[input.id] @@ -144,7 +144,7 @@ async def test_password_delete_right_word_from_home() -> None: for input in pilot.app.query(Input): input.password = True input.action_delete_right_word() - assert input.cursor_position == 0 + assert input.selection == 0 assert input.value == "" @@ -154,7 +154,7 @@ async def test_delete_right_word_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_right_word() - assert input.cursor_position == len(input.value) + assert input.selection == len(input.value) assert input.value == TEST_INPUTS[input.id] @@ -163,7 +163,7 @@ async def test_delete_right_all_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_delete_right_all() - assert input.cursor_position == 0 + assert input.selection == 0 assert input.value == "" @@ -173,5 +173,5 @@ async def test_delete_right_all_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_right_all() - assert input.cursor_position == len(input.value) + assert input.selection == len(input.value) assert input.value == TEST_INPUTS[input.id] diff --git a/tests/input/test_input_key_movement_actions.py b/tests/input/test_input_key_movement_actions.py index a6cf136947..60bf1f11b8 100644 --- a/tests/input/test_input_key_movement_actions.py +++ b/tests/input/test_input_key_movement_actions.py @@ -28,7 +28,7 @@ async def test_input_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_home() - assert input.cursor_position == 0 + assert input.selection == 0 async def test_input_end() -> None: @@ -36,7 +36,7 @@ async def test_input_end() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_end() - assert input.cursor_position == len(input.value) + assert input.selection == len(input.value) async def test_input_right_from_home() -> None: @@ -44,7 +44,7 @@ async def test_input_right_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_cursor_right() - assert input.cursor_position == (1 if input.value else 0) + assert input.selection == (1 if input.value else 0) async def test_input_right_from_end() -> None: @@ -53,7 +53,7 @@ async def test_input_right_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_cursor_right() - assert input.cursor_position == len(input.value) + assert input.selection == len(input.value) async def test_input_left_from_home() -> None: @@ -61,7 +61,7 @@ async def test_input_left_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_cursor_left() - assert input.cursor_position == 0 + assert input.selection == 0 async def test_input_left_from_end() -> None: @@ -70,7 +70,7 @@ async def test_input_left_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_cursor_left() - assert input.cursor_position == (len(input.value) - 1 if input.value else 0) + assert input.selection == (len(input.value) - 1 if input.value else 0) async def test_input_left_word_from_home() -> None: @@ -78,7 +78,7 @@ async def test_input_left_word_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_cursor_left_word() - assert input.cursor_position == 0 + assert input.selection == 0 async def test_input_left_word_from_end() -> None: @@ -94,7 +94,7 @@ async def test_input_left_word_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_cursor_left_word() - assert input.cursor_position == expected_at[input.id] + assert input.selection == expected_at[input.id] async def test_password_input_left_word_from_end() -> None: @@ -104,7 +104,7 @@ async def test_password_input_left_word_from_end() -> None: input.action_end() input.password = True input.action_cursor_left_word() - assert input.cursor_position == 0 + assert input.selection == 0 async def test_input_right_word_from_home() -> None: @@ -119,7 +119,7 @@ async def test_input_right_word_from_home() -> None: } for input in pilot.app.query(Input): input.action_cursor_right_word() - assert input.cursor_position == expected_at[input.id] + assert input.selection == expected_at[input.id] async def test_password_input_right_word_from_home() -> None: @@ -128,7 +128,7 @@ async def test_password_input_right_word_from_home() -> None: for input in pilot.app.query(Input): input.password = True input.action_cursor_right_word() - assert input.cursor_position == len(input.value) + assert input.selection == len(input.value) async def test_input_right_word_from_end() -> None: @@ -137,7 +137,7 @@ async def test_input_right_word_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_cursor_right_word() - assert input.cursor_position == len(input.value) + assert input.selection == len(input.value) async def test_input_right_word_to_the_end() -> None: @@ -152,7 +152,7 @@ async def test_input_right_word_to_the_end() -> None: } for input in pilot.app.query(Input): hops = 0 - while input.cursor_position < len(input.value): + while input.selection < len(input.value): input.action_cursor_right_word() hops += 1 assert hops == expected_hops[input.id] @@ -171,7 +171,7 @@ async def test_input_left_word_from_the_end() -> None: for input in pilot.app.query(Input): input.action_end() hops = 0 - while input.cursor_position: + while input.selection: input.action_cursor_left_word() hops += 1 assert hops == expected_hops[input.id] diff --git a/tests/input/test_input_mouse.py b/tests/input/test_input_mouse.py index e4bfbb51d6..fda0b9d837 100644 --- a/tests/input/test_input_mouse.py +++ b/tests/input/test_input_mouse.py @@ -32,14 +32,14 @@ async def test_mouse_clicks_within(click_at, should_land): # Input. await pilot.click(Input, Offset(click_at + 3, 1)) await pilot.pause() - assert pilot.app.query_one(Input).cursor_position == should_land + assert pilot.app.query_one(Input).selection == should_land async def test_mouse_click_outwith(): """Mouse clicks outside the input should not affect cursor position.""" async with InputApp().run_test() as pilot: - pilot.app.query_one(Input).cursor_position = 3 - assert pilot.app.query_one(Input).cursor_position == 3 + pilot.app.query_one(Input).selection = 3 + assert pilot.app.query_one(Input).selection == 3 await pilot.click(Input, Offset(0, 0)) await pilot.pause() - assert pilot.app.query_one(Input).cursor_position == 3 + assert pilot.app.query_one(Input).selection == 3 From f3a7fe1159317cc6d4b7b7de50df77512aad0c61 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 20 Jul 2023 10:03:55 +0100 Subject: [PATCH 067/366] Text selection with the mouse --- src/textual/widgets/_text_editor.py | 78 ++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 77b6f08976..4130d8c29b 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -151,6 +151,10 @@ class TextEditor(ScrollView, can_focus=True): color: $text; background: white 80%; } + +TextEditor > .text-editor--selection { + background: $primary 90%; +} """ COMPONENT_CLASSES: ClassVar[set[str]] = { @@ -158,6 +162,7 @@ class TextEditor(ScrollView, can_focus=True): "text-editor--active-line-gutter", "text-editor--gutter", "text-editor--cursor", + "text-editor--selection", } BINDINGS = [ @@ -225,6 +230,9 @@ def __init__( self._undo_stack: list[Edit] = [] """A stack (the end of the list is the top of the stack) for tracking edits.""" + self._selecting = False + """True if we're currently selecting text, otherwise False.""" + # --- Abstract syntax tree and related parsing machinery self._language: Language | None = None self._parser: Parser | None = None @@ -344,10 +352,47 @@ def render_line(self, widget_y: int) -> Strip: node_style = HIGHLIGHT_STYLES.get(highlight_name, null_style) line_text.stylize(node_style, start, end) - # Show the selection start, end = self.selection - start_row, start_column = start end_row, end_column = end + + # The selection has width, so we should highlight it + start_row, start_column = start + + print(start_row, document_y, end_row) + + selection_style = self.get_component_rich_style("text-editor--selection") + + selection_top = min(start, end) + selection_bottom = max(start, end) + + selection_top_row, selection_top_column = selection_top + selection_bottom_row, selection_bottom_column = selection_bottom + + if start != end and selection_top_row <= document_y <= selection_bottom_row: + # If this row is part of the selection + if document_y == selection_top_row == selection_bottom_row: + # Selection within a single line + line_text.stylize_before( + selection_style, + start=selection_top_column, + end=selection_bottom_column, + ) + else: + # Selection spanning multiple lines + if document_y == selection_top_row: + line_text.stylize_before( + selection_style, + start=selection_top_column, + end=len(line_string), + ) + elif document_y == end_row: + line_text.stylize_before( + selection_style, end=selection_bottom_column + ) + else: + line_text.stylize_before(selection_style, end=len(line_string)) + + # Show the cursor and the selection if end_row == document_y: cursor_style = self.get_component_rich_style("text-editor--cursor") line_text.stylize(cursor_style, end_column, end_column + 1) @@ -488,22 +533,39 @@ def _on_key(self, event: events.Key) -> None: self.dedent_line() event.stop() - def _on_click(self, event: events.Click) -> None: - """Clicking the content body moves the cursor.""" - - offset = event.get_content_offset(self) + def get_target_document_location(self, offset: Offset) -> tuple[int, int]: if offset is None: return - event.stop() - target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0) target_row = clamp( offset.y + int(self.scroll_y), 0, len(self.document_lines) - 1 ) target_column = self.cell_width_to_column_index(target_x, target_row) + + return target_row, target_column + + def _on_mouse_down(self, event: events.MouseDown) -> None: + event.stop() + offset = event.get_content_offset(self) + target_row, target_column = self.get_target_document_location(offset) self.selection = Selection.cursor((target_row, target_column)) + log.debug(f"started selection {self.selection!r}") + self._selecting = True + + def _on_mouse_move(self, event: events.MouseMove) -> None: + event.stop() + if self._selecting: + offset = event.get_content_offset(self) + target = self.get_target_document_location(offset) + selection_start, _ = self.selection + self.selection = Selection(selection_start, target) + log.debug(f"selection updated {self.selection!r}") + + def _on_mouse_up(self, event: events.MouseUp) -> None: + event.stop() self._record_last_intentional_cell_width() + self._selecting = False def _on_paste(self, event: events.Paste) -> None: text = event.text From 82dd2f7f84293076123a9a99b0bc863d33ca70e8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 20 Jul 2023 13:19:14 +0100 Subject: [PATCH 068/366] Move cursor left and select --- src/textual/widgets/_text_editor.py | 131 +++++++++++++++++----------- 1 file changed, 78 insertions(+), 53 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 4130d8c29b..07223eb976 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -153,7 +153,7 @@ class TextEditor(ScrollView, can_focus=True): } TextEditor > .text-editor--selection { - background: $primary 90%; + background: $primary; } """ @@ -170,6 +170,7 @@ class TextEditor(ScrollView, can_focus=True): Binding("up", "cursor_up", "cursor up", show=False), Binding("down", "cursor_down", "cursor down", show=False), Binding("left", "cursor_left", "cursor left", show=False), + Binding("shift+left", "cursor_left_select", "cursor left", show=False), Binding("ctrl+left", "cursor_left_word", "cursor left word", show=False), Binding("right", "cursor_right", "cursor right", show=False), Binding("ctrl+right", "cursor_right_word", "cursor right word", show=False), @@ -246,6 +247,7 @@ def watch_language(self, new_language: str | None) -> None: When the language reactive string is updated, fetch the Language definition from our tree-sitter library file. If the language reactive is set to None, then the no parser is used.""" + log.debug(f"updating editor language to {new_language!r}") if new_language: self._language = Language(LANGUAGES_PATH.resolve(), new_language) parser = Parser() @@ -253,8 +255,6 @@ def watch_language(self, new_language: str | None) -> None: self._parser.set_language(self._language) self._syntax_tree = self._build_ast(parser) self._highlights_query = Path(HIGHLIGHTS_PATH.resolve()).read_text() - else: - self._syntax_tree = None log.debug(f"parser set to {self._parser}") @@ -276,7 +276,7 @@ def _build_ast( else: return None - def _read_callable(self, byte_offset, point): + def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> str: row, column = point lines = self.document_lines row_out_of_bounds = row >= len(lines) @@ -302,11 +302,7 @@ def load_lines(self, lines: list[str]) -> None: This will replace any previously loaded lines.""" self.document_lines = lines - - # TODO Offer maximum line width and wrap if needed self._document_size = self._get_document_size(lines) - - # TODO - clear caches if self._parser is not None: self._syntax_tree = self._build_ast(self._parser) self._prepare_highlights() @@ -355,16 +351,13 @@ def render_line(self, widget_y: int) -> Strip: start, end = self.selection end_row, end_column = end - # The selection has width, so we should highlight it - start_row, start_column = start - - print(start_row, document_y, end_row) - selection_style = self.get_component_rich_style("text-editor--selection") + # Start and end can be before or after each other, depending on the direction + # you move the cursor during selecting text, but the "top" of the selection + # is always before the "bottom" of the selection. selection_top = min(start, end) selection_bottom = max(start, end) - selection_top_row, selection_top_column = selection_top selection_bottom_row, selection_bottom_column = selection_bottom @@ -589,11 +582,13 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: return len(line) # --- Reactive watchers and validators - # def validate_cursor_position(self, new_position: tuple[int, int]) -> tuple[int, int]: - # new_row, new_column = new_position - # clamped_row = clamp(new_row, 0, len(self.document_lines) - 1) - # clamped_column = clamp(new_column, 0, len(self.document_lines[clamped_row]) - 1) - # return clamped_row, clamped_column + def validate_cursor_position( + self, new_position: tuple[int, int] + ) -> tuple[int, int]: + new_row, new_column = new_position + clamped_row = clamp(new_row, 0, len(self.document_lines) - 1) + clamped_column = clamp(new_column, 0, len(self.document_lines[clamped_row])) + return clamped_row, clamped_column def watch_selection(self) -> None: self.scroll_cursor_visible() @@ -662,15 +657,42 @@ def action_cursor_left(self) -> None: """ if self.cursor_at_start_of_document: return + target_row, target_column = self.get_cursor_left_position() + self.selection = Selection.cursor((target_row, target_column)) + self._record_last_intentional_cell_width() + + def action_cursor_left_select(self): + """Move the end of the selection one position to the left. + + This will expand or contract the selection. + """ + if self.cursor_at_start_of_document: + return + new_cursor_position = self.get_cursor_left_position() + selection_start, selection_end = self.selection + + print( + f"selection start = {selection_start}, new_cursor_position = {new_cursor_position}" + ) + + self.selection = Selection(selection_start, new_cursor_position) + self._record_last_intentional_cell_width() + def get_cursor_left_position(self): + """Get the position the cursor will move to if it moves left.""" cursor_row, cursor_column = self.selection.end length_of_row_above = len(self.document_lines[cursor_row - 1]) - target_row = cursor_row if cursor_column != 0 else cursor_row - 1 target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above + return target_row, target_column - self.selection = Selection.cursor((target_row, target_column)) - self._record_last_intentional_cell_width() + def _fix_direction( + self, start: tuple[int, int], end: tuple[int, int] + ) -> tuple[tuple[int, int], tuple[int, int]]: + """Given a range, return a new range (x, y) such that x <= y which covers the same characters.""" + if start > end: + return end, start + return start, end def action_cursor_right(self) -> None: """Move the cursor one position to the right. @@ -783,6 +805,9 @@ def action_cursor_right_word(self) -> None: @property def active_line_text(self) -> str: # TODO - consider empty documents + log.error(self.selection) + log.error(self.selection.end) + log.error(self.selection.end[0]) return self.document_lines[self.selection.end[0]] def get_column_cell_width(self, row: int, column: int) -> int: @@ -1161,34 +1186,34 @@ def traverse_tree(cursor): retracing = False -if __name__ == "__main__": - language = Language(LANGUAGES_PATH.resolve(), "python") - parser = Parser() - parser.set_language(language) - - CODE = """\ - from textual.app import App - - - class ScreenApp(App): - def on_mount(self) -> None: - self.screen.styles.background = "darkblue" - self.screen.styles.border = ("heavy", "white") - - - if __name__ == "__main__": - app = ScreenApp() - app.run() - """ - - document_lines = CODE.splitlines(keepends=False) - - def read_callable(byte_offset, point): - row, column = point - if row >= len(document_lines) or column >= len(document_lines[row]): - return None - return document_lines[row][column:].encode("utf8") - - tree = parser.parse(bytes(CODE, "utf-8")) - - print(list(traverse_tree(tree.walk()))) +# if __name__ == "__main__": +# language = Language(LANGUAGES_PATH.resolve(), "python") +# parser = Parser() +# parser.set_language(language) +# +# CODE = """\ +# from textual.app import App +# +# +# class ScreenApp(App): +# def on_mount(self) -> None: +# self.screen.styles.background = "darkblue" +# self.screen.styles.border = ("heavy", "white") +# +# +# if __name__ == "__main__": +# app = ScreenApp() +# app.run() +# """ +# +# document_lines = CODE.splitlines(keepends=False) +# +# def read_callable(byte_offset, point): +# row, column = point +# if row >= len(document_lines) or column >= len(document_lines[row]): +# return None +# return document_lines[row][column:].encode("utf8") +# +# tree = parser.parse(bytes(CODE, "utf-8")) +# +# print(list(traverse_tree(tree.walk()))) From b9e25eae955e651d87db939ca7d22bc50cd858d3 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 20 Jul 2023 13:30:21 +0100 Subject: [PATCH 069/366] Cursor selection right direction --- src/textual/widgets/_text_editor.py | 51 ++++++++++++++++++----------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 07223eb976..b25ec3a0eb 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -170,9 +170,12 @@ class TextEditor(ScrollView, can_focus=True): Binding("up", "cursor_up", "cursor up", show=False), Binding("down", "cursor_down", "cursor down", show=False), Binding("left", "cursor_left", "cursor left", show=False), - Binding("shift+left", "cursor_left_select", "cursor left", show=False), + Binding("shift+left", "cursor_left_select", "cursor left select", show=False), Binding("ctrl+left", "cursor_left_word", "cursor left word", show=False), Binding("right", "cursor_right", "cursor right", show=False), + Binding( + "shift+right", "cursor_right_select", "cursor right select", show=False + ), Binding("ctrl+right", "cursor_right_word", "cursor right word", show=False), Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False), Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False), @@ -378,7 +381,7 @@ def render_line(self, widget_y: int) -> Strip: start=selection_top_column, end=len(line_string), ) - elif document_y == end_row: + elif document_y == selection_bottom_row: line_text.stylize_before( selection_style, end=selection_bottom_column ) @@ -538,6 +541,14 @@ def get_target_document_location(self, offset: Offset) -> tuple[int, int]: return target_row, target_column + def _fix_direction( + self, start: tuple[int, int], end: tuple[int, int] + ) -> tuple[tuple[int, int], tuple[int, int]]: + """Given a range, return a new range (x, y) such that x <= y which covers the same characters.""" + if start > end: + return end, start + return start, end + def _on_mouse_down(self, event: events.MouseDown) -> None: event.stop() offset = event.get_content_offset(self) @@ -670,11 +681,6 @@ def action_cursor_left_select(self): return new_cursor_position = self.get_cursor_left_position() selection_start, selection_end = self.selection - - print( - f"selection start = {selection_start}, new_cursor_position = {new_cursor_position}" - ) - self.selection = Selection(selection_start, new_cursor_position) self._record_last_intentional_cell_width() @@ -686,14 +692,6 @@ def get_cursor_left_position(self): target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above return target_row, target_column - def _fix_direction( - self, start: tuple[int, int], end: tuple[int, int] - ) -> tuple[tuple[int, int], tuple[int, int]]: - """Given a range, return a new range (x, y) such that x <= y which covers the same characters.""" - if start > end: - return end, start - return start, end - def action_cursor_right(self) -> None: """Move the cursor one position to the right. @@ -702,14 +700,29 @@ def action_cursor_right(self) -> None: if self.cursor_at_end_of_document: return - cursor_row, cursor_column = self.selection.end + target_row, target_column = self.get_cursor_right_position() + self.selection = Selection.cursor((target_row, target_column)) + self._record_last_intentional_cell_width() - target_row = cursor_row + 1 if self.cursor_at_end_of_row else cursor_row - target_column = 0 if self.cursor_at_end_of_row else cursor_column + 1 + def action_cursor_right_select(self): + """Move the end of the selection one position to the right. - self.selection = Selection.cursor((target_row, target_column)) + This will expand or contract the selection. + """ + if self.cursor_at_start_of_document: + return + new_cursor_position = self.get_cursor_right_position() + selection_start, selection_end = self.selection + self.selection = Selection(selection_start, new_cursor_position) self._record_last_intentional_cell_width() + def get_cursor_right_position(self) -> tuple[int, int]: + """Get the position the cursor will move to if it moves right.""" + cursor_row, cursor_column = self.selection.end + target_row = cursor_row + 1 if self.cursor_at_end_of_row else cursor_row + target_column = 0 if self.cursor_at_end_of_row else cursor_column + 1 + return target_row, target_column + def action_cursor_down(self) -> None: """Move the cursor down one cell.""" if self.cursor_at_last_row: From 85aea51d278bdd6020e1901b4a0cee08ab08fe8a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 20 Jul 2023 14:22:30 +0100 Subject: [PATCH 070/366] Keyboard support for selection up and down --- src/textual/widgets/_text_editor.py | 96 ++++++++++++++++++----------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index b25ec3a0eb..b0af235a9d 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -168,7 +168,9 @@ class TextEditor(ScrollView, can_focus=True): BINDINGS = [ # Cursor movement Binding("up", "cursor_up", "cursor up", show=False), + Binding("shift+up", "cursor_up_select", "cursor up select", show=False), Binding("down", "cursor_down", "cursor down", show=False), + Binding("shift+down", "cursor_down_select", "cursor down select", show=False), Binding("left", "cursor_left", "cursor left", show=False), Binding("shift+left", "cursor_left_select", "cursor left select", show=False), Binding("ctrl+left", "cursor_left_word", "cursor left word", show=False), @@ -578,11 +580,7 @@ def _on_paste(self, event: events.Paste) -> None: event.stop() def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: - """Given a row index and a cell width, return the column that the cell width - corresponds to.""" - - # TODO - this code can be reused in on_click. I think it might actually be slightly - # off, so double check it when writing tests. + """Return the column that the cell width corresponds to on the given row.""" total_cell_offset = 0 line = self.document_lines[row_index] for column_index, character in enumerate(line): @@ -592,21 +590,9 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: return column_index return len(line) - # --- Reactive watchers and validators - def validate_cursor_position( - self, new_position: tuple[int, int] - ) -> tuple[int, int]: - new_row, new_column = new_position - clamped_row = clamp(new_row, 0, len(self.document_lines) - 1) - clamped_column = clamp(new_column, 0, len(self.document_lines[clamped_row])) - return clamped_row, clamped_column - def watch_selection(self) -> None: self.scroll_cursor_visible() - def watch_virtual_size(self, vs): - log.debug(f"new virtual_size = {vs!r}") - # --- Cursor utilities def scroll_cursor_visible(self): # The end of the selection is always considered to be position of the cursor @@ -649,15 +635,37 @@ def cursor_at_end_of_document(self) -> bool: """True if the cursor is at the very end of the document.""" return self.cursor_at_last_row and self.cursor_at_end_of_row - def cursor_to_line_end(self) -> None: - cursor_row, cursor_column = self.selection.end + def cursor_to_line_end(self, select: bool = False) -> None: + """Move the cursor to the end of the line. + + Args: + select: Select the text between the old and new cursor locations. + """ + + start, end = self.selection + cursor_row, cursor_column = end target_column = len(self.document_lines[cursor_row]) - self.selection = Selection.cursor((cursor_row, target_column)) + + if select: + self.selection = Selection(start, target_column) + else: + self.selection = Selection.cursor((cursor_row, target_column)) + self._record_last_intentional_cell_width() - def cursor_to_line_start(self) -> None: - cursor_row, cursor_column = self.selection.end - self.selection = Selection.cursor((cursor_row, 0)) + def cursor_to_line_start(self, select: bool = False) -> None: + """Move the cursor to the start of the line. + + Args: + select: Select the text between the old and new cursor locations. + """ + start, end = self.selection + cursor_row, cursor_column = end + if select: + self.selection = Selection(start, (cursor_row, 0)) + else: + self.selection = Selection.cursor((cursor_row, 0)) + print(f"new selection = {self.selection}") # ------ Cursor movement actions def action_cursor_left(self) -> None: @@ -700,8 +708,8 @@ def action_cursor_right(self) -> None: if self.cursor_at_end_of_document: return - target_row, target_column = self.get_cursor_right_position() - self.selection = Selection.cursor((target_row, target_column)) + target = self.get_cursor_right_position() + self.selection = Selection.cursor(target) self._record_last_intentional_cell_width() def action_cursor_right_select(self): @@ -709,7 +717,7 @@ def action_cursor_right_select(self): This will expand or contract the selection. """ - if self.cursor_at_start_of_document: + if self.cursor_at_end_of_document: return new_cursor_position = self.get_cursor_right_position() selection_start, selection_end = self.selection @@ -728,34 +736,50 @@ def action_cursor_down(self) -> None: if self.cursor_at_last_row: self.cursor_to_line_end() - cursor_row, cursor_column = self.selection.end + target = self.get_cursor_down_position() + self.selection = Selection.cursor(target) - target_row = min(len(self.document_lines) - 1, cursor_row + 1) + def action_cursor_down_select(self) -> None: + """Move the cursor down one cell, selecting the range between the old and new positions.""" + if self.cursor_at_last_row: + self.cursor_to_line_end(select=True) + target = self.get_cursor_down_position() + start, end = self.selection + self.selection = Selection(start, target) + def get_cursor_down_position(self): + """Get the position the cursor will move to if it moves down.""" + cursor_row, cursor_column = self.selection.end + target_row = min(len(self.document_lines) - 1, cursor_row + 1) # Attempt to snap last intentional cell length target_column = self.cell_width_to_column_index( self._last_intentional_cell_width, target_row ) target_column = clamp(target_column, 0, len(self.document_lines[target_row])) - - self.selection = Selection.cursor((target_row, target_column)) + return target_row, target_column def action_cursor_up(self) -> None: """Move the cursor up one cell.""" - if self.cursor_at_first_row: - self.cursor_to_line_start() + target = self.get_cursor_up_position() + self.selection = Selection.cursor(target) - cursor_row, cursor_column = self.selection.end + def action_cursor_up_select(self) -> None: + """Move the cursor up one cell, selecting the range between the old and new positions.""" + target = self.get_cursor_up_position() + start, end = self.selection + self.selection = Selection(start, target) + def get_cursor_up_position(self) -> tuple[int, int]: + if self.cursor_at_first_row: + return 0, 0 + cursor_row, cursor_column = self.selection.end target_row = max(0, cursor_row - 1) - # Attempt to snap last intentional cell length target_column = self.cell_width_to_column_index( self._last_intentional_cell_width, target_row ) target_column = clamp(target_column, 0, len(self.document_lines[target_row])) - - self.selection = Selection.cursor((target_row, target_column)) + return target_row, target_column def action_cursor_line_end(self) -> None: self.cursor_to_line_end() From 5f856ac8cf4d101df5f3652a7e66359182c119c0 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 20 Jul 2023 14:40:30 +0100 Subject: [PATCH 071/366] Refactoring cursor positioning logic --- src/textual/widgets/_text_editor.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index b0af235a9d..2830052443 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -674,8 +674,6 @@ def action_cursor_left(self) -> None: If the cursor is at the left edge of the document, try to move it to the end of the previous line. """ - if self.cursor_at_start_of_document: - return target_row, target_column = self.get_cursor_left_position() self.selection = Selection.cursor((target_row, target_column)) self._record_last_intentional_cell_width() @@ -685,15 +683,15 @@ def action_cursor_left_select(self): This will expand or contract the selection. """ - if self.cursor_at_start_of_document: - return new_cursor_position = self.get_cursor_left_position() selection_start, selection_end = self.selection self.selection = Selection(selection_start, new_cursor_position) self._record_last_intentional_cell_width() - def get_cursor_left_position(self): + def get_cursor_left_position(self) -> tuple[int, int]: """Get the position the cursor will move to if it moves left.""" + if self.cursor_at_start_of_document: + return 0, 0 cursor_row, cursor_column = self.selection.end length_of_row_above = len(self.document_lines[cursor_row - 1]) target_row = cursor_row if cursor_column != 0 else cursor_row - 1 @@ -705,9 +703,6 @@ def action_cursor_right(self) -> None: If the cursor is at the end of a line, attempt to go to the start of the next line. """ - if self.cursor_at_end_of_document: - return - target = self.get_cursor_right_position() self.selection = Selection.cursor(target) self._record_last_intentional_cell_width() @@ -717,8 +712,6 @@ def action_cursor_right_select(self): This will expand or contract the selection. """ - if self.cursor_at_end_of_document: - return new_cursor_position = self.get_cursor_right_position() selection_start, selection_end = self.selection self.selection = Selection(selection_start, new_cursor_position) @@ -726,6 +719,8 @@ def action_cursor_right_select(self): def get_cursor_right_position(self) -> tuple[int, int]: """Get the position the cursor will move to if it moves right.""" + if self.cursor_at_end_of_document: + return self.selection.end cursor_row, cursor_column = self.selection.end target_row = cursor_row + 1 if self.cursor_at_end_of_row else cursor_row target_column = 0 if self.cursor_at_end_of_row else cursor_column + 1 @@ -733,16 +728,11 @@ def get_cursor_right_position(self) -> tuple[int, int]: def action_cursor_down(self) -> None: """Move the cursor down one cell.""" - if self.cursor_at_last_row: - self.cursor_to_line_end() - target = self.get_cursor_down_position() self.selection = Selection.cursor(target) def action_cursor_down_select(self) -> None: """Move the cursor down one cell, selecting the range between the old and new positions.""" - if self.cursor_at_last_row: - self.cursor_to_line_end(select=True) target = self.get_cursor_down_position() start, end = self.selection self.selection = Selection(start, target) @@ -750,6 +740,9 @@ def action_cursor_down_select(self) -> None: def get_cursor_down_position(self): """Get the position the cursor will move to if it moves down.""" cursor_row, cursor_column = self.selection.end + if self.cursor_at_last_row: + return cursor_row, len(self.document_lines[cursor_row]) + target_row = min(len(self.document_lines) - 1, cursor_row + 1) # Attempt to snap last intentional cell length target_column = self.cell_width_to_column_index( @@ -842,9 +835,6 @@ def action_cursor_right_word(self) -> None: @property def active_line_text(self) -> str: # TODO - consider empty documents - log.error(self.selection) - log.error(self.selection.end) - log.error(self.selection.end[0]) return self.document_lines[self.selection.end[0]] def get_column_cell_width(self, row: int, column: int) -> int: From 738d69addc391e3428c748861d5177dc71d9adb1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 09:53:05 +0100 Subject: [PATCH 072/366] Docstrings, fix delete syntax tree edit --- src/textual/widgets/_text_editor.py | 34 ++++++++++++++++++----------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 2830052443..6340cf65a6 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -110,9 +110,16 @@ def undo(self, editor: TextEditor) -> None: @dataclass class Delete: + """Performs a delete operation.""" + from_position: tuple[int, int] + """The position to delete from (inclusive).""" + to_position: tuple[int, int] + """The position to delete to (exclusive).""" + cursor_destination: tuple[int, int] | None = None + """Where to move the cursor to after the deletion.""" def do(self, editor: TextEditor) -> None: """Do the action.""" @@ -763,6 +770,7 @@ def action_cursor_up_select(self) -> None: self.selection = Selection(start, target) def get_cursor_up_position(self) -> tuple[int, int]: + """Get the position the cursor will move to if it moves up.""" if self.cursor_at_first_row: return 0, 0 cursor_row, cursor_column = self.selection.end @@ -775,9 +783,11 @@ def get_cursor_up_position(self) -> tuple[int, int]: return target_row, target_column def action_cursor_line_end(self) -> None: + """Move the cursor to the end of the line.""" self.cursor_to_line_end() def action_cursor_line_start(self) -> None: + """Move the cursor to the start of the line.""" self.cursor_to_line_start() def action_cursor_left_word(self) -> None: @@ -976,26 +986,22 @@ def _delete_range( Returns: A string containing the deleted text. """ + from_position = min(from_position, to_position) + to_position = max(from_position, to_position) from_row, from_column = from_position to_row, to_column = to_position - lines = self.document_lines + start_byte = self._position_to_byte_offset(min(from_position, to_position)) + old_end_byte = self._position_to_byte_offset(max(from_position, to_position)) - # Ensure that from_position is before to_position - if from_position > to_position: - from_row, from_column, to_row, to_column = ( - to_row, - to_column, - from_row, - from_column, - ) + lines = self.document_lines # If the range is within a single line if from_row == to_row: line = lines[from_row] deleted_text = line[from_column:to_column] - lines[from_row] = line[:from_column] + line[to_column:] + lines[from_row] = line[:from_column] + line[to_column + 1 :] else: # The range spans multiple lines start_line = lines[from_row] @@ -1016,11 +1022,10 @@ def _delete_range( del lines[from_row + 1 : to_row + 1] if self._syntax_tree is not None: - start_byte = self._position_to_byte_offset(from_position) self._syntax_tree.edit( start_byte=start_byte, - old_end_byte=self._position_to_byte_offset(to_position), - new_end_byte=start_byte, + old_end_byte=old_end_byte, + new_end_byte=old_end_byte - len(deleted_text), start_point=from_position, old_end_point=to_position, new_end_point=from_position, @@ -1031,6 +1036,7 @@ def _delete_range( self._prepare_highlights() self._refresh_size() + if cursor_destination is not None: self.selection = Selection.cursor(cursor_destination) else: @@ -1049,6 +1055,8 @@ def action_delete_left(self) -> None: start, end = self.selection end_row, end_column = end + # If the cursor is at the right hand side + if self.cursor_at_start_of_row: to_position = (end_row - 1, len(lines[end_row - 1])) else: From ac922409ed051dda1adcb13dbd5898060439fc48 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 10:32:34 +0100 Subject: [PATCH 073/366] Fixing deletion ranges --- src/textual/widgets/_text_editor.py | 79 ++++++++++++++++------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 6340cf65a6..b45ce698ad 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -62,6 +62,7 @@ class Highlight(NamedTuple): class Selection(NamedTuple): """A range of characters within a document from a start point to the end point. The position of the cursor is always considered to be the `end` point of the selection. + The selection is inclusive of the minimum point and exclusive of the maximum point. """ start: tuple[int, int] = (0, 0) @@ -73,11 +74,15 @@ def cursor(cls, position: tuple[int, int]) -> "Selection": return cls(position, position) @property - def is_cursor(self) -> bool: + def is_empty(self) -> bool: """Return True if the selection has 0 width, i.e. it's just a cursor.""" start, end = self return start == end + def range(self) -> tuple[tuple[int, int], tuple[int, int]]: + start, end = self + return _fix_direction(start, end) + @runtime_checkable class Edit(Protocol): @@ -138,6 +143,15 @@ def __rich_repr__(self): yield "deleted_text", self.deleted_text +def _fix_direction( + start: tuple[int, int], end: tuple[int, int] +) -> tuple[tuple[int, int], tuple[int, int]]: + """Given a range, return a new range (x, y) such that x <= y which covers the same characters.""" + if start > end: + return end, start + return start, end + + class TextEditor(ScrollView, can_focus=True): DEFAULT_CSS = """\ $editor-active-line-bg: white 8%; @@ -550,14 +564,6 @@ def get_target_document_location(self, offset: Offset) -> tuple[int, int]: return target_row, target_column - def _fix_direction( - self, start: tuple[int, int], end: tuple[int, int] - ) -> tuple[tuple[int, int], tuple[int, int]]: - """Given a range, return a new range (x, y) such that x <= y which covers the same characters.""" - if start > end: - return end, start - return start, end - def _on_mouse_down(self, event: events.MouseDown) -> None: event.stop() offset = event.get_content_offset(self) @@ -857,7 +863,6 @@ def get_column_cell_width(self, row: int, column: int) -> int: def _record_last_intentional_cell_width(self) -> None: row, column = self.selection.end column_cell_length = self.get_column_cell_width(row, column) - log(f"last intentional cell width = {column_cell_length}") self._last_intentional_cell_width = column_cell_length # --- Editor operations @@ -973,7 +978,9 @@ def delete_range( to_position: tuple[int, int], cursor_destination: tuple[int, int] | None = None, ) -> str: - return self.edit(Delete(from_position, to_position, cursor_destination)) + top, bottom = _fix_direction(from_position, to_position) + print(f"top and bottom: {top, bottom}") + return self.edit(Delete(top, bottom, cursor_destination)) def _delete_range( self, @@ -983,17 +990,16 @@ def _delete_range( ) -> str: """Delete text between `from_position` and `to_position`. + `from_position` is inclusive. The `to_position` is exclusive. + Returns: A string containing the deleted text. """ - from_position = min(from_position, to_position) - to_position = max(from_position, to_position) - from_row, from_column = from_position to_row, to_column = to_position - start_byte = self._position_to_byte_offset(min(from_position, to_position)) - old_end_byte = self._position_to_byte_offset(max(from_position, to_position)) + start_byte = self._position_to_byte_offset(from_position) + old_end_byte = self._position_to_byte_offset(to_position) lines = self.document_lines @@ -1001,7 +1007,7 @@ def _delete_range( if from_row == to_row: line = lines[from_row] deleted_text = line[from_column:to_column] - lines[from_row] = line[:from_column] + line[to_column + 1 :] + lines[from_row] = line[:from_column] + line[to_column:] else: # The range spans multiple lines start_line = lines[from_row] @@ -1047,22 +1053,23 @@ def _delete_range( def action_delete_left(self) -> None: """Deletes the character to the left of the cursor and updates the cursor position.""" - if self.cursor_at_start_of_document: - return - lines = self.document_lines + selection = self.selection + empty = selection.is_empty - start, end = self.selection - end_row, end_column = end + if self.cursor_at_start_of_document and empty: + return - # If the cursor is at the right hand side + start, end = selection + end_row, end_column = end - if self.cursor_at_start_of_row: - to_position = (end_row - 1, len(lines[end_row - 1])) - else: - to_position = (end_row, end_column - 1) + if empty: + if self.cursor_at_start_of_row: + end = (end_row - 1, len(self.document_lines[end_row - 1])) + else: + end = (end_row, end_column - 1) - self.edit(Delete(start, to_position)) + self.delete_range(start, end) def action_delete_right(self) -> None: """Deletes the character to the right of the cursor and keeps the cursor at the same position.""" @@ -1077,7 +1084,7 @@ def action_delete_right(self) -> None: else: to_position = (end_row, end_column + 1) - self.edit(Delete(start, to_position)) + self.delete_range(start, to_position) def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" @@ -1088,21 +1095,21 @@ def action_delete_line(self) -> None: from_position = (start_row, 0) to_position = (end_row + 1, 0) - self.edit(Delete(from_position, to_position)) + self.delete_range(from_position, to_position) def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor position to the start of the line.""" from_position = self.selection.end cursor_row, cursor_column = from_position to_position = (cursor_row, 0) - self.edit(Delete(from_position, to_position)) + self.delete_range(from_position, to_position) def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor position to the end of the line.""" from_position = self.selection.end cursor_row, cursor_column = from_position to_position = (cursor_row, len(self.document_lines[cursor_row])) - self.edit(Delete(from_position, to_position)) + self.delete_range(from_position, to_position) def action_delete_word_left(self) -> None: """Deletes the word to the left of the cursor and updates the cursor position.""" @@ -1113,7 +1120,7 @@ def action_delete_word_left(self) -> None: # deletes the characters within the selection range, ignoring word boundaries. start, end = self.selection if start != end: - self.edit(Delete(start, end)) + self.delete_range(start, end) cursor_row, cursor_column = end @@ -1131,7 +1138,7 @@ def action_delete_word_left(self) -> None: # If we're already on the first line and no word boundary is found, delete to the start of the line from_position = (cursor_row, 0) - self.edit(Delete(from_position, self.selection.end)) + self.delete_range(from_position, self.selection.end) def action_delete_word_right(self) -> None: """Deletes the word to the right of the cursor and keeps the cursor at the same position.""" @@ -1140,7 +1147,7 @@ def action_delete_word_right(self) -> None: start, end = self.selection if start != end: - self.edit(Delete(start, end)) + self.delete_range(start, end) cursor_row, cursor_column = end @@ -1158,7 +1165,7 @@ def action_delete_word_right(self) -> None: # If we're already on the last line and no word boundary is found, delete to the end of the line to_position = (cursor_row, len(self.document_lines[cursor_row])) - self.edit(Delete(end, to_position)) + self.delete_range(end, to_position) # --- Debugging @dataclass From 064e6eae0e62c08cbeff80c2bd7d4f24735f7376 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 11:03:08 +0100 Subject: [PATCH 074/366] Include languages file --- .gitignore | 1 + tree-sitter/textual-languages.so | Bin 0 -> 514209 bytes 2 files changed, 1 insertion(+) create mode 100755 tree-sitter/textual-languages.so diff --git a/.gitignore b/.gitignore index b3307c9311..1da36b6f27 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ __pycache__/ # C extensions *.so +!tree-sitter/textual-languages.so # Distribution / packaging .Python diff --git a/tree-sitter/textual-languages.so b/tree-sitter/textual-languages.so new file mode 100755 index 0000000000000000000000000000000000000000..fcec820ad71636b88df8fee7cf5b9c72afdad918 GIT binary patch literal 514209 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 Date: Mon, 24 Jul 2023 11:31:55 +0100 Subject: [PATCH 075/366] Simplify cursor_left --- src/textual/widgets/_text_editor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index b45ce698ad..02049b410b 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -687,8 +687,8 @@ def action_cursor_left(self) -> None: If the cursor is at the left edge of the document, try to move it to the end of the previous line. """ - target_row, target_column = self.get_cursor_left_position() - self.selection = Selection.cursor((target_row, target_column)) + target = self.get_cursor_left_position() + self.selection = Selection.cursor(target) self._record_last_intentional_cell_width() def action_cursor_left_select(self): From ddb623af4e9d5274fdf1a0602ec285e7b7518fee Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 13:17:35 +0100 Subject: [PATCH 076/366] Fix broken Input tests --- src/textual/widgets/_text_editor.py | 2 -- .../test_input_key_modification_actions.py | 32 +++++++++---------- .../input/test_input_key_movement_actions.py | 28 ++++++++-------- tests/input/test_input_mouse.py | 8 ++--- 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_editor.py index 02049b410b..b2ab437351 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_editor.py @@ -6,7 +6,6 @@ from pathlib import Path from typing import ClassVar, NamedTuple -from rich.cells import get_character_cell_size from rich.style import Style from rich.text import Text from tree_sitter import Language, Parser, Tree @@ -599,7 +598,6 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: for column_index, character in enumerate(line): total_cell_offset += cell_len(character) if total_cell_offset >= cell_width + 1: - log(f"cell width {cell_width} -> column_index {column_index}") return column_index return len(line) diff --git a/tests/input/test_input_key_modification_actions.py b/tests/input/test_input_key_modification_actions.py index 25ddb3f810..bfd935fd9d 100644 --- a/tests/input/test_input_key_modification_actions.py +++ b/tests/input/test_input_key_modification_actions.py @@ -26,7 +26,7 @@ async def test_delete_left_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_delete_left() - assert input.selection == 0 + assert input.cursor_position == 0 assert input.value == TEST_INPUTS[input.id] @@ -36,7 +36,7 @@ async def test_delete_left_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_left() - assert input.selection == len(input.value) + assert input.cursor_position == len(input.value) assert input.value == TEST_INPUTS[input.id][:-1] @@ -45,16 +45,16 @@ async def test_delete_left_word_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_delete_left_word() - assert input.selection == 0 + assert input.cursor_position == 0 assert input.value == TEST_INPUTS[input.id] async def test_delete_left_word_from_inside_first_word() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): - input.selection = 1 + input.cursor_position = 1 input.action_delete_left_word() - assert input.selection == 0 + assert input.cursor_position == 0 assert input.value == TEST_INPUTS[input.id][1:] @@ -70,7 +70,7 @@ async def test_delete_left_word_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_left_word() - assert input.selection == len(input.value) + assert input.cursor_position == len(input.value) assert input.value == expected[input.id] @@ -81,7 +81,7 @@ async def test_password_delete_left_word_from_end() -> None: input.action_end() input.password = True input.action_delete_left_word() - assert input.selection == 0 + assert input.cursor_position == 0 assert input.value == "" @@ -90,7 +90,7 @@ async def test_delete_left_all_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_delete_left_all() - assert input.selection == 0 + assert input.cursor_position == 0 assert input.value == TEST_INPUTS[input.id] @@ -100,7 +100,7 @@ async def test_delete_left_all_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_left_all() - assert input.selection == 0 + assert input.cursor_position == 0 assert input.value == "" @@ -109,7 +109,7 @@ async def test_delete_right_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_delete_right() - assert input.selection == 0 + assert input.cursor_position == 0 assert input.value == TEST_INPUTS[input.id][1:] @@ -119,7 +119,7 @@ async def test_delete_right_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_right() - assert input.selection == len(input.value) + assert input.cursor_position == len(input.value) assert input.value == TEST_INPUTS[input.id] @@ -134,7 +134,7 @@ async def test_delete_right_word_from_home() -> None: } for input in pilot.app.query(Input): input.action_delete_right_word() - assert input.selection == 0 + assert input.cursor_position == 0 assert input.value == expected[input.id] @@ -144,7 +144,7 @@ async def test_password_delete_right_word_from_home() -> None: for input in pilot.app.query(Input): input.password = True input.action_delete_right_word() - assert input.selection == 0 + assert input.cursor_position == 0 assert input.value == "" @@ -154,7 +154,7 @@ async def test_delete_right_word_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_right_word() - assert input.selection == len(input.value) + assert input.cursor_position == len(input.value) assert input.value == TEST_INPUTS[input.id] @@ -163,7 +163,7 @@ async def test_delete_right_all_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_delete_right_all() - assert input.selection == 0 + assert input.cursor_position == 0 assert input.value == "" @@ -173,5 +173,5 @@ async def test_delete_right_all_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_delete_right_all() - assert input.selection == len(input.value) + assert input.cursor_position == len(input.value) assert input.value == TEST_INPUTS[input.id] diff --git a/tests/input/test_input_key_movement_actions.py b/tests/input/test_input_key_movement_actions.py index 60bf1f11b8..a6cf136947 100644 --- a/tests/input/test_input_key_movement_actions.py +++ b/tests/input/test_input_key_movement_actions.py @@ -28,7 +28,7 @@ async def test_input_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_home() - assert input.selection == 0 + assert input.cursor_position == 0 async def test_input_end() -> None: @@ -36,7 +36,7 @@ async def test_input_end() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_end() - assert input.selection == len(input.value) + assert input.cursor_position == len(input.value) async def test_input_right_from_home() -> None: @@ -44,7 +44,7 @@ async def test_input_right_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_cursor_right() - assert input.selection == (1 if input.value else 0) + assert input.cursor_position == (1 if input.value else 0) async def test_input_right_from_end() -> None: @@ -53,7 +53,7 @@ async def test_input_right_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_cursor_right() - assert input.selection == len(input.value) + assert input.cursor_position == len(input.value) async def test_input_left_from_home() -> None: @@ -61,7 +61,7 @@ async def test_input_left_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_cursor_left() - assert input.selection == 0 + assert input.cursor_position == 0 async def test_input_left_from_end() -> None: @@ -70,7 +70,7 @@ async def test_input_left_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_cursor_left() - assert input.selection == (len(input.value) - 1 if input.value else 0) + assert input.cursor_position == (len(input.value) - 1 if input.value else 0) async def test_input_left_word_from_home() -> None: @@ -78,7 +78,7 @@ async def test_input_left_word_from_home() -> None: async with InputTester().run_test() as pilot: for input in pilot.app.query(Input): input.action_cursor_left_word() - assert input.selection == 0 + assert input.cursor_position == 0 async def test_input_left_word_from_end() -> None: @@ -94,7 +94,7 @@ async def test_input_left_word_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_cursor_left_word() - assert input.selection == expected_at[input.id] + assert input.cursor_position == expected_at[input.id] async def test_password_input_left_word_from_end() -> None: @@ -104,7 +104,7 @@ async def test_password_input_left_word_from_end() -> None: input.action_end() input.password = True input.action_cursor_left_word() - assert input.selection == 0 + assert input.cursor_position == 0 async def test_input_right_word_from_home() -> None: @@ -119,7 +119,7 @@ async def test_input_right_word_from_home() -> None: } for input in pilot.app.query(Input): input.action_cursor_right_word() - assert input.selection == expected_at[input.id] + assert input.cursor_position == expected_at[input.id] async def test_password_input_right_word_from_home() -> None: @@ -128,7 +128,7 @@ async def test_password_input_right_word_from_home() -> None: for input in pilot.app.query(Input): input.password = True input.action_cursor_right_word() - assert input.selection == len(input.value) + assert input.cursor_position == len(input.value) async def test_input_right_word_from_end() -> None: @@ -137,7 +137,7 @@ async def test_input_right_word_from_end() -> None: for input in pilot.app.query(Input): input.action_end() input.action_cursor_right_word() - assert input.selection == len(input.value) + assert input.cursor_position == len(input.value) async def test_input_right_word_to_the_end() -> None: @@ -152,7 +152,7 @@ async def test_input_right_word_to_the_end() -> None: } for input in pilot.app.query(Input): hops = 0 - while input.selection < len(input.value): + while input.cursor_position < len(input.value): input.action_cursor_right_word() hops += 1 assert hops == expected_hops[input.id] @@ -171,7 +171,7 @@ async def test_input_left_word_from_the_end() -> None: for input in pilot.app.query(Input): input.action_end() hops = 0 - while input.selection: + while input.cursor_position: input.action_cursor_left_word() hops += 1 assert hops == expected_hops[input.id] diff --git a/tests/input/test_input_mouse.py b/tests/input/test_input_mouse.py index fda0b9d837..e4bfbb51d6 100644 --- a/tests/input/test_input_mouse.py +++ b/tests/input/test_input_mouse.py @@ -32,14 +32,14 @@ async def test_mouse_clicks_within(click_at, should_land): # Input. await pilot.click(Input, Offset(click_at + 3, 1)) await pilot.pause() - assert pilot.app.query_one(Input).selection == should_land + assert pilot.app.query_one(Input).cursor_position == should_land async def test_mouse_click_outwith(): """Mouse clicks outside the input should not affect cursor position.""" async with InputApp().run_test() as pilot: - pilot.app.query_one(Input).selection = 3 - assert pilot.app.query_one(Input).selection == 3 + pilot.app.query_one(Input).cursor_position = 3 + assert pilot.app.query_one(Input).cursor_position == 3 await pilot.click(Input, Offset(0, 0)) await pilot.pause() - assert pilot.app.query_one(Input).selection == 3 + assert pilot.app.query_one(Input).cursor_position == 3 From 746314ac022bdd17f4d2a3993a2241bc2484e3ab Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 15:19:20 +0100 Subject: [PATCH 077/366] Create Document class with insert operation only --- src/textual/_document.py | 94 +++++++++++++++++++ src/textual/_types.py | 2 +- src/textual/widgets/__init__.py | 4 +- src/textual/widgets/__init__.pyi | 2 +- .../{_text_editor.py => _text_area.py} | 68 +++++++------- src/textual/widgets/text_editor.py | 2 +- .../test_text_area_document_delete.py} | 4 +- .../test_text_area_document_insert.py | 93 ++++++++++++++++++ .../test_text_editor_document_insert.py | 92 ------------------ 9 files changed, 228 insertions(+), 133 deletions(-) create mode 100644 src/textual/_document.py rename src/textual/widgets/{_text_editor.py => _text_area.py} (97%) rename tests/{text_editor/test_text_editor_document_delete.py => text_area/test_text_area_document_delete.py} (98%) create mode 100644 tests/text_area/test_text_area_document_insert.py delete mode 100644 tests/text_editor/test_text_editor_document_insert.py diff --git a/src/textual/_document.py b/src/textual/_document.py new file mode 100644 index 0000000000..e7aae38ebc --- /dev/null +++ b/src/textual/_document.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from rich.text import Text + +from textual._cells import cell_len +from textual._types import SupportsIndex +from textual.geometry import Size +from textual.widgets._text_area import _fix_direction + + +class Document: + def __init__(self) -> None: + self._lines: list[str] = [] + + def load_text(self, text: str) -> None: + """Load text from a string into the document. + + Args: + text: The text to load into the document + """ + lines = text.splitlines(keepends=False) + if text[-1] == "\n": + lines.append("") + + self._lines = lines + + def insert_range( + self, start: tuple[int, int], end: tuple[int, int], text: str + ) -> tuple[int, int]: + """Insert text at the given range. + + Args: + start: A tuple (row, column) where the edit starts. + end: A tuple (row, column) where the edit ends. + text: The text to insert between start and end. + + Returns: + The new end location after the edit is complete. + """ + if not text: + return end + + top, bottom = _fix_direction(start, end) + top_row, top_column = top + bottom_row, bottom_column = bottom + + insert_lines = text.splitlines() + if text.endswith("\n"): + # Special case where a single newline character is inserted. + insert_lines.append("") + + lines = self._lines + + before_selection = lines[top_row][:top_column] + after_selection = lines[bottom_row][bottom_column:] + + insert_lines[0] = before_selection + insert_lines[0] + destination_column = len(insert_lines[-1]) + insert_lines[-1] = insert_lines[-1] + after_selection + + lines[top_row : bottom_row + 1] = insert_lines + destination_row = top_row + len(insert_lines) - 1 + + end_point = destination_row, destination_column + return end_point + + def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: + """Delete the text at the given range.""" + + @property + def size(self) -> Size: + """Returns the size (width, height) of the document.""" + lines = self._lines + text_width = max(cell_len(line) for line in lines) + height = len(lines) + # We add one to the text width to leave a space for the cursor, since it + # can rest at the end of a line where there isn't yet any character. + # Similarly, the cursor can rest below the bottom line of text, where + # a line doesn't currently exist. + + # TODO: This was previously `Size(text_width + 1, height)` to leave space + # for the cursor. However, that is a widget level concern. + return Size(text_width, height) + + @property + def line_count(self) -> int: + """Returns the number of lines in the document""" + return len(self._lines) + + def get_line(self, index: int) -> Text: + """Returns the line with the given index from the document""" + + def __getitem__(self, item: SupportsIndex | slice) -> str: + return self._lines[item] diff --git a/src/textual/_types.py b/src/textual/_types.py index 41d08dae5b..e8ed4846cb 100644 --- a/src/textual/_types.py +++ b/src/textual/_types.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Union -from typing_extensions import Protocol, runtime_checkable +from typing_extensions import Protocol, SupportsIndex, runtime_checkable if TYPE_CHECKING: from rich.segment import Segment diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index be59f4f5d4..ea56010e78 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -36,7 +36,7 @@ from ._switch import Switch from ._tabbed_content import TabbedContent, TabPane from ._tabs import Tab, Tabs - from ._text_editor import TextEditor + from ._text_area import TextArea from ._text_log import TextLog from ._tooltip import Tooltip from ._tree import Tree @@ -73,7 +73,7 @@ "TabbedContent", "TabPane", "Tabs", - "TextEditor", + "TextArea", "TextLog", "Tooltip", "Tree", diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi index 60535633dd..a757a37ef8 100644 --- a/src/textual/widgets/__init__.pyi +++ b/src/textual/widgets/__init__.pyi @@ -28,7 +28,7 @@ from ._tabbed_content import TabbedContent as TabbedContent from ._tabbed_content import TabPane as TabPane from ._tabs import Tab as Tab from ._tabs import Tabs as Tabs -from ._text_editor import TextEditor as TextEditor +from ._text_area import TextArea as TextArea from ._text_log import TextLog as TextLog from ._tooltip import Tooltip as Tooltip from ._tree import Tree as Tree diff --git a/src/textual/widgets/_text_editor.py b/src/textual/widgets/_text_area.py similarity index 97% rename from src/textual/widgets/_text_editor.py rename to src/textual/widgets/_text_area.py index b2ab437351..1df7937861 100644 --- a/src/textual/widgets/_text_editor.py +++ b/src/textual/widgets/_text_area.py @@ -87,10 +87,10 @@ def range(self) -> tuple[tuple[int, int], tuple[int, int]]: class Edit(Protocol): """Protocol for actions performed in the text editor that can be done and undone.""" - def do(self, editor: TextEditor) -> object | None: + def do(self, editor: TextArea) -> object | None: """Do the action.""" - def undo(self, editor: TextEditor) -> object | None: + def undo(self, editor: TextArea) -> object | None: """Undo the action.""" @@ -102,13 +102,13 @@ class Insert(NamedTuple): to_position: tuple[int, int] move_cursor: bool = True - def do(self, editor: TextEditor) -> None: + def do(self, editor: TextArea) -> None: if self.text: editor._insert_text_range( self.text, self.from_position, self.to_position, self.move_cursor ) - def undo(self, editor: TextEditor) -> None: + def undo(self, editor: TextArea) -> None: """Undo the action.""" @@ -125,14 +125,14 @@ class Delete: cursor_destination: tuple[int, int] | None = None """Where to move the cursor to after the deletion.""" - def do(self, editor: TextEditor) -> None: + def do(self, editor: TextArea) -> None: """Do the action.""" self.deleted_text = editor._delete_range( self.from_position, self.to_position, self.cursor_destination ) return self.deleted_text - def undo(self, editor: TextEditor) -> None: + def undo(self, editor: TextArea) -> None: """Undo the action.""" def __rich_repr__(self): @@ -142,47 +142,38 @@ def __rich_repr__(self): yield "deleted_text", self.deleted_text -def _fix_direction( - start: tuple[int, int], end: tuple[int, int] -) -> tuple[tuple[int, int], tuple[int, int]]: - """Given a range, return a new range (x, y) such that x <= y which covers the same characters.""" - if start > end: - return end, start - return start, end - - -class TextEditor(ScrollView, can_focus=True): +class TextArea(ScrollView, can_focus=True): DEFAULT_CSS = """\ $editor-active-line-bg: white 8%; -TextEditor { +TextArea { background: $panel; } -TextEditor > .text-editor--active-line { +TextArea > .text-area--active-line { background: $editor-active-line-bg; } -TextEditor > .text-editor--active-line-gutter { +TextArea > .text-area--active-line-gutter { color: $text; background: $editor-active-line-bg; } -TextEditor > .text-editor--gutter { +TextArea > .text-area--gutter { color: $text-muted 40%; } -TextEditor > .text-editor--cursor { +TextArea > .text-area--cursor { color: $text; background: white 80%; } -TextEditor > .text-editor--selection { +TextArea > .text-area--selection { background: $primary; } """ COMPONENT_CLASSES: ClassVar[set[str]] = { - "text-editor--active-line", - "text-editor--active-line-gutter", - "text-editor--gutter", - "text-editor--cursor", - "text-editor--selection", + "text-area--active-line", + "text-area--active-line-gutter", + "text-area--gutter", + "text-area--cursor", + "text-area--selection", } BINDINGS = [ @@ -335,6 +326,7 @@ def load_lines(self, lines: list[str]) -> None: log.debug(f"loaded text. parser = {self._parser} ast = {self._syntax_tree}") def clear(self) -> None: + # TODO: Perform a delete on the whole document. self.load_text("") # --- Methods for measuring things (e.g. virtual sizes) @@ -376,7 +368,7 @@ def render_line(self, widget_y: int) -> Strip: start, end = self.selection end_row, end_column = end - selection_style = self.get_component_rich_style("text-editor--selection") + selection_style = self.get_component_rich_style("text-area--selection") # Start and end can be before or after each other, depending on the direction # you move the cursor during selecting text, but the "top" of the selection @@ -412,21 +404,19 @@ def render_line(self, widget_y: int) -> Strip: # Show the cursor and the selection if end_row == document_y: - cursor_style = self.get_component_rich_style("text-editor--cursor") + cursor_style = self.get_component_rich_style("text-area--cursor") line_text.stylize(cursor_style, end_column, end_column + 1) - active_line_style = self.get_component_rich_style( - "text-editor--active-line" - ) + active_line_style = self.get_component_rich_style("text-area--active-line") line_text.stylize_before(active_line_style) # Show the gutter if self.show_line_numbers: if end_row == document_y: gutter_style = self.get_component_rich_style( - "text-editor--active-line-gutter" + "text-area--active-line-gutter" ) else: - gutter_style = self.get_component_rich_style("text-editor--gutter") + gutter_style = self.get_component_rich_style("text-area--gutter") gutter_width_no_margin = self.gutter_width - 2 gutter = Text( @@ -1205,6 +1195,16 @@ def debug_state(self) -> "EditorDebug": ) +def _fix_direction( + start: tuple[int, int], end: tuple[int, int] +) -> tuple[tuple[int, int], tuple[int, int]]: + """Given a range, return a new range (x, y) such + that x <= y which covers the same characters.""" + if start > end: + return end, start + return start, end + + def traverse_tree(cursor): reached_root = False while reached_root == False: diff --git a/src/textual/widgets/text_editor.py b/src/textual/widgets/text_editor.py index 2b5caf6317..c40af33762 100644 --- a/src/textual/widgets/text_editor.py +++ b/src/textual/widgets/text_editor.py @@ -1,3 +1,3 @@ -from ._text_editor import Highlight +from ._text_area import Highlight __all__ = ["Highlight"] diff --git a/tests/text_editor/test_text_editor_document_delete.py b/tests/text_area/test_text_area_document_delete.py similarity index 98% rename from tests/text_editor/test_text_editor_document_delete.py rename to tests/text_area/test_text_area_document_delete.py index 809b882b79..e58674e04d 100644 --- a/tests/text_editor/test_text_editor_document_delete.py +++ b/tests/text_area/test_text_area_document_delete.py @@ -1,6 +1,6 @@ import pytest -from textual.widgets import TextEditor +from textual.widgets import TextArea TEXT = """I must not fear. Fear is the mind-killer. @@ -10,7 +10,7 @@ @pytest.fixture def editor(): - editor = TextEditor() + editor = TextArea() editor.load_text(TEXT) return editor diff --git a/tests/text_area/test_text_area_document_insert.py b/tests/text_area/test_text_area_document_insert.py new file mode 100644 index 0000000000..3ca887205a --- /dev/null +++ b/tests/text_area/test_text_area_document_insert.py @@ -0,0 +1,93 @@ +import pytest + +from textual._document import Document +from textual.widgets import TextArea + +TEXT = """I must not fear. +Fear is the mind-killer.""" + + +def test_insert_text_no_newlines(): + document = Document() + document.load_text(TEXT) + document.insert_range((0, 1), (0, 1), " really") + assert document._lines == [ + "I really must not fear.", + "Fear is the mind-killer.", + ] + + +def test_insert_text_empty_string(): + document = Document() + document.load_text(TEXT) + document.insert_range((0, 1), (0, 1), "") + assert document._lines == ["I must not fear.", "Fear is the mind-killer."] + + +@pytest.mark.xfail(reason="undecided on behaviour") +def test_insert_text_invalid_column(): + # TODO - what is the correct behaviour here? + # right now it appends to the end of the line if the column is too large. + document = Document() + document.load_text(TEXT) + document.insert_range((0, 999), (0, 999), " really") + assert document._lines == ["I must not fear.", "Fear is the mind-killer."] + + +@pytest.mark.xfail(reason="undecided on behaviour") +def test_insert_text_invalid_row(): + # TODO - this raises an IndexError for list index out of range + document = Document() + document.load_text(TEXT) + document.insert_range((999, 0), (999, 0), " really") + assert document._lines == ["I must not fear.", "Fear is the mind-killer."] + + +def test_insert_text_range_newline_file_start(): + document = Document() + document.load_text(TEXT) + document.insert_range((0, 0), (0, 0), "\n") + assert document._lines == ["", "I must not fear.", "Fear is the mind-killer."] + + +def test_insert_text_newline_splits_line(): + document = Document() + document.load_text(TEXT) + document.insert_range((0, 1), (0, 1), "\n") + assert document._lines == ["I", " must not fear.", "Fear is the mind-killer."] + + +def test_insert_text_multiple_lines_ends_with_newline(): + document = Document() + document.load_text(TEXT) + document.insert_range((0, 1), (0, 1), "Hello,\nworld!\n") + assert document._lines == [ + "IHello,", + "world!", + " must not fear.", + "Fear is the mind-killer.", + ] + + +def test_insert_text_multiple_lines_starts_with_newline(): + document = Document() + document.load_text(TEXT) + document.insert_range((0, 1), (0, 1), "\nHello,\nworld!\n") + assert document._lines == [ + "I", + "Hello,", + "world!", + " must not fear.", + "Fear is the mind-killer.", + ] + + +def test_insert_range_text_no_newlines(): + """Ensuring we can do a simple replacement of text.""" + document = Document() + document.load_text(TEXT) + document.insert_range((0, 2), (0, 6), "MUST") + assert document._lines == [ + "I MUST not fear.", + "Fear is the mind-killer.", + ] diff --git a/tests/text_editor/test_text_editor_document_insert.py b/tests/text_editor/test_text_editor_document_insert.py deleted file mode 100644 index fe98e226ce..0000000000 --- a/tests/text_editor/test_text_editor_document_insert.py +++ /dev/null @@ -1,92 +0,0 @@ -import pytest - -from textual.widgets import TextEditor - -TEXT = """I must not fear. -Fear is the mind-killer.""" - - -def test_insert_text_no_newlines(): - editor = TextEditor() - editor.load_text(TEXT) - editor.insert_text(" really", (0, 1)) - assert editor.document_lines == [ - "I really must not fear.", - "Fear is the mind-killer.", - ] - - -def test_insert_text_empty_string(): - editor = TextEditor() - editor.load_text(TEXT) - editor.insert_text("", (0, 1)) - assert editor.document_lines == ["I must not fear.", "Fear is the mind-killer."] - - -@pytest.mark.xfail(reason="undecided on behaviour") -def test_insert_text_invalid_column(): - # TODO - what is the correct behaviour here? - # right now it appends to the end of the line if the column is too large. - editor = TextEditor() - editor.load_text(TEXT) - editor.insert_text(" really", (0, 999)) - assert editor.document_lines == ["I must not fear.", "Fear is the mind-killer."] - - -@pytest.mark.xfail(reason="undecided on behaviour") -def test_insert_text_invalid_row(): - # TODO - this raises an IndexError for list index out of range - editor = TextEditor() - editor.load_text(TEXT) - editor.insert_text(" really", (999, 0)) - assert editor.document_lines == ["I must not fear.", "Fear is the mind-killer."] - - -def test_insert_text_range_newline_file_start(): - editor = TextEditor() - editor.load_text(TEXT) - editor.insert_text("\n", (0, 0)) - assert editor.document_lines == ["", "I must not fear.", "Fear is the mind-killer."] - - -def test_insert_text_newline_splits_line(): - editor = TextEditor() - editor.load_text(TEXT) - editor.insert_text("\n", (0, 1)) - assert editor.document_lines == ["I", " must not fear.", "Fear is the mind-killer."] - - -def test_insert_text_multiple_lines_ends_with_newline(): - editor = TextEditor() - editor.load_text(TEXT) - editor.insert_text("Hello,\nworld!\n", (0, 1)) - assert editor.document_lines == [ - "IHello,", - "world!", - " must not fear.", - "Fear is the mind-killer.", - ] - - -def test_insert_text_multiple_lines_starts_with_newline(): - editor = TextEditor() - editor.load_text(TEXT) - editor.insert_text("\nHello,\nworld!\n", (0, 1)) - assert editor.document_lines == [ - "I", - "Hello,", - "world!", - " must not fear.", - "Fear is the mind-killer.", - ] - - -def test_insert_range_text_no_newlines(): - """Ensuring we can do a simple replacement of text.""" - editor = TextEditor() - editor.load_text(TEXT) - editor.insert_text_range("MUST", (0, 2), (0, 6)) - assert editor.document_lines == [ - "I MUST not fear.", - "Fear is the mind-killer.", - ] From 8765da301f21eebd8914134f5eb1d1c84d99d80f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 15:37:37 +0100 Subject: [PATCH 078/366] Add deletion to Document class --- src/textual/_document.py | 41 ++++++++++++- .../test_text_area_document_delete.py | 58 +++++++++---------- .../test_text_area_document_insert.py | 1 - 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/textual/_document.py b/src/textual/_document.py index e7aae38ebc..45998437da 100644 --- a/src/textual/_document.py +++ b/src/textual/_document.py @@ -65,7 +65,46 @@ def insert_range( return end_point def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: - """Delete the text at the given range.""" + """Delete the text at the given range. + + Args: + start: A tuple (row, column) where the edit starts. + end: A tuple (row, column) where the edit ends. + + Returns: + The text that was deleted from the document. + """ + top, bottom = _fix_direction(start, end) + top_row, top_column = top + bottom_row, bottom_column = bottom + + lines = self._lines + + if top_row == bottom_row: + # The deletion range is within a single line. + line = lines[top_row] + deleted_text = line[top_column:bottom_column] + lines[top_row] = line[:top_column] + line[bottom_column:] + else: + # The deletion range spans multiple lines. + start_line = lines[top_row] + end_line = lines[bottom_row] + + deleted_text = start_line[top_column:] + "\n" + for row in range(top_row + 1, bottom_row): + deleted_text += lines[row] + "\n" + + deleted_text += end_line[:bottom_column] + if bottom_column == len(end_line): + deleted_text += "\n" + + # Update the lines at the start and end of the range + lines[top_row] = start_line[:top_column] + end_line[bottom_column:] + + # Delete the lines in between + del lines[top_row + 1 : bottom_row + 1] + + return deleted_text @property def size(self) -> Size: diff --git a/tests/text_area/test_text_area_document_delete.py b/tests/text_area/test_text_area_document_delete.py index e58674e04d..d1a926cb46 100644 --- a/tests/text_area/test_text_area_document_delete.py +++ b/tests/text_area/test_text_area_document_delete.py @@ -1,6 +1,6 @@ import pytest -from textual.widgets import TextArea +from textual._document import Document TEXT = """I must not fear. Fear is the mind-killer. @@ -9,16 +9,16 @@ @pytest.fixture -def editor(): - editor = TextArea() - editor.load_text(TEXT) - return editor +def document(): + document = Document() + document.load_text(TEXT) + return document -def test_delete_range_single_character(editor): - deleted_text = editor.delete_range((0, 0), (0, 1)) +def test_delete_range_single_character(document): + deleted_text = document.delete_range((0, 0), (0, 1)) assert deleted_text == "I" - assert editor.document_lines == [ + assert document._lines == [ " must not fear.", "Fear is the mind-killer.", "I forgot the rest of the quote.", @@ -26,22 +26,22 @@ def test_delete_range_single_character(editor): ] -def test_delete_range_single_newline(editor): +def test_delete_range_single_newline(document): """Testing deleting newline from right to left""" - deleted_text = editor.delete_range((1, 0), (0, 16)) + deleted_text = document.delete_range((1, 0), (0, 16)) assert deleted_text == "\n" - assert editor.document_lines == [ + assert document._lines == [ "I must not fear.Fear is the mind-killer.", "I forgot the rest of the quote.", "Sorry Will.", ] -def test_delete_range_single_character_end_of_document_newline(editor): +def test_delete_range_single_character_end_of_document_newline(document): """Check deleting the newline character at the end of the document""" - deleted_text = editor.delete_range((1, 0), (0, 16)) + deleted_text = document.delete_range((1, 0), (0, 16)) assert deleted_text == "\n" - assert editor.document_lines == [ + assert document._lines == [ "I must not fear.", "Fear is the mind-killer.", "I forgot the rest of the quote.", @@ -49,10 +49,10 @@ def test_delete_range_single_character_end_of_document_newline(editor): ] -def test_delete_range_multiple_characters_on_one_line(editor): - deleted_text = editor.delete_range((0, 2), (0, 7)) +def test_delete_range_multiple_characters_on_one_line(document): + deleted_text = document.delete_range((0, 2), (0, 7)) assert deleted_text == "must " - assert editor.document_lines == [ + assert document._lines == [ "I not fear.", "Fear is the mind-killer.", "I forgot the rest of the quote.", @@ -60,32 +60,32 @@ def test_delete_range_multiple_characters_on_one_line(editor): ] -def test_delete_range_multiple_lines_partially_spanned(editor): +def test_delete_range_multiple_lines_partially_spanned(document): """Deleting a selection that partially spans the first and final lines of the selection.""" - deleted_text = editor.delete_range((0, 2), (2, 2)) + deleted_text = document.delete_range((0, 2), (2, 2)) assert deleted_text == "must not fear.\nFear is the mind-killer.\nI " - assert editor.document_lines == [ + assert document._lines == [ "I forgot the rest of the quote.", "Sorry Will.", ] -def test_delete_range_end_of_line(editor): +def test_delete_range_end_of_line(document): """Testing deleting newline from left to right""" - deleted_text = editor.delete_range((0, 16), (1, 0)) + deleted_text = document.delete_range((0, 16), (1, 0)) assert deleted_text == "\n" - assert editor.document_lines == [ + assert document._lines == [ "I must not fear.Fear is the mind-killer.", "I forgot the rest of the quote.", "Sorry Will.", ] -def test_delete_range_single_line_excluding_newline(editor): +def test_delete_range_single_line_excluding_newline(document): """Delete from the start to the end of the line.""" - deleted_text = editor.delete_range((2, 0), (2, 31)) + deleted_text = document.delete_range((2, 0), (2, 31)) assert deleted_text == "I forgot the rest of the quote." - assert editor.document_lines == [ + assert document._lines == [ "I must not fear.", "Fear is the mind-killer.", "", @@ -93,11 +93,11 @@ def test_delete_range_single_line_excluding_newline(editor): ] -def test_delete_range_single_line_including_newline(editor): +def test_delete_range_single_line_including_newline(document): """Delete from the start of a line to the start of the line below.""" - deleted_text = editor.delete_range((2, 0), (3, 0)) + deleted_text = document.delete_range((2, 0), (3, 0)) assert deleted_text == "I forgot the rest of the quote.\n" - assert editor.document_lines == [ + assert document._lines == [ "I must not fear.", "Fear is the mind-killer.", "Sorry Will.", diff --git a/tests/text_area/test_text_area_document_insert.py b/tests/text_area/test_text_area_document_insert.py index 3ca887205a..8eb87b8fe4 100644 --- a/tests/text_area/test_text_area_document_insert.py +++ b/tests/text_area/test_text_area_document_insert.py @@ -1,7 +1,6 @@ import pytest from textual._document import Document -from textual.widgets import TextArea TEXT = """I must not fear. Fear is the mind-killer.""" From 0b81fda00de2ebb2bf6a5cd8f311c10b8a8db103 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 15:38:54 +0100 Subject: [PATCH 079/366] Test renaming --- .../text_area/test_text_area_document_insert.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/text_area/test_text_area_document_insert.py b/tests/text_area/test_text_area_document_insert.py index 8eb87b8fe4..7cccc8e495 100644 --- a/tests/text_area/test_text_area_document_insert.py +++ b/tests/text_area/test_text_area_document_insert.py @@ -6,7 +6,7 @@ Fear is the mind-killer.""" -def test_insert_text_no_newlines(): +def test_insert_no_newlines(): document = Document() document.load_text(TEXT) document.insert_range((0, 1), (0, 1), " really") @@ -16,7 +16,7 @@ def test_insert_text_no_newlines(): ] -def test_insert_text_empty_string(): +def test_insert_empty_string(): document = Document() document.load_text(TEXT) document.insert_range((0, 1), (0, 1), "") @@ -24,7 +24,7 @@ def test_insert_text_empty_string(): @pytest.mark.xfail(reason="undecided on behaviour") -def test_insert_text_invalid_column(): +def test_insert_invalid_column(): # TODO - what is the correct behaviour here? # right now it appends to the end of the line if the column is too large. document = Document() @@ -34,7 +34,7 @@ def test_insert_text_invalid_column(): @pytest.mark.xfail(reason="undecided on behaviour") -def test_insert_text_invalid_row(): +def test_insert_invalid_row(): # TODO - this raises an IndexError for list index out of range document = Document() document.load_text(TEXT) @@ -42,21 +42,21 @@ def test_insert_text_invalid_row(): assert document._lines == ["I must not fear.", "Fear is the mind-killer."] -def test_insert_text_range_newline_file_start(): +def test_insert_range_newline_file_start(): document = Document() document.load_text(TEXT) document.insert_range((0, 0), (0, 0), "\n") assert document._lines == ["", "I must not fear.", "Fear is the mind-killer."] -def test_insert_text_newline_splits_line(): +def test_insert_newline_splits_line(): document = Document() document.load_text(TEXT) document.insert_range((0, 1), (0, 1), "\n") assert document._lines == ["I", " must not fear.", "Fear is the mind-killer."] -def test_insert_text_multiple_lines_ends_with_newline(): +def test_insert_multiple_lines_ends_with_newline(): document = Document() document.load_text(TEXT) document.insert_range((0, 1), (0, 1), "Hello,\nworld!\n") @@ -68,7 +68,7 @@ def test_insert_text_multiple_lines_ends_with_newline(): ] -def test_insert_text_multiple_lines_starts_with_newline(): +def test_insert_multiple_lines_starts_with_newline(): document = Document() document.load_text(TEXT) document.insert_range((0, 1), (0, 1), "\nHello,\nworld!\n") From c6aab449781775ec15bbc01dfac5f63869e269b4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 16:00:09 +0100 Subject: [PATCH 080/366] Byte offset calculation fix --- src/textual/widgets/_text_area.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 1df7937861..9cd8e1a9f7 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -932,8 +932,8 @@ def _position_to_byte_offset(self, position: tuple[int, int]) -> int: lines = self.document_lines row, column = position lines_above = lines[:row] - bytes_lines_above = sum(len(line) + 1 for line in lines_above) - bytes_this_line_left_of_cursor = len(lines[row][:column]) + bytes_lines_above = sum(len(line.encode("utf-8")) + 1 for line in lines_above) + bytes_this_line_left_of_cursor = len(lines[row][:column].encode("utf-8")) return bytes_lines_above + bytes_this_line_left_of_cursor def dedent_line(self) -> None: From 826483cebae185dc34be0b72fd655e0800cf5cc9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 16:20:22 +0100 Subject: [PATCH 081/366] Remove debugging --- src/textual/widgets/_text_area.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 9cd8e1a9f7..56aeac992c 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -568,7 +568,6 @@ def _on_mouse_move(self, event: events.MouseMove) -> None: target = self.get_target_document_location(offset) selection_start, _ = self.selection self.selection = Selection(selection_start, target) - log.debug(f"selection updated {self.selection!r}") def _on_mouse_up(self, event: events.MouseUp) -> None: event.stop() From 48c89836bcfa9c81347639cbbb6e331587318f88 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 16:21:58 +0100 Subject: [PATCH 082/366] Fixing a type hint --- src/textual/widgets/_text_area.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 56aeac992c..4f793ebef2 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -125,7 +125,7 @@ class Delete: cursor_destination: tuple[int, int] | None = None """Where to move the cursor to after the deletion.""" - def do(self, editor: TextArea) -> None: + def do(self, editor: TextArea) -> str: """Do the action.""" self.deleted_text = editor._delete_range( self.from_position, self.to_position, self.cursor_destination From abcd52cac57a054ab521aac8054d567101e6b08d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 17:00:11 +0100 Subject: [PATCH 083/366] Begin migrating TextArea to use Document --- src/textual/_document.py | 12 +- src/textual/widgets/_text_area.py | 434 +++++++++--------- .../test_text_area_document_delete.py | 20 +- 3 files changed, 241 insertions(+), 225 deletions(-) diff --git a/src/textual/_document.py b/src/textual/_document.py index 45998437da..9f8eace138 100644 --- a/src/textual/_document.py +++ b/src/textual/_document.py @@ -5,6 +5,8 @@ from textual._cells import cell_len from textual._types import SupportsIndex from textual.geometry import Size + +# TODO - probably need to move _fix_direction either to this file or a standalone file. from textual.widgets._text_area import _fix_direction @@ -12,6 +14,10 @@ class Document: def __init__(self) -> None: self._lines: list[str] = [] + @property + def lines(self) -> list[str]: + return self._lines + def load_text(self, text: str) -> None: """Load text from a string into the document. @@ -116,9 +122,6 @@ def size(self) -> Size: # can rest at the end of a line where there isn't yet any character. # Similarly, the cursor can rest below the bottom line of text, where # a line doesn't currently exist. - - # TODO: This was previously `Size(text_width + 1, height)` to leave space - # for the cursor. However, that is a widget level concern. return Size(text_width, height) @property @@ -128,6 +131,9 @@ def line_count(self) -> int: def get_line(self, index: int) -> Text: """Returns the line with the given index from the document""" + line_string = self[index] + line_string = line_string.replace("\n", "").replace("\r", "") + return Text(line_string, end="", tab_size=4) def __getitem__(self, item: SupportsIndex | slice) -> str: return self._lines[item] diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 4f793ebef2..a13cfb6c9f 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -13,6 +13,7 @@ from textual import events, log from textual._cells import cell_len +from textual._document import Document from textual._types import Protocol, runtime_checkable from textual.binding import Binding from textual.geometry import Offset, Region, Size, Spacing, clamp @@ -103,10 +104,10 @@ class Insert(NamedTuple): move_cursor: bool = True def do(self, editor: TextArea) -> None: - if self.text: - editor._insert_text_range( - self.text, self.from_position, self.to_position, self.move_cursor - ) + text, start, end, move_cursor = self + edit_end = editor.document.insert_range(start, end, text) + if move_cursor: + editor.selection = Selection.cursor(edit_end) def undo(self, editor: TextArea) -> None: """Undo the action.""" @@ -144,16 +145,16 @@ def __rich_repr__(self): class TextArea(ScrollView, can_focus=True): DEFAULT_CSS = """\ -$editor-active-line-bg: white 8%; +$text-area-active-line-bg: white 8%; TextArea { background: $panel; } TextArea > .text-area--active-line { - background: $editor-active-line-bg; + background: $text-area-active-line-bg; } TextArea > .text-area--active-line-gutter { color: $text; - background: $editor-active-line-bg; + background: $text-area-active-line-bg; } TextArea > .text-area--gutter { color: $text-muted 40%; @@ -227,6 +228,10 @@ def __init__( super().__init__(name=name, id=id, classes=classes, disabled=disabled) # --- Core editor data + self.document = Document() + + # TODO - currently migrating the document_lines over to use Document. + self.document_lines: list[str] = [] """Each string in this list represents a line in the document. Includes new line characters.""" @@ -294,7 +299,7 @@ def _build_ast( def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> str: row, column = point - lines = self.document_lines + lines = self.document.lines row_out_of_bounds = row >= len(lines) column_out_of_bounds = not row_out_of_bounds and column > len(lines[row]) if row_out_of_bounds or column_out_of_bounds: @@ -343,27 +348,34 @@ def _get_document_size(self, document_lines: list[str]) -> Size: return Size(text_width + 1, height) def _refresh_size(self) -> None: - self._document_size = self._get_document_size(self.document_lines) + width, height = self.document.size + # +1 to reserve cursor end-of-line resting space. + self._document_size = Size(width + 1, height) def render_line(self, widget_y: int) -> Strip: - document_lines = self.document_lines + document = self.document + + # TODO - we should ask the document itself for the content of the line + # for the given line number. - document_y = round(self.scroll_y + widget_y) - out_of_bounds = document_y >= len(document_lines) + line_index = round(self.scroll_y + widget_y) + out_of_bounds = line_index >= document.line_count if out_of_bounds: return Strip.blank(self.size.width) - line_string = document_lines[document_y].replace("\n", "").replace("\r", "") - line_text = Text(f"{line_string} ", end="", tab_size=4) - line_text.set_length(self.virtual_size.width) + line = document.get_line(line_index) + line_length = len(line) + line.set_length(self.virtual_size.width) + # TODO - probably remove this highlighting code from here, we can + # do it in our syntax aware Document subclass. # Apply highlighting null_style = Style.null() if self._highlights: - highlights = self._highlights[document_y] + highlights = self._highlights[line_index] for start, end, highlight_name in highlights: node_style = HIGHLIGHT_STYLES.get(highlight_name, null_style) - line_text.stylize(node_style, start, end) + line.stylize(node_style, start, end) start, end = self.selection end_row, end_column = end @@ -378,40 +390,38 @@ def render_line(self, widget_y: int) -> Strip: selection_top_row, selection_top_column = selection_top selection_bottom_row, selection_bottom_column = selection_bottom - if start != end and selection_top_row <= document_y <= selection_bottom_row: + if start != end and selection_top_row <= line_index <= selection_bottom_row: # If this row is part of the selection - if document_y == selection_top_row == selection_bottom_row: + if line_index == selection_top_row == selection_bottom_row: # Selection within a single line - line_text.stylize_before( + line.stylize_before( selection_style, start=selection_top_column, end=selection_bottom_column, ) else: # Selection spanning multiple lines - if document_y == selection_top_row: - line_text.stylize_before( + if line_index == selection_top_row: + line.stylize_before( selection_style, start=selection_top_column, - end=len(line_string), - ) - elif document_y == selection_bottom_row: - line_text.stylize_before( - selection_style, end=selection_bottom_column + end=line_length, ) + elif line_index == selection_bottom_row: + line.stylize_before(selection_style, end=selection_bottom_column) else: - line_text.stylize_before(selection_style, end=len(line_string)) + line.stylize_before(selection_style, end=line_length) # Show the cursor and the selection - if end_row == document_y: + if end_row == line_index: cursor_style = self.get_component_rich_style("text-area--cursor") - line_text.stylize(cursor_style, end_column, end_column + 1) + line.stylize(cursor_style, end_column, end_column + 1) active_line_style = self.get_component_rich_style("text-area--active-line") - line_text.stylize_before(active_line_style) + line.stylize_before(active_line_style) # Show the gutter if self.show_line_numbers: - if end_row == document_y: + if end_row == line_index: gutter_style = self.get_component_rich_style( "text-area--active-line-gutter" ) @@ -420,7 +430,7 @@ def render_line(self, widget_y: int) -> Strip: gutter_width_no_margin = self.gutter_width - 2 gutter = Text( - f"{document_y + 1:>{gutter_width_no_margin}} ", + f"{line_index + 1:>{gutter_width_no_margin}} ", style=gutter_style, end="", ) @@ -429,7 +439,7 @@ def render_line(self, widget_y: int) -> Strip: gutter_segments = self.app.console.render(gutter) text_segments = self.app.console.render( - line_text, self.app.console.options.update_width(self.virtual_size.width) + line, self.app.console.options.update_width(self.virtual_size.width) ) virtual_width, virtual_height = self.virtual_size @@ -448,7 +458,7 @@ def gutter_width(self) -> int: # The longest number in the gutter plus two extra characters: `│ `. gutter_margin = 2 gutter_longest_number = ( - len(str(len(self.document_lines) + 1)) + gutter_margin + len(str(self.document.line_count + 1)) + gutter_margin if self.show_line_numbers else 0 ) @@ -547,7 +557,7 @@ def get_target_document_location(self, offset: Offset) -> tuple[int, int]: target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0) target_row = clamp( - offset.y + int(self.scroll_y), 0, len(self.document_lines) - 1 + offset.y + int(self.scroll_y), 0, self.document.line_count - 1 ) target_column = self.cell_width_to_column_index(target_x, target_row) @@ -583,7 +593,7 @@ def _on_paste(self, event: events.Paste) -> None: def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: """Return the column that the cell width corresponds to on the given row.""" total_cell_offset = 0 - line = self.document_lines[row_index] + line = self.document[row_index] for column_index, character in enumerate(line): total_cell_offset += cell_len(character) if total_cell_offset >= cell_width + 1: @@ -613,7 +623,7 @@ def cursor_at_first_row(self) -> bool: @property def cursor_at_last_row(self) -> bool: - return self.selection.end[0] == len(self.document_lines) - 1 + return self.selection.end[0] == self.document.line_count - 1 @property def cursor_at_start_of_row(self) -> bool: @@ -622,7 +632,7 @@ def cursor_at_start_of_row(self) -> bool: @property def cursor_at_end_of_row(self) -> bool: cursor_row, cursor_column = self.selection.end - row_length = len(self.document_lines[cursor_row]) + row_length = len(self.document[cursor_row]) cursor_at_end = cursor_column == row_length return cursor_at_end @@ -644,7 +654,6 @@ def cursor_to_line_end(self, select: bool = False) -> None: start, end = self.selection cursor_row, cursor_column = end - target_column = len(self.document_lines[cursor_row]) if select: self.selection = Selection(start, target_column) @@ -693,7 +702,7 @@ def get_cursor_left_position(self) -> tuple[int, int]: if self.cursor_at_start_of_document: return 0, 0 cursor_row, cursor_column = self.selection.end - length_of_row_above = len(self.document_lines[cursor_row - 1]) + length_of_row_above = len(self.document[cursor_row - 1]) target_row = cursor_row if cursor_column != 0 else cursor_row - 1 target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above return target_row, target_column @@ -741,14 +750,14 @@ def get_cursor_down_position(self): """Get the position the cursor will move to if it moves down.""" cursor_row, cursor_column = self.selection.end if self.cursor_at_last_row: - return cursor_row, len(self.document_lines[cursor_row]) + return cursor_row, len(self.document[cursor_row]) - target_row = min(len(self.document_lines) - 1, cursor_row + 1) + target_row = min(self.document.line_count - 1, cursor_row + 1) # Attempt to snap last intentional cell length target_column = self.cell_width_to_column_index( self._last_intentional_cell_width, target_row ) - target_column = clamp(target_column, 0, len(self.document_lines[target_row])) + target_column = clamp(target_column, 0, len(self.document[target_row])) return target_row, target_column def action_cursor_up(self) -> None: @@ -772,7 +781,7 @@ def get_cursor_up_position(self) -> tuple[int, int]: target_column = self.cell_width_to_column_index( self._last_intentional_cell_width, target_row ) - target_column = clamp(target_column, 0, len(self.document_lines[target_row])) + target_column = clamp(target_column, 0, len(self.document[target_row])) return target_row, target_column def action_cursor_line_end(self) -> None: @@ -792,7 +801,7 @@ def action_cursor_left_word(self) -> None: cursor_row, cursor_column = self.selection.end # Check the current line for a word boundary - line = self.document_lines[cursor_row][:cursor_column] + line = self.document[cursor_row][:cursor_column] matches = list(re.finditer(self._word_pattern, line)) if matches: @@ -801,7 +810,7 @@ def action_cursor_left_word(self) -> None: elif cursor_row > 0: # If no word boundary is found and we're not on the first line, move to the end of the previous line cursor_row -= 1 - cursor_column = len(self.document_lines[cursor_row]) + cursor_column = len(self.document[cursor_row]) else: # If we're already on the first line and no word boundary is found, move to the start of the line cursor_column = 0 @@ -818,19 +827,19 @@ def action_cursor_right_word(self) -> None: cursor_row, cursor_column = self.selection.end # Check the current line for a word boundary - line = self.document_lines[cursor_row][cursor_column:] + line = self.document[cursor_row][cursor_column:] matches = list(re.finditer(self._word_pattern, line)) if matches: # If a word boundary is found, move the cursor there cursor_column += matches[0].end() - elif cursor_row < len(self.document_lines) - 1: + elif cursor_row < self.document.line_count - 1: # If no word boundary is found and we're not on the last line, move to the start of the next line cursor_row += 1 cursor_column = 0 else: # If we're already on the last line and no word boundary is found, move to the end of the line - cursor_column = len(self.document_lines[cursor_row]) + cursor_column = len(self.document[cursor_row]) self.selection = Selection.cursor((cursor_row, cursor_column)) self._record_last_intentional_cell_width() @@ -838,13 +847,13 @@ def action_cursor_right_word(self) -> None: @property def active_line_text(self) -> str: # TODO - consider empty documents - return self.document_lines[self.selection.end[0]] + return self.document[self.selection.end[0]] def get_column_cell_width(self, row: int, column: int) -> int: """Given a row and column index within the editor, return the cell offset of the column from the start of the row (the left edge of the editor content area). """ - line = self.document_lines[row] + line = self.document[row] return cell_len(line[:column]) def _record_last_intentional_cell_width(self) -> None: @@ -867,73 +876,73 @@ def insert_text_range( ): self.edit(Insert(text, from_position, to_position, move_cursor)) - def _insert_text_range( - self, - text: str, - from_position: tuple[int, int], - to_position: tuple[int, int], - move_cursor: bool = True, - ) -> None: - """Insert text at a given range and move the cursor to the end of the inserted text.""" - - inserted_text = text - lines = self.document_lines - - from_row, from_column = from_position - to_row, to_column = to_position - - if from_position > to_position: - from_row, from_column, to_row, to_column = ( - to_row, - to_column, - from_row, - from_column, - ) - - insert_lines = inserted_text.splitlines() - if inserted_text.endswith("\n"): - # Special case where a single newline character is inserted. - insert_lines.append("") - - before_selection = lines[from_row][:from_column] - after_selection = lines[to_row][to_column:] - - insert_lines[0] = before_selection + insert_lines[0] - destination_column = len(insert_lines[-1]) - insert_lines[-1] = insert_lines[-1] + after_selection - lines[from_row : to_row + 1] = insert_lines - destination_row = from_row + len(insert_lines) - 1 - - cursor_destination = (destination_row, destination_column) - - start_byte = self._position_to_byte_offset(from_position) - if self._syntax_tree is not None: - self._syntax_tree.edit( - start_byte=start_byte, - old_end_byte=self._position_to_byte_offset(to_position), - new_end_byte=start_byte + len(inserted_text), - start_point=from_position, - old_end_point=to_position, - new_end_point=cursor_destination, - ) - self._syntax_tree = self._parser.parse( - self._read_callable, self._syntax_tree - ) - self._prepare_highlights() - self._refresh_size() - if move_cursor: - self.selection = Selection.cursor(cursor_destination) - - def _position_to_byte_offset(self, position: tuple[int, int]) -> int: - """Given a document coordinate, return the byte offset of that coordinate.""" - - # TODO - this assumes all line endings are a single byte `\n` - lines = self.document_lines - row, column = position - lines_above = lines[:row] - bytes_lines_above = sum(len(line.encode("utf-8")) + 1 for line in lines_above) - bytes_this_line_left_of_cursor = len(lines[row][:column].encode("utf-8")) - return bytes_lines_above + bytes_this_line_left_of_cursor + # def _insert_text_range( + # self, + # text: str, + # from_position: tuple[int, int], + # to_position: tuple[int, int], + # move_cursor: bool = True, + # ) -> None: + # """Insert text at a given range and move the cursor to the end of the inserted text.""" + # + # inserted_text = text + # lines = self.document_lines + # + # from_row, from_column = from_position + # to_row, to_column = to_position + # + # if from_position > to_position: + # from_row, from_column, to_row, to_column = ( + # to_row, + # to_column, + # from_row, + # from_column, + # ) + # + # insert_lines = inserted_text.splitlines() + # if inserted_text.endswith("\n"): + # # Special case where a single newline character is inserted. + # insert_lines.append("") + # + # before_selection = lines[from_row][:from_column] + # after_selection = lines[to_row][to_column:] + # + # insert_lines[0] = before_selection + insert_lines[0] + # destination_column = len(insert_lines[-1]) + # insert_lines[-1] = insert_lines[-1] + after_selection + # lines[from_row : to_row + 1] = insert_lines + # destination_row = from_row + len(insert_lines) - 1 + # + # cursor_destination = (destination_row, destination_column) + # + # start_byte = self._position_to_byte_offset(from_position) + # if self._syntax_tree is not None: + # self._syntax_tree.edit( + # start_byte=start_byte, + # old_end_byte=self._position_to_byte_offset(to_position), + # new_end_byte=start_byte + len(inserted_text), + # start_point=from_position, + # old_end_point=to_position, + # new_end_point=cursor_destination, + # ) + # self._syntax_tree = self._parser.parse( + # self._read_callable, self._syntax_tree + # ) + # self._prepare_highlights() + # self._refresh_size() + # if move_cursor: + # self.selection = Selection.cursor(cursor_destination) + + # def _position_to_byte_offset(self, position: tuple[int, int]) -> int: + # """Given a document coordinate, return the byte offset of that coordinate.""" + # + # # TODO - this assumes all line endings are a single byte `\n` + # lines = self.document_lines + # row, column = position + # lines_above = lines[:row] + # bytes_lines_above = sum(len(line.encode("utf-8")) + 1 for line in lines_above) + # bytes_this_line_left_of_cursor = len(lines[row][:column].encode("utf-8")) + # return bytes_lines_above + bytes_this_line_left_of_cursor def dedent_line(self) -> None: """Reduces the indentation of the current line by one level. @@ -941,23 +950,25 @@ def dedent_line(self) -> None: A dedent is simply a Delete operation on some amount of whitespace which may exist at the start of a line. """ - cursor_row, cursor_column = self.selection.end - - # Define one level of indentation as four spaces - indent_level = " " * 4 - - current_line = self.document_lines[cursor_row] - - # If the line is indented, reduce the indentation - # TODO - if the line is less than the indent level we should just dedent as far as possible. - if current_line.startswith(indent_level): - self.document_lines[cursor_row] = current_line[len(indent_level) :] - - if cursor_column > len(current_line): - self.selection = Selection.cursor((cursor_row, len(current_line))) - - self._refresh_size() - self.refresh() + # cursor_row, cursor_column = self.selection.end + # + # # Define one level of indentation as four spaces + # indent_level = " " * 4 + # + # current_line = self.document[cursor_row] + # + # # If the line is indented, reduce the indentation + # # TODO - if the line is less than the indent level we should just dedent as far as possible. + # if current_line.startswith(indent_level): + # self.document_lines[cursor_row] = current_line[len(indent_level) :] + # + # if cursor_column > len(current_line): + # self.selection = Selection.cursor((cursor_row, len(current_line))) + # + # self._refresh_size() + # self.refresh() + # TODO - reimplement with new Document API + pass def delete_range( self, @@ -966,77 +977,76 @@ def delete_range( cursor_destination: tuple[int, int] | None = None, ) -> str: top, bottom = _fix_direction(from_position, to_position) - print(f"top and bottom: {top, bottom}") return self.edit(Delete(top, bottom, cursor_destination)) - def _delete_range( - self, - from_position: tuple[int, int], - to_position: tuple[int, int], - cursor_destination: tuple[int, int] | None, - ) -> str: - """Delete text between `from_position` and `to_position`. - - `from_position` is inclusive. The `to_position` is exclusive. - - Returns: - A string containing the deleted text. - """ - from_row, from_column = from_position - to_row, to_column = to_position - - start_byte = self._position_to_byte_offset(from_position) - old_end_byte = self._position_to_byte_offset(to_position) - - lines = self.document_lines - - # If the range is within a single line - if from_row == to_row: - line = lines[from_row] - deleted_text = line[from_column:to_column] - lines[from_row] = line[:from_column] + line[to_column:] - else: - # The range spans multiple lines - start_line = lines[from_row] - end_line = lines[to_row] - - deleted_text = start_line[from_column:] + "\n" - for row in range(from_row + 1, to_row): - deleted_text += lines[row] + "\n" - - deleted_text += end_line[:to_column] - if to_column == len(end_line): - deleted_text += "\n" - - # Update the lines at the start and end of the range - lines[from_row] = start_line[:from_column] + end_line[to_column:] - - # Delete the lines in between - del lines[from_row + 1 : to_row + 1] - - if self._syntax_tree is not None: - self._syntax_tree.edit( - start_byte=start_byte, - old_end_byte=old_end_byte, - new_end_byte=old_end_byte - len(deleted_text), - start_point=from_position, - old_end_point=to_position, - new_end_point=from_position, - ) - self._syntax_tree = self._parser.parse( - self._read_callable, self._syntax_tree - ) - self._prepare_highlights() - - self._refresh_size() - - if cursor_destination is not None: - self.selection = Selection.cursor(cursor_destination) - else: - # Move the cursor to the start of the deleted range - self.selection = Selection.cursor((from_row, from_column)) - - return deleted_text + # def _delete_range( + # self, + # from_position: tuple[int, int], + # to_position: tuple[int, int], + # cursor_destination: tuple[int, int] | None, + # ) -> str: + # """Delete text between `from_position` and `to_position`. + # + # `from_position` is inclusive. The `to_position` is exclusive. + # + # Returns: + # A string containing the deleted text. + # """ + # from_row, from_column = from_position + # to_row, to_column = to_position + # + # start_byte = self._position_to_byte_offset(from_position) + # old_end_byte = self._position_to_byte_offset(to_position) + # + # lines = self.document_lines + # + # # If the range is within a single line + # if from_row == to_row: + # line = lines[from_row] + # deleted_text = line[from_column:to_column] + # lines[from_row] = line[:from_column] + line[to_column:] + # else: + # # The range spans multiple lines + # start_line = lines[from_row] + # end_line = lines[to_row] + # + # deleted_text = start_line[from_column:] + "\n" + # for row in range(from_row + 1, to_row): + # deleted_text += lines[row] + "\n" + # + # deleted_text += end_line[:to_column] + # if to_column == len(end_line): + # deleted_text += "\n" + # + # # Update the lines at the start and end of the range + # lines[from_row] = start_line[:from_column] + end_line[to_column:] + # + # # Delete the lines in between + # del lines[from_row + 1 : to_row + 1] + # + # if self._syntax_tree is not None: + # self._syntax_tree.edit( + # start_byte=start_byte, + # old_end_byte=old_end_byte, + # new_end_byte=old_end_byte - len(deleted_text), + # start_point=from_position, + # old_end_point=to_position, + # new_end_point=from_position, + # ) + # self._syntax_tree = self._parser.parse( + # self._read_callable, self._syntax_tree + # ) + # self._prepare_highlights() + # + # self._refresh_size() + # + # if cursor_destination is not None: + # self.selection = Selection.cursor(cursor_destination) + # else: + # # Move the cursor to the start of the deleted range + # self.selection = Selection.cursor((from_row, from_column)) + # + # return deleted_text def action_delete_left(self) -> None: """Deletes the character to the left of the cursor and updates the cursor position.""" @@ -1052,7 +1062,7 @@ def action_delete_left(self) -> None: if empty: if self.cursor_at_start_of_row: - end = (end_row - 1, len(self.document_lines[end_row - 1])) + end = (end_row - 1, len(self.document[end_row - 1])) else: end = (end_row, end_column - 1) @@ -1095,7 +1105,7 @@ def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor position to the end of the line.""" from_position = self.selection.end cursor_row, cursor_column = from_position - to_position = (cursor_row, len(self.document_lines[cursor_row])) + to_position = (cursor_row, len(self.document[cursor_row])) self.delete_range(from_position, to_position) def action_delete_word_left(self) -> None: @@ -1112,7 +1122,7 @@ def action_delete_word_left(self) -> None: cursor_row, cursor_column = end # Check the current line for a word boundary - line = self.document_lines[cursor_row][:cursor_column] + line = self.document[cursor_row][:cursor_column] matches = list(re.finditer(self._word_pattern, line)) if matches: @@ -1120,7 +1130,7 @@ def action_delete_word_left(self) -> None: from_position = (cursor_row, matches[-1].start()) elif cursor_row > 0: # If no word boundary is found, and we're not on the first line, delete to the end of the previous line - from_position = (cursor_row - 1, len(self.document_lines[cursor_row - 1])) + from_position = (cursor_row - 1, len(self.document[cursor_row - 1])) else: # If we're already on the first line and no word boundary is found, delete to the start of the line from_position = (cursor_row, 0) @@ -1139,18 +1149,18 @@ def action_delete_word_right(self) -> None: cursor_row, cursor_column = end # Check the current line for a word boundary - line = self.document_lines[cursor_row][cursor_column:] + line = self.document[cursor_row][cursor_column:] matches = list(re.finditer(self._word_pattern, line)) if matches: # If a word boundary is found, delete the word to_position = (cursor_row, cursor_column + matches[0].end()) - elif cursor_row < len(self.document_lines) - 1: + elif cursor_row < self.document.line_count - 1: # If no word boundary is found, and we're not on the last line, delete to the start of the next line to_position = (cursor_row + 1, 0) else: # If we're already on the last line and no word boundary is found, delete to the end of the line - to_position = (cursor_row, len(self.document_lines[cursor_row])) + to_position = (cursor_row, len(self.document[cursor_row])) self.delete_range(end, to_position) diff --git a/tests/text_area/test_text_area_document_delete.py b/tests/text_area/test_text_area_document_delete.py index d1a926cb46..78e948abf2 100644 --- a/tests/text_area/test_text_area_document_delete.py +++ b/tests/text_area/test_text_area_document_delete.py @@ -15,7 +15,7 @@ def document(): return document -def test_delete_range_single_character(document): +def test_delete_single_character(document): deleted_text = document.delete_range((0, 0), (0, 1)) assert deleted_text == "I" assert document._lines == [ @@ -26,7 +26,7 @@ def test_delete_range_single_character(document): ] -def test_delete_range_single_newline(document): +def test_delete_single_newline(document): """Testing deleting newline from right to left""" deleted_text = document.delete_range((1, 0), (0, 16)) assert deleted_text == "\n" @@ -37,7 +37,7 @@ def test_delete_range_single_newline(document): ] -def test_delete_range_single_character_end_of_document_newline(document): +def test_delete_single_character_end_of_document_newline(document): """Check deleting the newline character at the end of the document""" deleted_text = document.delete_range((1, 0), (0, 16)) assert deleted_text == "\n" @@ -49,7 +49,7 @@ def test_delete_range_single_character_end_of_document_newline(document): ] -def test_delete_range_multiple_characters_on_one_line(document): +def test_delete_multiple_characters_on_one_line(document): deleted_text = document.delete_range((0, 2), (0, 7)) assert deleted_text == "must " assert document._lines == [ @@ -60,7 +60,7 @@ def test_delete_range_multiple_characters_on_one_line(document): ] -def test_delete_range_multiple_lines_partially_spanned(document): +def test_delete_multiple_lines_partially_spanned(document): """Deleting a selection that partially spans the first and final lines of the selection.""" deleted_text = document.delete_range((0, 2), (2, 2)) assert deleted_text == "must not fear.\nFear is the mind-killer.\nI " @@ -70,7 +70,7 @@ def test_delete_range_multiple_lines_partially_spanned(document): ] -def test_delete_range_end_of_line(document): +def test_delete_end_of_line(document): """Testing deleting newline from left to right""" deleted_text = document.delete_range((0, 16), (1, 0)) assert deleted_text == "\n" @@ -81,7 +81,7 @@ def test_delete_range_end_of_line(document): ] -def test_delete_range_single_line_excluding_newline(document): +def test_delete_single_line_excluding_newline(document): """Delete from the start to the end of the line.""" deleted_text = document.delete_range((2, 0), (2, 31)) assert deleted_text == "I forgot the rest of the quote." @@ -93,7 +93,7 @@ def test_delete_range_single_line_excluding_newline(document): ] -def test_delete_range_single_line_including_newline(document): +def test_delete_single_line_including_newline(document): """Delete from the start of a line to the start of the line below.""" deleted_text = document.delete_range((2, 0), (3, 0)) assert deleted_text == "I forgot the rest of the quote.\n" @@ -104,11 +104,11 @@ def test_delete_range_single_line_including_newline(document): ] -def test_delete_range_single_character_start_of_document(): +def test_delete_single_character_start_of_document(): """Check deletion of the first character in the document""" pass -def test_delete_range_single_character_end_of_document_newline(): +def test_delete_single_character_end_of_document_newline(): """Check deleting the newline character at the end of the document""" pass From a4dcdc0f1c67eb5657b14837bf0117ee246a7fac Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 24 Jul 2023 17:08:47 +0100 Subject: [PATCH 084/366] Getting inserting working in with the Document --- src/textual/_document.py | 6 +++--- src/textual/_fix_direction.py | 11 +++++++++++ src/textual/widgets/_text_area.py | 30 +++++++++++++++--------------- 3 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 src/textual/_fix_direction.py diff --git a/src/textual/_document.py b/src/textual/_document.py index 9f8eace138..49e1f1e1f5 100644 --- a/src/textual/_document.py +++ b/src/textual/_document.py @@ -3,11 +3,11 @@ from rich.text import Text from textual._cells import cell_len -from textual._types import SupportsIndex -from textual.geometry import Size # TODO - probably need to move _fix_direction either to this file or a standalone file. -from textual.widgets._text_area import _fix_direction +from textual._fix_direction import _fix_direction +from textual._types import SupportsIndex +from textual.geometry import Size class Document: diff --git a/src/textual/_fix_direction.py b/src/textual/_fix_direction.py new file mode 100644 index 0000000000..0c2da806f6 --- /dev/null +++ b/src/textual/_fix_direction.py @@ -0,0 +1,11 @@ +from __future__ import annotations + + +def _fix_direction( + start: tuple[int, int], end: tuple[int, int] +) -> tuple[tuple[int, int], tuple[int, int]]: + """Given a range, return a new range (x, y) such + that x <= y which covers the same characters.""" + if start > end: + return end, start + return start, end diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index a13cfb6c9f..b5b8ea57fe 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -14,6 +14,7 @@ from textual import events, log from textual._cells import cell_len from textual._document import Document +from textual._fix_direction import _fix_direction from textual._types import Protocol, runtime_checkable from textual.binding import Binding from textual.geometry import Offset, Region, Size, Spacing, clamp @@ -312,16 +313,20 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> str: return return_value def load_text(self, text: str) -> None: - """Load text from a string into the editor.""" - lines = text.splitlines(keepends=False) - if text[-1] == "\n": - lines.append("") - self.load_lines(lines) + """Load text from a string into the editor. + + Args: + text: The text to load into the editor. + """ + self.document.load_text(text) + self._refresh_size() def load_lines(self, lines: list[str]) -> None: """Load text from a list of lines into the editor. This will replace any previously loaded lines.""" + + # TODO - migrate the highlighting stuff out of here into the Document subclass self.document_lines = lines self._document_size = self._get_document_size(lines) if self._parser is not None: @@ -519,6 +524,10 @@ def _prepare_highlights( def edit(self, edit: Edit) -> object | None: log.debug(f"performing edit {edit!r}") result = edit.do(self) + + # After edits we need to refresh + self._refresh_size() + self._undo_stack.append(edit) # TODO: Think about this... @@ -654,6 +663,7 @@ def cursor_to_line_end(self, select: bool = False) -> None: start, end = self.selection cursor_row, cursor_column = end + target_column = len(self.document[cursor_row]) if select: self.selection = Selection(start, target_column) @@ -1204,16 +1214,6 @@ def debug_state(self) -> "EditorDebug": ) -def _fix_direction( - start: tuple[int, int], end: tuple[int, int] -) -> tuple[tuple[int, int], tuple[int, int]]: - """Given a range, return a new range (x, y) such - that x <= y which covers the same characters.""" - if start > end: - return end, start - return start, end - - def traverse_tree(cursor): reached_root = False while reached_root == False: From 9276673c9fe781a52923cc1195979723d06e3e27 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 25 Jul 2023 09:58:39 +0100 Subject: [PATCH 085/366] Restructuring text area stuff --- src/textual/_document.py | 27 +++++++++++++++ src/textual/widgets/_text_area.py | 33 ++----------------- .../test_document_delete.py} | 0 .../test_document_insert.py} | 0 4 files changed, 30 insertions(+), 30 deletions(-) rename tests/{text_area/test_text_area_document_delete.py => document/test_document_delete.py} (100%) rename tests/{text_area/test_text_area_document_insert.py => document/test_document_insert.py} (100%) diff --git a/src/textual/_document.py b/src/textual/_document.py index 49e1f1e1f5..1e98d4ca27 100644 --- a/src/textual/_document.py +++ b/src/textual/_document.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import NamedTuple + from rich.text import Text from textual._cells import cell_len @@ -137,3 +139,28 @@ def get_line(self, index: int) -> Text: def __getitem__(self, item: SupportsIndex | slice) -> str: return self._lines[item] + + +class Selection(NamedTuple): + """A range of characters within a document from a start point to the end point. + The position of the cursor is always considered to be the `end` point of the selection. + The selection is inclusive of the minimum point and exclusive of the maximum point. + """ + + start: tuple[int, int] = (0, 0) + end: tuple[int, int] = (0, 0) + + @classmethod + def cursor(cls, position: tuple[int, int]) -> "Selection": + """Create a Selection with the same start and end point.""" + return cls(position, position) + + @property + def is_empty(self) -> bool: + """Return True if the selection has 0 width, i.e. it's just a cursor.""" + start, end = self + return start == end + + def range(self) -> tuple[tuple[int, int], tuple[int, int]]: + start, end = self + return _fix_direction(start, end) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index b5b8ea57fe..61c0cdd46e 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -13,7 +13,7 @@ from textual import events, log from textual._cells import cell_len -from textual._document import Document +from textual._document import Document, Selection from textual._fix_direction import _fix_direction from textual._types import Protocol, runtime_checkable from textual.binding import Binding @@ -60,31 +60,6 @@ class Highlight(NamedTuple): highlight_name: str | None -class Selection(NamedTuple): - """A range of characters within a document from a start point to the end point. - The position of the cursor is always considered to be the `end` point of the selection. - The selection is inclusive of the minimum point and exclusive of the maximum point. - """ - - start: tuple[int, int] = (0, 0) - end: tuple[int, int] = (0, 0) - - @classmethod - def cursor(cls, position: tuple[int, int]) -> "Selection": - """Create a Selection with the same start and end point.""" - return cls(position, position) - - @property - def is_empty(self) -> bool: - """Return True if the selection has 0 width, i.e. it's just a cursor.""" - start, end = self - return start == end - - def range(self) -> tuple[tuple[int, int], tuple[int, int]]: - start, end = self - return _fix_direction(start, end) - - @runtime_checkable class Edit(Protocol): """Protocol for actions performed in the text editor that can be done and undone.""" @@ -114,8 +89,7 @@ def undo(self, editor: TextArea) -> None: """Undo the action.""" -@dataclass -class Delete: +class Delete(NamedTuple): """Performs a delete operation.""" from_position: tuple[int, int] @@ -211,7 +185,7 @@ class TextArea(ScrollView, can_focus=True): language: Reactive[str | None] = reactive(None) """The language to use for syntax highlighting (via tree-sitter).""" - selection: Reactive[Selection] = reactive(Selection(), always_update=True) + selection: Reactive[Selection] = reactive(Selection()) """The cursor position (zero-based line_index, offset).""" show_line_numbers: Reactive[bool] = reactive(True) """True to show line number gutter, otherwise False.""" @@ -684,7 +658,6 @@ def cursor_to_line_start(self, select: bool = False) -> None: self.selection = Selection(start, (cursor_row, 0)) else: self.selection = Selection.cursor((cursor_row, 0)) - print(f"new selection = {self.selection}") # ------ Cursor movement actions def action_cursor_left(self) -> None: diff --git a/tests/text_area/test_text_area_document_delete.py b/tests/document/test_document_delete.py similarity index 100% rename from tests/text_area/test_text_area_document_delete.py rename to tests/document/test_document_delete.py diff --git a/tests/text_area/test_text_area_document_insert.py b/tests/document/test_document_insert.py similarity index 100% rename from tests/text_area/test_text_area_document_insert.py rename to tests/document/test_document_insert.py From c9603e854228f6c63f0056bbd3bd14562e74df9e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 25 Jul 2023 10:38:44 +0100 Subject: [PATCH 086/366] Moving deletion over to use the Document class --- src/textual/document/__init__.py | 0 src/textual/{ => document}/_document.py | 2 - .../document/_syntax_aware_document.py | 6 + src/textual/widgets/_text_area.py | 184 ++++++++---------- tests/document/test_document_delete.py | 2 +- tests/document/test_document_insert.py | 2 +- tests/document/test_selection.py | 0 7 files changed, 93 insertions(+), 103 deletions(-) create mode 100644 src/textual/document/__init__.py rename src/textual/{ => document}/_document.py (98%) create mode 100644 src/textual/document/_syntax_aware_document.py create mode 100644 tests/document/test_selection.py diff --git a/src/textual/document/__init__.py b/src/textual/document/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/textual/_document.py b/src/textual/document/_document.py similarity index 98% rename from src/textual/_document.py rename to src/textual/document/_document.py index 1e98d4ca27..315ce5ab74 100644 --- a/src/textual/_document.py +++ b/src/textual/document/_document.py @@ -5,8 +5,6 @@ from rich.text import Text from textual._cells import cell_len - -# TODO - probably need to move _fix_direction either to this file or a standalone file. from textual._fix_direction import _fix_direction from textual._types import SupportsIndex from textual.geometry import Size diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py new file mode 100644 index 0000000000..5fa097fa26 --- /dev/null +++ b/src/textual/document/_syntax_aware_document.py @@ -0,0 +1,6 @@ +from textual.document._document import Document + + +class SyntaxAwareDocument(Document): + def __init__(self): + super().__init__() diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 61c0cdd46e..1cf8bae16d 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -13,10 +13,10 @@ from textual import events, log from textual._cells import cell_len -from textual._document import Document, Selection from textual._fix_direction import _fix_direction from textual._types import Protocol, runtime_checkable from textual.binding import Binding +from textual.document._document import Document, Selection from textual.geometry import Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView @@ -89,7 +89,8 @@ def undo(self, editor: TextArea) -> None: """Undo the action.""" -class Delete(NamedTuple): +@dataclass +class Delete: """Performs a delete operation.""" from_position: tuple[int, int] @@ -101,15 +102,26 @@ class Delete(NamedTuple): cursor_destination: tuple[int, int] | None = None """Where to move the cursor to after the deletion.""" + deleted_text: str | None = None + """The text that was deleted, or None if the deletion hasn't occurred yet.""" + def do(self, editor: TextArea) -> str: - """Do the action.""" - self.deleted_text = editor._delete_range( - self.from_position, self.to_position, self.cursor_destination - ) + """Do the delete action and record the text that was deleted.""" + from_position = self.from_position + to_position = self.to_position + cursor_destination = self.cursor_destination + + self.deleted_text = editor.document.delete_range(from_position, to_position) + + if cursor_destination is not None: + editor.selection = Selection.cursor(cursor_destination) + else: + editor.selection = Selection.cursor(from_position) + return self.deleted_text def undo(self, editor: TextArea) -> None: - """Undo the action.""" + """Undo the delete action.""" def __rich_repr__(self): yield "from_position", self.from_position @@ -185,13 +197,19 @@ class TextArea(ScrollView, can_focus=True): language: Reactive[str | None] = reactive(None) """The language to use for syntax highlighting (via tree-sitter).""" + selection: Reactive[Selection] = reactive(Selection()) """The cursor position (zero-based line_index, offset).""" + show_line_numbers: Reactive[bool] = reactive(True) """True to show line number gutter, otherwise False.""" - _document_size: Reactive[Size] = reactive(Size(), init=False) - """Tracks the width of the document. Used to update virtual size. Do not - update virtual size directly.""" + + _document_size: Reactive[Size] = reactive(Size(), init=False, always_update=True) + """Tracks the size of the document. + + This is the width and height of the bounding box of the text. + Used to update virtual size. + """ def __init__( self, @@ -295,37 +313,26 @@ def load_text(self, text: str) -> None: self.document.load_text(text) self._refresh_size() - def load_lines(self, lines: list[str]) -> None: - """Load text from a list of lines into the editor. - - This will replace any previously loaded lines.""" - - # TODO - migrate the highlighting stuff out of here into the Document subclass - self.document_lines = lines - self._document_size = self._get_document_size(lines) - if self._parser is not None: - self._syntax_tree = self._build_ast(self._parser) - self._prepare_highlights() - - log.debug(f"loaded text. parser = {self._parser} ast = {self._syntax_tree}") + # + # def load_lines(self, lines: list[str]) -> None: + # """Load text from a list of lines into the editor. + # + # This will replace any previously loaded lines.""" + # + # # TODO - migrate the highlighting stuff out of here into the Document subclass + # self.document_lines = lines + # self._document_size = self._get_document_size(lines) + # if self._parser is not None: + # self._syntax_tree = self._build_ast(self._parser) + # self._prepare_highlights() + # + # log.debug(f"loaded text. parser = {self._parser} ast = {self._syntax_tree}") def clear(self) -> None: - # TODO: Perform a delete on the whole document. + # TODO: Perform a delete on the whole document to ensure this is + # integrated with undo/redo. self.load_text("") - # --- Methods for measuring things (e.g. virtual sizes) - def _get_document_size(self, document_lines: list[str]) -> Size: - """Return the virtual size of the document - the document only - refers to the area in which the cursor can move. It does not, for - example, include the width of the gutter.""" - text_width = max(cell_len(line) for line in document_lines) - height = len(document_lines) - # We add one to the text width to leave a space for the cursor, since it - # can rest at the end of a line where there isn't yet any character. - # Similarly, the cursor can rest below the bottom line of text, where - # a line doesn't currently exist. - return Size(text_width + 1, height) - def _refresh_size(self) -> None: width, height = self.document.size # +1 to reserve cursor end-of-line resting space. @@ -334,16 +341,13 @@ def _refresh_size(self) -> None: def render_line(self, widget_y: int) -> Strip: document = self.document - # TODO - we should ask the document itself for the content of the line - # for the given line number. - line_index = round(self.scroll_y + widget_y) out_of_bounds = line_index >= document.line_count if out_of_bounds: return Strip.blank(self.size.width) line = document.get_line(line_index) - line_length = len(line) + codepoint_count = len(line) line.set_length(self.virtual_size.width) # TODO - probably remove this highlighting code from here, we can @@ -384,12 +388,12 @@ def render_line(self, widget_y: int) -> Strip: line.stylize_before( selection_style, start=selection_top_column, - end=line_length, + end=codepoint_count, ) elif line_index == selection_bottom_row: line.stylize_before(selection_style, end=selection_bottom_column) else: - line.stylize_before(selection_style, end=line_length) + line.stylize_before(selection_style, end=codepoint_count) # Show the cursor and the selection if end_row == line_index: @@ -499,7 +503,6 @@ def edit(self, edit: Edit) -> object | None: log.debug(f"performing edit {edit!r}") result = edit.do(self) - # After edits we need to refresh self._refresh_size() self._undo_stack.append(edit) @@ -586,7 +589,44 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: def watch_selection(self) -> None: self.scroll_cursor_visible() - # --- Cursor utilities + def validate_selection(self, selection: Selection) -> Selection: + start, end = selection + clamp_location = self.clamp_location + return Selection(clamp_location(start), clamp_location(end)) + + # --- Cursor/selection utilities + def is_visitable(self, location: tuple[int, int]) -> bool: + """Return True if the location is somewhere that can naturally be reached by the cursor. + + Generally this means it's at a row within the document, and a column which contains a character, + OR at the resting position at the end of a row.""" + row, column = location + document = self.document + row_text = document[row] + is_valid_row = row < document.line_count + is_valid_column = column <= len(row_text) + return is_valid_row and is_valid_column + + def is_visitable_selection(self, selection: Selection) -> bool: + """Return True if the Selection is valid (start and end in bounds)""" + visitable = self.is_visitable + start, end = selection + return visitable(start) and visitable(end) + + def clamp_location(self, location: tuple[int, int]) -> tuple[int, int]: + document = self.document + + row, column = location + try: + line_text = document[row] + except IndexError: + line_text = "" + + row = clamp(row, 0, document.line_count - 1) + column = clamp(column, 0, len(line_text)) + + return row, column + def scroll_cursor_visible(self): # The end of the selection is always considered to be position of the cursor # ... this is a constraint we need to enforce in code. @@ -1185,57 +1225,3 @@ def debug_state(self) -> "EditorDebug": ), highlight_cache_current_row=self._highlights[self.selection.end[0]], ) - - -def traverse_tree(cursor): - reached_root = False - while reached_root == False: - yield cursor.node - - if cursor.goto_first_child(): - continue - - if cursor.goto_next_sibling(): - continue - - retracing = True - while retracing: - if not cursor.goto_parent(): - retracing = False - reached_root = True - - if cursor.goto_next_sibling(): - retracing = False - - -# if __name__ == "__main__": -# language = Language(LANGUAGES_PATH.resolve(), "python") -# parser = Parser() -# parser.set_language(language) -# -# CODE = """\ -# from textual.app import App -# -# -# class ScreenApp(App): -# def on_mount(self) -> None: -# self.screen.styles.background = "darkblue" -# self.screen.styles.border = ("heavy", "white") -# -# -# if __name__ == "__main__": -# app = ScreenApp() -# app.run() -# """ -# -# document_lines = CODE.splitlines(keepends=False) -# -# def read_callable(byte_offset, point): -# row, column = point -# if row >= len(document_lines) or column >= len(document_lines[row]): -# return None -# return document_lines[row][column:].encode("utf8") -# -# tree = parser.parse(bytes(CODE, "utf-8")) -# -# print(list(traverse_tree(tree.walk()))) diff --git a/tests/document/test_document_delete.py b/tests/document/test_document_delete.py index 78e948abf2..4fb398bd83 100644 --- a/tests/document/test_document_delete.py +++ b/tests/document/test_document_delete.py @@ -1,6 +1,6 @@ import pytest -from textual._document import Document +from textual.document._document import Document TEXT = """I must not fear. Fear is the mind-killer. diff --git a/tests/document/test_document_insert.py b/tests/document/test_document_insert.py index 7cccc8e495..237a1a1193 100644 --- a/tests/document/test_document_insert.py +++ b/tests/document/test_document_insert.py @@ -1,6 +1,6 @@ import pytest -from textual._document import Document +from textual.document._document import Document TEXT = """I must not fear. Fear is the mind-killer.""" diff --git a/tests/document/test_selection.py b/tests/document/test_selection.py new file mode 100644 index 0000000000..e69de29bb2 From 4a88d4313c2d68cb777fd656ccda292c254a0d47 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 25 Jul 2023 11:01:50 +0100 Subject: [PATCH 087/366] Delete and Insert API improvements --- src/textual/widgets/_text_area.py | 141 ++++++++++++++++-------------- 1 file changed, 77 insertions(+), 64 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 1cf8bae16d..7eeb496f4f 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -2,7 +2,7 @@ import re from collections import defaultdict -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import ClassVar, NamedTuple @@ -70,24 +70,38 @@ def do(self, editor: TextArea) -> object | None: def undo(self, editor: TextArea) -> object | None: """Undo the action.""" + def post_refresh(self, editor: TextArea) -> None: + """Code to execute after content size recalculated and repainted.""" -class Insert(NamedTuple): + +@dataclass +class Insert: """Implements the Edit protocol for inserting text at some position.""" text: str from_position: tuple[int, int] to_position: tuple[int, int] - move_cursor: bool = True + cursor_destination: tuple[int, int] | None = None + _edit_end: tuple[int, int] | None = field(init=False, default=None) def do(self, editor: TextArea) -> None: - text, start, end, move_cursor = self - edit_end = editor.document.insert_range(start, end, text) - if move_cursor: - editor.selection = Selection.cursor(edit_end) + self._edit_end = editor._document.insert_range( + self.from_position, + self.to_position, + self.text, + ) def undo(self, editor: TextArea) -> None: """Undo the action.""" + def post_refresh(self, editor: TextArea) -> None: + # Update the cursor position + cursor_destination = self.cursor_destination + if cursor_destination is not None: + editor.selection = cursor_destination + else: + editor.selection = Selection.cursor(self._edit_end) + @dataclass class Delete: @@ -107,21 +121,20 @@ class Delete: def do(self, editor: TextArea) -> str: """Do the delete action and record the text that was deleted.""" - from_position = self.from_position - to_position = self.to_position - cursor_destination = self.cursor_destination + self.deleted_text = editor._document.delete_range( + self.from_position, self.to_position + ) + return self.deleted_text - self.deleted_text = editor.document.delete_range(from_position, to_position) + def undo(self, editor: TextArea) -> None: + """Undo the delete action.""" + def post_refresh(self, editor: TextArea) -> None: + cursor_destination = self.cursor_destination if cursor_destination is not None: editor.selection = Selection.cursor(cursor_destination) else: - editor.selection = Selection.cursor(from_position) - - return self.deleted_text - - def undo(self, editor: TextArea) -> None: - """Undo the delete action.""" + editor.selection = Selection.cursor(self.from_position) def __rich_repr__(self): yield "from_position", self.from_position @@ -180,6 +193,7 @@ class TextArea(ScrollView, can_focus=True): Binding("ctrl+right", "cursor_right_word", "cursor right word", show=False), Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False), Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False), + # Deletion Binding("backspace", "delete_left", "delete left", show=False), Binding( "ctrl+w", "delete_word_left", "delete left to start of word", show=False @@ -221,12 +235,8 @@ def __init__( super().__init__(name=name, id=id, classes=classes, disabled=disabled) # --- Core editor data - self.document = Document() - - # TODO - currently migrating the document_lines over to use Document. - - self.document_lines: list[str] = [] - """Each string in this list represents a line in the document. Includes new line characters.""" + self._document = Document() + """The document this widget is currently editing.""" self._highlights: dict[int, list[Highlight]] = defaultdict(list) """Mapping line numbers to the set of cached highlights for that line.""" @@ -292,7 +302,7 @@ def _build_ast( def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> str: row, column = point - lines = self.document.lines + lines = self._document.lines row_out_of_bounds = row >= len(lines) column_out_of_bounds = not row_out_of_bounds and column > len(lines[row]) if row_out_of_bounds or column_out_of_bounds: @@ -310,7 +320,7 @@ def load_text(self, text: str) -> None: Args: text: The text to load into the editor. """ - self.document.load_text(text) + self._document.load_text(text) self._refresh_size() # @@ -334,12 +344,12 @@ def clear(self) -> None: self.load_text("") def _refresh_size(self) -> None: - width, height = self.document.size + width, height = self._document.size # +1 to reserve cursor end-of-line resting space. self._document_size = Size(width + 1, height) def render_line(self, widget_y: int) -> Strip: - document = self.document + document = self._document line_index = round(self.scroll_y + widget_y) out_of_bounds = line_index >= document.line_count @@ -441,7 +451,7 @@ def gutter_width(self) -> int: # The longest number in the gutter plus two extra characters: `│ `. gutter_margin = 2 gutter_longest_number = ( - len(str(self.document.line_count + 1)) + gutter_margin + len(str(self._document.line_count + 1)) + gutter_margin if self.show_line_numbers else 0 ) @@ -503,13 +513,14 @@ def edit(self, edit: Edit) -> object | None: log.debug(f"performing edit {edit!r}") result = edit.do(self) - self._refresh_size() - - self._undo_stack.append(edit) - # TODO: Think about this... + self._undo_stack.append(edit) self._undo_stack = self._undo_stack[-20:] + self._refresh_size() + + edit.post_refresh(self) + return result def undo(self) -> None: @@ -543,7 +554,7 @@ def get_target_document_location(self, offset: Offset) -> tuple[int, int]: target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0) target_row = clamp( - offset.y + int(self.scroll_y), 0, self.document.line_count - 1 + offset.y + int(self.scroll_y), 0, self._document.line_count - 1 ) target_column = self.cell_width_to_column_index(target_x, target_row) @@ -579,7 +590,7 @@ def _on_paste(self, event: events.Paste) -> None: def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: """Return the column that the cell width corresponds to on the given row.""" total_cell_offset = 0 - line = self.document[row_index] + line = self._document[row_index] for column_index, character in enumerate(line): total_cell_offset += cell_len(character) if total_cell_offset >= cell_width + 1: @@ -601,7 +612,7 @@ def is_visitable(self, location: tuple[int, int]) -> bool: Generally this means it's at a row within the document, and a column which contains a character, OR at the resting position at the end of a row.""" row, column = location - document = self.document + document = self._document row_text = document[row] is_valid_row = row < document.line_count is_valid_column = column <= len(row_text) @@ -614,7 +625,7 @@ def is_visitable_selection(self, selection: Selection) -> bool: return visitable(start) and visitable(end) def clamp_location(self, location: tuple[int, int]) -> tuple[int, int]: - document = self.document + document = self._document row, column = location try: @@ -628,8 +639,7 @@ def clamp_location(self, location: tuple[int, int]) -> tuple[int, int]: return row, column def scroll_cursor_visible(self): - # The end of the selection is always considered to be position of the cursor - # ... this is a constraint we need to enforce in code. + log.debug("scrolling cursor visible") row, column = self.selection.end text = self.active_line_text[:column] column_offset = cell_len(text) @@ -646,7 +656,7 @@ def cursor_at_first_row(self) -> bool: @property def cursor_at_last_row(self) -> bool: - return self.selection.end[0] == self.document.line_count - 1 + return self.selection.end[0] == self._document.line_count - 1 @property def cursor_at_start_of_row(self) -> bool: @@ -655,7 +665,7 @@ def cursor_at_start_of_row(self) -> bool: @property def cursor_at_end_of_row(self) -> bool: cursor_row, cursor_column = self.selection.end - row_length = len(self.document[cursor_row]) + row_length = len(self._document[cursor_row]) cursor_at_end = cursor_column == row_length return cursor_at_end @@ -677,7 +687,7 @@ def cursor_to_line_end(self, select: bool = False) -> None: start, end = self.selection cursor_row, cursor_column = end - target_column = len(self.document[cursor_row]) + target_column = len(self._document[cursor_row]) if select: self.selection = Selection(start, target_column) @@ -725,7 +735,7 @@ def get_cursor_left_position(self) -> tuple[int, int]: if self.cursor_at_start_of_document: return 0, 0 cursor_row, cursor_column = self.selection.end - length_of_row_above = len(self.document[cursor_row - 1]) + length_of_row_above = len(self._document[cursor_row - 1]) target_row = cursor_row if cursor_column != 0 else cursor_row - 1 target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above return target_row, target_column @@ -773,14 +783,14 @@ def get_cursor_down_position(self): """Get the position the cursor will move to if it moves down.""" cursor_row, cursor_column = self.selection.end if self.cursor_at_last_row: - return cursor_row, len(self.document[cursor_row]) + return cursor_row, len(self._document[cursor_row]) - target_row = min(self.document.line_count - 1, cursor_row + 1) + target_row = min(self._document.line_count - 1, cursor_row + 1) # Attempt to snap last intentional cell length target_column = self.cell_width_to_column_index( self._last_intentional_cell_width, target_row ) - target_column = clamp(target_column, 0, len(self.document[target_row])) + target_column = clamp(target_column, 0, len(self._document[target_row])) return target_row, target_column def action_cursor_up(self) -> None: @@ -804,7 +814,7 @@ def get_cursor_up_position(self) -> tuple[int, int]: target_column = self.cell_width_to_column_index( self._last_intentional_cell_width, target_row ) - target_column = clamp(target_column, 0, len(self.document[target_row])) + target_column = clamp(target_column, 0, len(self._document[target_row])) return target_row, target_column def action_cursor_line_end(self) -> None: @@ -824,7 +834,7 @@ def action_cursor_left_word(self) -> None: cursor_row, cursor_column = self.selection.end # Check the current line for a word boundary - line = self.document[cursor_row][:cursor_column] + line = self._document[cursor_row][:cursor_column] matches = list(re.finditer(self._word_pattern, line)) if matches: @@ -833,7 +843,7 @@ def action_cursor_left_word(self) -> None: elif cursor_row > 0: # If no word boundary is found and we're not on the first line, move to the end of the previous line cursor_row -= 1 - cursor_column = len(self.document[cursor_row]) + cursor_column = len(self._document[cursor_row]) else: # If we're already on the first line and no word boundary is found, move to the start of the line cursor_column = 0 @@ -850,19 +860,19 @@ def action_cursor_right_word(self) -> None: cursor_row, cursor_column = self.selection.end # Check the current line for a word boundary - line = self.document[cursor_row][cursor_column:] + line = self._document[cursor_row][cursor_column:] matches = list(re.finditer(self._word_pattern, line)) if matches: # If a word boundary is found, move the cursor there cursor_column += matches[0].end() - elif cursor_row < self.document.line_count - 1: + elif cursor_row < self._document.line_count - 1: # If no word boundary is found and we're not on the last line, move to the start of the next line cursor_row += 1 cursor_column = 0 else: # If we're already on the last line and no word boundary is found, move to the end of the line - cursor_column = len(self.document[cursor_row]) + cursor_column = len(self._document[cursor_row]) self.selection = Selection.cursor((cursor_row, cursor_column)) self._record_last_intentional_cell_width() @@ -870,13 +880,13 @@ def action_cursor_right_word(self) -> None: @property def active_line_text(self) -> str: # TODO - consider empty documents - return self.document[self.selection.end[0]] + return self._document[self.selection.end[0]] def get_column_cell_width(self, row: int, column: int) -> int: """Given a row and column index within the editor, return the cell offset of the column from the start of the row (the left edge of the editor content area). """ - line = self.document[row] + line = self._document[row] return cell_len(line[:column]) def _record_last_intentional_cell_width(self) -> None: @@ -886,18 +896,21 @@ def _record_last_intentional_cell_width(self) -> None: # --- Editor operations def insert_text( - self, text: str, position: tuple[int, int], move_cursor: bool = True + self, + text: str, + position: tuple[int, int], + cursor_destination: tuple[int, int] | None = None, ) -> None: - self.edit(Insert(text, position, position, move_cursor)) + self.edit(Insert(text, position, position, cursor_destination)) def insert_text_range( self, text: str, from_position: tuple[int, int], to_position: tuple[int, int], - move_cursor: bool = True, + cursor_destination: tuple[int, int] | None = None, ): - self.edit(Insert(text, from_position, to_position, move_cursor)) + self.edit(Insert(text, from_position, to_position, cursor_destination)) # def _insert_text_range( # self, @@ -1085,7 +1098,7 @@ def action_delete_left(self) -> None: if empty: if self.cursor_at_start_of_row: - end = (end_row - 1, len(self.document[end_row - 1])) + end = (end_row - 1, len(self._document[end_row - 1])) else: end = (end_row, end_column - 1) @@ -1128,7 +1141,7 @@ def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor position to the end of the line.""" from_position = self.selection.end cursor_row, cursor_column = from_position - to_position = (cursor_row, len(self.document[cursor_row])) + to_position = (cursor_row, len(self._document[cursor_row])) self.delete_range(from_position, to_position) def action_delete_word_left(self) -> None: @@ -1145,7 +1158,7 @@ def action_delete_word_left(self) -> None: cursor_row, cursor_column = end # Check the current line for a word boundary - line = self.document[cursor_row][:cursor_column] + line = self._document[cursor_row][:cursor_column] matches = list(re.finditer(self._word_pattern, line)) if matches: @@ -1153,7 +1166,7 @@ def action_delete_word_left(self) -> None: from_position = (cursor_row, matches[-1].start()) elif cursor_row > 0: # If no word boundary is found, and we're not on the first line, delete to the end of the previous line - from_position = (cursor_row - 1, len(self.document[cursor_row - 1])) + from_position = (cursor_row - 1, len(self._document[cursor_row - 1])) else: # If we're already on the first line and no word boundary is found, delete to the start of the line from_position = (cursor_row, 0) @@ -1172,18 +1185,18 @@ def action_delete_word_right(self) -> None: cursor_row, cursor_column = end # Check the current line for a word boundary - line = self.document[cursor_row][cursor_column:] + line = self._document[cursor_row][cursor_column:] matches = list(re.finditer(self._word_pattern, line)) if matches: # If a word boundary is found, delete the word to_position = (cursor_row, cursor_column + matches[0].end()) - elif cursor_row < self.document.line_count - 1: + elif cursor_row < self._document.line_count - 1: # If no word boundary is found, and we're not on the last line, delete to the start of the next line to_position = (cursor_row + 1, 0) else: # If we're already on the last line and no word boundary is found, delete to the end of the line - to_position = (cursor_row, len(self.document[cursor_row])) + to_position = (cursor_row, len(self._document[cursor_row])) self.delete_range(end, to_position) From 9aca4dad95cde992c96b9fd8687ce361d1880ecc Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 25 Jul 2023 12:59:34 +0100 Subject: [PATCH 088/366] Moving syntax highlighting logic into SyntaxAwareDOcument --- .../document/_syntax_aware_document.py | 175 +++++++++++++++++- src/textual/widgets/_text_area.py | 170 +---------------- 2 files changed, 175 insertions(+), 170 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 5fa097fa26..1895aa4c5d 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -1,6 +1,179 @@ +from __future__ import annotations + +from collections import defaultdict +from pathlib import Path +from typing import NamedTuple + +from rich.style import Style +from rich.text import Text +from tree_sitter import Language, Parser, Tree +from tree_sitter.binding import Query + +from textual import log from textual.document._document import Document +# TODO - remove hardcoded python.scm highlight query file +TREE_SITTER_PATH = Path(__file__) / "../../../../tree-sitter/" +LANGUAGES_PATH = TREE_SITTER_PATH / "textual-languages.so" +HIGHLIGHTS_PATH = TREE_SITTER_PATH / "highlights/python.scm" + +HIGHLIGHT_STYLES = { + "string": Style(color="#E6DB74"), + "string.documentation": Style(color="yellow"), + "comment": Style(color="#75715E"), + "keyword": Style(color="#F92672"), + "include": Style(color="#F92672"), + "keyword.function": Style(color="#F92672"), + "keyword.return": Style(color="#F92672"), + "conditional": Style(color="#F92672"), + "number": Style(color="#AE81FF"), + "class": Style(color="#A6E22E"), + "function": Style(color="#A6E22E"), + "function.call": Style(color="#A6E22E"), + "method": Style(color="#A6E22E"), + "method.call": Style(color="#A6E22E"), + # "constant": Style(color="#AE81FF"), + "variable": Style(color="white"), + "parameter": Style(color="cyan"), + "type": Style(color="cyan"), + "escape": Style(bgcolor="magenta"), +} + + +class Highlight(NamedTuple): + """A range to highlight within a single line""" + + start_column: int | None + end_column: int | None + highlight_name: str | None + class SyntaxAwareDocument(Document): - def __init__(self): + def __init__(self, language: str | None = None): super().__init__() + + # TODO validate language string + + self._language: Language = Language(LANGUAGES_PATH.resolve(), language) + """The tree-sitter Language.""" + + self._parser: Parser = Parser() + """The tree-sitter Parser""" + self._parser.set_language(self._language) + + self._syntax_tree = self._build_ast(self._parser) + """The tree-sitter Tree (syntax tree) built from the document.""" + + self._highlights_query = Path(HIGHLIGHTS_PATH.resolve()).read_text() + """The tree-sitter query string for used to fetch highlighted ranges""" + + self._highlights: dict[int, list[Highlight]] = defaultdict(list) + """Mapping line numbers to the set of cached highlights for that line.""" + + def load_text(self, text: str) -> None: + super().load_text(text) + self._build_ast(self._parser) + + def insert_range( + self, start: tuple[int, int], end: tuple[int, int], text: str + ) -> tuple[int, int]: + end_location = super().insert_range(start, end, text) + + # TODO - apply edits to the ast + + def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: + deleted_text = super().delete_range(start, end) + + # TODO - apply edits to the ast + + def _prepare_highlights( + self, + start_point: tuple[int, int] | None = None, + end_point: tuple[int, int] = None, + ) -> None: + # TODO - we're ignoring get changed ranges for now. Either I'm misunderstanding + # it or I've made a mistake somewhere with AST editing. + + highlights = self._highlights + query: Query = self._language.query(self._highlights_query) + + log.debug(f"capturing nodes in range {start_point!r} -> {end_point!r}") + + captures_kwargs = {} + if start_point is not None: + captures_kwargs["start_point"] = start_point + if end_point is not None: + captures_kwargs["end_point"] = end_point + + captures = query.captures(self._syntax_tree.root_node, **captures_kwargs) + + highlight_updates: dict[int, list[Highlight]] = defaultdict(list) + for capture in captures: + node, highlight_name = capture + node_start_row, node_start_column = node.start_point + node_end_row, node_end_column = node.end_point + + if node_start_row == node_end_row: + highlight = Highlight( + node_start_column, node_end_column, highlight_name + ) + highlight_updates[node_start_row].append(highlight) + else: + # Add the first line + highlight_updates[node_start_row].append( + Highlight(node_start_column, None, highlight_name) + ) + # Add the middle lines - entire row of this node is highlighted + for node_row in range(node_start_row + 1, node_end_row): + highlight_updates[node_row].append( + Highlight(0, None, highlight_name) + ) + + # Add the last line + highlight_updates[node_end_row].append( + Highlight(0, node_end_column, highlight_name) + ) + + for line_index, updated_highlights in highlight_updates.items(): + highlights[line_index] = updated_highlights + + def _build_ast( + self, + parser: Parser, + ) -> Tree | None: + """Fully parse the document and build the abstract syntax tree for it. + + Returns None if there's no parser available (e.g. when no language is selected). + """ + if parser: + return parser.parse(self._read_callable) + else: + return None + + def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | None: + row, column = point + lines = self._lines + + row_out_of_bounds = row >= len(lines) + column_out_of_bounds = not row_out_of_bounds and column > len(lines[row]) + + if row_out_of_bounds or column_out_of_bounds: + return_value = None + elif column == len(lines[row]) and row < len(lines): + return_value = "\n".encode("utf8") + else: + return_value = lines[row][column].encode("utf8") + + return return_value + + def get_line(self, line_index: int) -> Text: + null_style = Style.null() + line = Text(self[line_index]) + + if self._highlights: + highlights = self._highlights[line_index] + for start, end, highlight_name in highlights: + node_style = HIGHLIGHT_STYLES.get(highlight_name, null_style) + line.stylize(node_style, start, end) + + return line diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 7eeb496f4f..ad796dd8a4 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -17,48 +17,12 @@ from textual._types import Protocol, runtime_checkable from textual.binding import Binding from textual.document._document import Document, Selection +from textual.document._syntax_aware_document import Highlight from textual.geometry import Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView from textual.strip import Strip -TREE_SITTER_PATH = Path(__file__) / "../../../../tree-sitter/" -LANGUAGES_PATH = TREE_SITTER_PATH / "textual-languages.so" - -# TODO - remove hardcoded python.scm highlight query file -HIGHLIGHTS_PATH = TREE_SITTER_PATH / "highlights/python.scm" - -# TODO - temporary proof of concept approach -HIGHLIGHT_STYLES = { - "string": Style(color="#E6DB74"), - "string.documentation": Style(color="yellow"), - "comment": Style(color="#75715E"), - "keyword": Style(color="#F92672"), - "include": Style(color="#F92672"), - "keyword.function": Style(color="#F92672"), - "keyword.return": Style(color="#F92672"), - "conditional": Style(color="#F92672"), - "number": Style(color="#AE81FF"), - "class": Style(color="#A6E22E"), - "function": Style(color="#A6E22E"), - "function.call": Style(color="#A6E22E"), - "method": Style(color="#A6E22E"), - "method.call": Style(color="#A6E22E"), - # "constant": Style(color="#AE81FF"), - "variable": Style(color="white"), - "parameter": Style(color="cyan"), - "type": Style(color="cyan"), - "escape": Style(bgcolor="magenta"), -} - - -class Highlight(NamedTuple): - """A range to highlight within a single line""" - - start_column: int | None - end_column: int | None - highlight_name: str | None - @runtime_checkable class Edit(Protocol): @@ -238,12 +202,6 @@ def __init__( self._document = Document() """The document this widget is currently editing.""" - self._highlights: dict[int, list[Highlight]] = defaultdict(list) - """Mapping line numbers to the set of cached highlights for that line.""" - - self._highlights_query: str | None = None - """The string containing the tree-sitter AST query used for syntax highlighting.""" - self._last_intentional_cell_width: int = 0 """Tracks the last column (measured in terms of cell length, since we care here about where the cursor visually moves more than the logical characters) the user explicitly navigated to so that we can reset @@ -258,62 +216,11 @@ def __init__( self._selecting = False """True if we're currently selecting text, otherwise False.""" - # --- Abstract syntax tree and related parsing machinery - self._language: Language | None = None - self._parser: Parser | None = None - """The tree-sitter parser which extracts the syntax tree from the document.""" - self._syntax_tree: Tree | None = None - """The tree-sitter Tree (AST) built from the document.""" - - def watch_language(self, new_language: str | None) -> None: - """Update the language used in AST parsing. - - When the language reactive string is updated, fetch the Language definition - from our tree-sitter library file. If the language reactive is set to None, - then the no parser is used.""" - log.debug(f"updating editor language to {new_language!r}") - if new_language: - self._language = Language(LANGUAGES_PATH.resolve(), new_language) - parser = Parser() - self._parser = parser - self._parser.set_language(self._language) - self._syntax_tree = self._build_ast(parser) - self._highlights_query = Path(HIGHLIGHTS_PATH.resolve()).read_text() - - log.debug(f"parser set to {self._parser}") - def watch__document_size(self, size: Size) -> None: log.debug(f"document size set to {size!r} ") document_width, document_height = size self.virtual_size = Size(document_width + self.gutter_width, document_height) - def _build_ast( - self, - parser: Parser, - ) -> Tree | None: - """Fully parse the document and build the abstract syntax tree for it. - - Returns None if there's no parser available (e.g. when no language is selected). - """ - if parser: - return parser.parse(self._read_callable) - else: - return None - - def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> str: - row, column = point - lines = self._document.lines - row_out_of_bounds = row >= len(lines) - column_out_of_bounds = not row_out_of_bounds and column > len(lines[row]) - if row_out_of_bounds or column_out_of_bounds: - return_value = None - elif column == len(lines[row]) and row < len(lines): - return_value = "\n".encode("utf8") - else: - return_value = lines[row][column].encode("utf8") - # print(f"(point={point!r}) (offset={byte_offset!r}) {return_value!r}") - return return_value - def load_text(self, text: str) -> None: """Load text from a string into the editor. @@ -360,16 +267,6 @@ def render_line(self, widget_y: int) -> Strip: codepoint_count = len(line) line.set_length(self.virtual_size.width) - # TODO - probably remove this highlighting code from here, we can - # do it in our syntax aware Document subclass. - # Apply highlighting - null_style = Style.null() - if self._highlights: - highlights = self._highlights[line_index] - for start, end, highlight_name in highlights: - node_style = HIGHLIGHT_STYLES.get(highlight_name, null_style) - line.stylize(node_style, start, end) - start, end = self.selection end_row, end_column = end @@ -382,7 +279,6 @@ def render_line(self, widget_y: int) -> Strip: selection_bottom = max(start, end) selection_top_row, selection_top_column = selection_top selection_bottom_row, selection_bottom_column = selection_bottom - if start != end and selection_top_row <= line_index <= selection_bottom_row: # If this row is part of the selection if line_index == selection_top_row == selection_bottom_row: @@ -457,58 +353,6 @@ def gutter_width(self) -> int: ) return gutter_longest_number - # --- Syntax highlighting - def _prepare_highlights( - self, - start_point: tuple[int, int] | None = None, - end_point: tuple[int, int] = None, - ) -> None: - # TODO - we're ignoring get changed ranges for now. Either I'm misunderstanding - # it or I've made a mistake somewhere with AST editing. - - highlights = self._highlights - query: Query = self._language.query(self._highlights_query) - - log.debug(f"capturing nodes in range {start_point!r} -> {end_point!r}") - - captures_kwargs = {} - if start_point is not None: - captures_kwargs["start_point"] = start_point - if end_point is not None: - captures_kwargs["end_point"] = end_point - - captures = query.captures(self._syntax_tree.root_node, **captures_kwargs) - - highlight_updates: dict[int, list[Highlight]] = defaultdict(list) - for capture in captures: - node, highlight_name = capture - node_start_row, node_start_column = node.start_point - node_end_row, node_end_column = node.end_point - - if node_start_row == node_end_row: - highlight = Highlight( - node_start_column, node_end_column, highlight_name - ) - highlight_updates[node_start_row].append(highlight) - else: - # Add the first line - highlight_updates[node_start_row].append( - Highlight(node_start_column, None, highlight_name) - ) - # Add the middle lines - entire row of this node is highlighted - for node_row in range(node_start_row + 1, node_end_row): - highlight_updates[node_row].append( - Highlight(0, None, highlight_name) - ) - - # Add the last line - highlight_updates[node_end_row].append( - Highlight(0, node_end_column, highlight_name) - ) - - for line_index, updated_highlights in highlight_updates.items(): - highlights[line_index] = updated_highlights - def edit(self, edit: Edit) -> object | None: log.debug(f"performing edit {edit!r}") result = edit.do(self) @@ -1212,10 +1056,6 @@ class EditorDebug: tree_sexp: str active_line_text: str active_line_cell_len: int - highlight_cache_key_count: int - highlight_cache_total_size: int - highlight_cache_current_row_size: int - highlight_cache_current_row: list[Highlight] def debug_state(self) -> "EditorDebug": return self.EditorDebug( @@ -1229,12 +1069,4 @@ def debug_state(self) -> "EditorDebug": tree_sexp="", active_line_text=repr(self.active_line_text), active_line_cell_len=cell_len(self.active_line_text), - highlight_cache_key_count=len(self._highlights), - highlight_cache_total_size=sum( - len(highlights) for key, highlights in self._highlights.items() - ), - highlight_cache_current_row_size=len( - self._highlights[self.selection.end[0]] - ), - highlight_cache_current_row=self._highlights[self.selection.end[0]], ) From 4dcb765510794b20f034791c4fbbfa888e1a1b48 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 25 Jul 2023 13:07:59 +0100 Subject: [PATCH 089/366] Tidying --- .../document/_syntax_aware_document.py | 3 +- src/textual/widgets/_text_area.py | 30 ++++++++----------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 1895aa4c5d..5bc386d502 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -73,14 +73,13 @@ def __init__(self, language: str | None = None): def load_text(self, text: str) -> None: super().load_text(text) self._build_ast(self._parser) + self._prepare_highlights() def insert_range( self, start: tuple[int, int], end: tuple[int, int], text: str ) -> tuple[int, int]: end_location = super().insert_range(start, end, text) - # TODO - apply edits to the ast - def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: deleted_text = super().delete_range(start, end) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index ad796dd8a4..1e201b3064 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -374,6 +374,7 @@ def undo(self) -> None: # --- Lower level event/key handling def _on_key(self, event: events.Key) -> None: + event.stop() log.debug(f"{event!r}") key = event.key if event.is_printable or key == "tab" or key == "enter": @@ -383,19 +384,14 @@ def _on_key(self, event: events.Key) -> None: insert = "\n" else: insert = event.character - event.stop() assert event.character is not None start, end = self.selection self.insert_text_range(insert, start, end) event.prevent_default() elif key == "shift+tab": self.dedent_line() - event.stop() def get_target_document_location(self, offset: Offset) -> tuple[int, int]: - if offset is None: - return - target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0) target_row = clamp( offset.y + int(self.scroll_y), 0, self._document.line_count - 1 @@ -407,18 +403,19 @@ def get_target_document_location(self, offset: Offset) -> tuple[int, int]: def _on_mouse_down(self, event: events.MouseDown) -> None: event.stop() offset = event.get_content_offset(self) - target_row, target_column = self.get_target_document_location(offset) - self.selection = Selection.cursor((target_row, target_column)) - log.debug(f"started selection {self.selection!r}") - self._selecting = True + if offset is not None: + target = self.get_target_document_location(offset) + self.selection = Selection.cursor(target) + self._selecting = True def _on_mouse_move(self, event: events.MouseMove) -> None: event.stop() if self._selecting: offset = event.get_content_offset(self) - target = self.get_target_document_location(offset) - selection_start, _ = self.selection - self.selection = Selection(selection_start, target) + if offset is not None: + target = self.get_target_document_location(offset) + selection_start, _ = self.selection + self.selection = Selection(selection_start, target) def _on_mouse_up(self, event: events.MouseUp) -> None: event.stop() @@ -426,10 +423,10 @@ def _on_mouse_up(self, event: events.MouseUp) -> None: self._selecting = False def _on_paste(self, event: events.Paste) -> None: + event.stop() text = event.text if text: self.insert_text(text, self.selection) - event.stop() def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: """Return the column that the cell width corresponds to on the given row.""" @@ -446,8 +443,8 @@ def watch_selection(self) -> None: def validate_selection(self, selection: Selection) -> Selection: start, end = selection - clamp_location = self.clamp_location - return Selection(clamp_location(start), clamp_location(end)) + clamp_visitable = self.clamp_visitable + return Selection(clamp_visitable(start), clamp_visitable(end)) # --- Cursor/selection utilities def is_visitable(self, location: tuple[int, int]) -> bool: @@ -468,7 +465,7 @@ def is_visitable_selection(self, selection: Selection) -> bool: start, end = selection return visitable(start) and visitable(end) - def clamp_location(self, location: tuple[int, int]) -> tuple[int, int]: + def clamp_visitable(self, location: tuple[int, int]) -> tuple[int, int]: document = self._document row, column = location @@ -483,7 +480,6 @@ def clamp_location(self, location: tuple[int, int]) -> tuple[int, int]: return row, column def scroll_cursor_visible(self): - log.debug("scrolling cursor visible") row, column = self.selection.end text = self.active_line_text[:column] column_offset = cell_len(text) From 574639fed5e7349f7dbff4dbb893cdab927a6af1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 25 Jul 2023 13:27:15 +0100 Subject: [PATCH 090/366] Position -> location --- src/textual/document/_document.py | 6 +- src/textual/widgets/_text_area.py | 199 +++++++++++++++--------------- 2 files changed, 103 insertions(+), 102 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 315ce5ab74..6045ff0d0f 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -141,7 +141,7 @@ def __getitem__(self, item: SupportsIndex | slice) -> str: class Selection(NamedTuple): """A range of characters within a document from a start point to the end point. - The position of the cursor is always considered to be the `end` point of the selection. + The location of the cursor is always considered to be the `end` point of the selection. The selection is inclusive of the minimum point and exclusive of the maximum point. """ @@ -149,9 +149,9 @@ class Selection(NamedTuple): end: tuple[int, int] = (0, 0) @classmethod - def cursor(cls, position: tuple[int, int]) -> "Selection": + def cursor(cls, location: tuple[int, int]) -> "Selection": """Create a Selection with the same start and end point.""" - return cls(position, position) + return cls(location, location) @property def is_empty(self) -> bool: diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 1e201b3064..08e2333949 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -40,18 +40,18 @@ def post_refresh(self, editor: TextArea) -> None: @dataclass class Insert: - """Implements the Edit protocol for inserting text at some position.""" + """Implements the Edit protocol for inserting text at some location.""" text: str - from_position: tuple[int, int] - to_position: tuple[int, int] + from_location: tuple[int, int] + to_location: tuple[int, int] cursor_destination: tuple[int, int] | None = None _edit_end: tuple[int, int] | None = field(init=False, default=None) def do(self, editor: TextArea) -> None: self._edit_end = editor._document.insert_range( - self.from_position, - self.to_position, + self.from_location, + self.to_location, self.text, ) @@ -59,7 +59,7 @@ def undo(self, editor: TextArea) -> None: """Undo the action.""" def post_refresh(self, editor: TextArea) -> None: - # Update the cursor position + # Update the cursor location cursor_destination = self.cursor_destination if cursor_destination is not None: editor.selection = cursor_destination @@ -71,11 +71,11 @@ def post_refresh(self, editor: TextArea) -> None: class Delete: """Performs a delete operation.""" - from_position: tuple[int, int] - """The position to delete from (inclusive).""" + from_location: tuple[int, int] + """The location to delete from (inclusive).""" - to_position: tuple[int, int] - """The position to delete to (exclusive).""" + to_location: tuple[int, int] + """The location to delete to (exclusive).""" cursor_destination: tuple[int, int] | None = None """Where to move the cursor to after the deletion.""" @@ -86,7 +86,7 @@ class Delete: def do(self, editor: TextArea) -> str: """Do the delete action and record the text that was deleted.""" self.deleted_text = editor._document.delete_range( - self.from_position, self.to_position + self.from_location, self.to_location ) return self.deleted_text @@ -98,11 +98,11 @@ def post_refresh(self, editor: TextArea) -> None: if cursor_destination is not None: editor.selection = Selection.cursor(cursor_destination) else: - editor.selection = Selection.cursor(self.from_position) + editor.selection = Selection.cursor(self.from_location) def __rich_repr__(self): - yield "from_position", self.from_position - yield "to_position", self.to_position + yield "from_location", self.from_location + yield "to_location", self.to_location if hasattr(self, "deleted_text"): yield "deleted_text", self.deleted_text @@ -177,7 +177,7 @@ class TextArea(ScrollView, can_focus=True): """The language to use for syntax highlighting (via tree-sitter).""" selection: Reactive[Selection] = reactive(Selection()) - """The cursor position (zero-based line_index, offset).""" + """The selection location (zero-based line_index, offset).""" show_line_numbers: Reactive[bool] = reactive(True) """True to show line number gutter, otherwise False.""" @@ -374,7 +374,6 @@ def undo(self) -> None: # --- Lower level event/key handling def _on_key(self, event: events.Key) -> None: - event.stop() log.debug(f"{event!r}") key = event.key if event.is_printable or key == "tab" or key == "enter": @@ -387,8 +386,10 @@ def _on_key(self, event: events.Key) -> None: assert event.character is not None start, end = self.selection self.insert_text_range(insert, start, end) + event.stop() event.prevent_default() elif key == "shift+tab": + event.stop() self.dedent_line() def get_target_document_location(self, offset: Offset) -> tuple[int, int]: @@ -451,7 +452,7 @@ def is_visitable(self, location: tuple[int, int]) -> bool: """Return True if the location is somewhere that can naturally be reached by the cursor. Generally this means it's at a row within the document, and a column which contains a character, - OR at the resting position at the end of a row.""" + OR at the resting location at the end of a row.""" row, column = location document = self._document row_text = document[row] @@ -551,27 +552,27 @@ def cursor_to_line_start(self, select: bool = False) -> None: # ------ Cursor movement actions def action_cursor_left(self) -> None: - """Move the cursor one position to the left. + """Move the cursor one location to the left. If the cursor is at the left edge of the document, try to move it to the end of the previous line. """ - target = self.get_cursor_left_position() + target = self.get_cursor_left_location() self.selection = Selection.cursor(target) self._record_last_intentional_cell_width() def action_cursor_left_select(self): - """Move the end of the selection one position to the left. + """Move the end of the selection one location to the left. This will expand or contract the selection. """ - new_cursor_position = self.get_cursor_left_position() + new_cursor_location = self.get_cursor_left_location() selection_start, selection_end = self.selection - self.selection = Selection(selection_start, new_cursor_position) + self.selection = Selection(selection_start, new_cursor_location) self._record_last_intentional_cell_width() - def get_cursor_left_position(self) -> tuple[int, int]: - """Get the position the cursor will move to if it moves left.""" + def get_cursor_left_location(self) -> tuple[int, int]: + """Get the location the cursor will move to if it moves left.""" if self.cursor_at_start_of_document: return 0, 0 cursor_row, cursor_column = self.selection.end @@ -581,26 +582,26 @@ def get_cursor_left_position(self) -> tuple[int, int]: return target_row, target_column def action_cursor_right(self) -> None: - """Move the cursor one position to the right. + """Move the cursor one location to the right. If the cursor is at the end of a line, attempt to go to the start of the next line. """ - target = self.get_cursor_right_position() + target = self.get_cursor_right_location() self.selection = Selection.cursor(target) self._record_last_intentional_cell_width() def action_cursor_right_select(self): - """Move the end of the selection one position to the right. + """Move the end of the selection one location to the right. This will expand or contract the selection. """ - new_cursor_position = self.get_cursor_right_position() + new_cursor_location = self.get_cursor_right_location() selection_start, selection_end = self.selection - self.selection = Selection(selection_start, new_cursor_position) + self.selection = Selection(selection_start, new_cursor_location) self._record_last_intentional_cell_width() - def get_cursor_right_position(self) -> tuple[int, int]: - """Get the position the cursor will move to if it moves right.""" + def get_cursor_right_location(self) -> tuple[int, int]: + """Get the location the cursor will move to if it moves right.""" if self.cursor_at_end_of_document: return self.selection.end cursor_row, cursor_column = self.selection.end @@ -610,17 +611,17 @@ def get_cursor_right_position(self) -> tuple[int, int]: def action_cursor_down(self) -> None: """Move the cursor down one cell.""" - target = self.get_cursor_down_position() + target = self.get_cursor_down_location() self.selection = Selection.cursor(target) def action_cursor_down_select(self) -> None: - """Move the cursor down one cell, selecting the range between the old and new positions.""" - target = self.get_cursor_down_position() + """Move the cursor down one cell, selecting the range between the old and new locations.""" + target = self.get_cursor_down_location() start, end = self.selection self.selection = Selection(start, target) - def get_cursor_down_position(self): - """Get the position the cursor will move to if it moves down.""" + def get_cursor_down_location(self): + """Get the location the cursor will move to if it moves down.""" cursor_row, cursor_column = self.selection.end if self.cursor_at_last_row: return cursor_row, len(self._document[cursor_row]) @@ -635,17 +636,17 @@ def get_cursor_down_position(self): def action_cursor_up(self) -> None: """Move the cursor up one cell.""" - target = self.get_cursor_up_position() + target = self.get_cursor_up_location() self.selection = Selection.cursor(target) def action_cursor_up_select(self) -> None: - """Move the cursor up one cell, selecting the range between the old and new positions.""" - target = self.get_cursor_up_position() + """Move the cursor up one cell, selecting the range between the old and new locations.""" + target = self.get_cursor_up_location() start, end = self.selection self.selection = Selection(start, target) - def get_cursor_up_position(self) -> tuple[int, int]: - """Get the position the cursor will move to if it moves up.""" + def get_cursor_up_location(self) -> tuple[int, int]: + """Get the location the cursor will move to if it moves up.""" if self.cursor_at_first_row: return 0, 0 cursor_row, cursor_column = self.selection.end @@ -738,25 +739,25 @@ def _record_last_intentional_cell_width(self) -> None: def insert_text( self, text: str, - position: tuple[int, int], + location: tuple[int, int], cursor_destination: tuple[int, int] | None = None, ) -> None: - self.edit(Insert(text, position, position, cursor_destination)) + self.edit(Insert(text, location, location, cursor_destination)) def insert_text_range( self, text: str, - from_position: tuple[int, int], - to_position: tuple[int, int], + from_location: tuple[int, int], + to_location: tuple[int, int], cursor_destination: tuple[int, int] | None = None, ): - self.edit(Insert(text, from_position, to_position, cursor_destination)) + self.edit(Insert(text, from_location, to_location, cursor_destination)) # def _insert_text_range( # self, # text: str, - # from_position: tuple[int, int], - # to_position: tuple[int, int], + # from_location: tuple[int, int], + # to_location: tuple[int, int], # move_cursor: bool = True, # ) -> None: # """Insert text at a given range and move the cursor to the end of the inserted text.""" @@ -764,10 +765,10 @@ def insert_text_range( # inserted_text = text # lines = self.document_lines # - # from_row, from_column = from_position - # to_row, to_column = to_position + # from_row, from_column = from_location + # to_row, to_column = to_location # - # if from_position > to_position: + # if from_location > to_location: # from_row, from_column, to_row, to_column = ( # to_row, # to_column, @@ -791,14 +792,14 @@ def insert_text_range( # # cursor_destination = (destination_row, destination_column) # - # start_byte = self._position_to_byte_offset(from_position) + # start_byte = self._location_to_byte_offset(from_location) # if self._syntax_tree is not None: # self._syntax_tree.edit( # start_byte=start_byte, - # old_end_byte=self._position_to_byte_offset(to_position), + # old_end_byte=self._location_to_byte_offset(to_location), # new_end_byte=start_byte + len(inserted_text), - # start_point=from_position, - # old_end_point=to_position, + # start_point=from_location, + # old_end_point=to_location, # new_end_point=cursor_destination, # ) # self._syntax_tree = self._parser.parse( @@ -809,12 +810,12 @@ def insert_text_range( # if move_cursor: # self.selection = Selection.cursor(cursor_destination) - # def _position_to_byte_offset(self, position: tuple[int, int]) -> int: + # def _location_to_byte_offset(self, location: tuple[int, int]) -> int: # """Given a document coordinate, return the byte offset of that coordinate.""" # # # TODO - this assumes all line endings are a single byte `\n` # lines = self.document_lines - # row, column = position + # row, column = location # lines_above = lines[:row] # bytes_lines_above = sum(len(line.encode("utf-8")) + 1 for line in lines_above) # bytes_this_line_left_of_cursor = len(lines[row][:column].encode("utf-8")) @@ -848,31 +849,31 @@ def dedent_line(self) -> None: def delete_range( self, - from_position: tuple[int, int], - to_position: tuple[int, int], + from_location: tuple[int, int], + to_location: tuple[int, int], cursor_destination: tuple[int, int] | None = None, ) -> str: - top, bottom = _fix_direction(from_position, to_position) + top, bottom = _fix_direction(from_location, to_location) return self.edit(Delete(top, bottom, cursor_destination)) # def _delete_range( # self, - # from_position: tuple[int, int], - # to_position: tuple[int, int], + # from_location: tuple[int, int], + # to_location: tuple[int, int], # cursor_destination: tuple[int, int] | None, # ) -> str: - # """Delete text between `from_position` and `to_position`. + # """Delete text between `from_location` and `to_location`. # - # `from_position` is inclusive. The `to_position` is exclusive. + # `from_location` is inclusive. The `to_location` is exclusive. # # Returns: # A string containing the deleted text. # """ - # from_row, from_column = from_position - # to_row, to_column = to_position + # from_row, from_column = from_location + # to_row, to_column = to_location # - # start_byte = self._position_to_byte_offset(from_position) - # old_end_byte = self._position_to_byte_offset(to_position) + # start_byte = self._location_to_byte_offset(from_location) + # old_end_byte = self._location_to_byte_offset(to_location) # # lines = self.document_lines # @@ -905,9 +906,9 @@ def delete_range( # start_byte=start_byte, # old_end_byte=old_end_byte, # new_end_byte=old_end_byte - len(deleted_text), - # start_point=from_position, - # old_end_point=to_position, - # new_end_point=from_position, + # start_point=from_location, + # old_end_point=to_location, + # new_end_point=from_location, # ) # self._syntax_tree = self._parser.parse( # self._read_callable, self._syntax_tree @@ -925,7 +926,7 @@ def delete_range( # return deleted_text def action_delete_left(self) -> None: - """Deletes the character to the left of the cursor and updates the cursor position.""" + """Deletes the character to the left of the cursor and updates the cursor location.""" selection = self.selection empty = selection.is_empty @@ -945,7 +946,7 @@ def action_delete_left(self) -> None: self.delete_range(start, end) def action_delete_right(self) -> None: - """Deletes the character to the right of the cursor and keeps the cursor at the same position.""" + """Deletes the character to the right of the cursor and keeps the cursor at the same location.""" if self.cursor_at_end_of_document: return @@ -953,11 +954,11 @@ def action_delete_right(self) -> None: end_row, end_column = end if self.cursor_at_end_of_row: - to_position = (end_row + 1, 0) + to_location = (end_row + 1, 0) else: - to_position = (end_row, end_column + 1) + to_location = (end_row, end_column + 1) - self.delete_range(start, to_position) + self.delete_range(start, to_location) def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" @@ -965,27 +966,27 @@ def action_delete_line(self) -> None: start_row, start_column = start end_row, end_column = end - from_position = (start_row, 0) - to_position = (end_row + 1, 0) + from_location = (start_row, 0) + to_location = (end_row + 1, 0) - self.delete_range(from_position, to_position) + self.delete_range(from_location, to_location) def action_delete_to_start_of_line(self) -> None: - """Deletes from the cursor position to the start of the line.""" - from_position = self.selection.end - cursor_row, cursor_column = from_position - to_position = (cursor_row, 0) - self.delete_range(from_position, to_position) + """Deletes from the cursor location to the start of the line.""" + from_location = self.selection.end + cursor_row, cursor_column = from_location + to_location = (cursor_row, 0) + self.delete_range(from_location, to_location) def action_delete_to_end_of_line(self) -> None: - """Deletes from the cursor position to the end of the line.""" - from_position = self.selection.end - cursor_row, cursor_column = from_position - to_position = (cursor_row, len(self._document[cursor_row])) - self.delete_range(from_position, to_position) + """Deletes from the cursor location to the end of the line.""" + from_location = self.selection.end + cursor_row, cursor_column = from_location + to_location = (cursor_row, len(self._document[cursor_row])) + self.delete_range(from_location, to_location) def action_delete_word_left(self) -> None: - """Deletes the word to the left of the cursor and updates the cursor position.""" + """Deletes the word to the left of the cursor and updates the cursor location.""" if self.cursor_at_start_of_document: return @@ -1003,18 +1004,18 @@ def action_delete_word_left(self) -> None: if matches: # If a word boundary is found, delete the word - from_position = (cursor_row, matches[-1].start()) + from_location = (cursor_row, matches[-1].start()) elif cursor_row > 0: # If no word boundary is found, and we're not on the first line, delete to the end of the previous line - from_position = (cursor_row - 1, len(self._document[cursor_row - 1])) + from_location = (cursor_row - 1, len(self._document[cursor_row - 1])) else: # If we're already on the first line and no word boundary is found, delete to the start of the line - from_position = (cursor_row, 0) + from_location = (cursor_row, 0) - self.delete_range(from_position, self.selection.end) + self.delete_range(from_location, self.selection.end) def action_delete_word_right(self) -> None: - """Deletes the word to the right of the cursor and keeps the cursor at the same position.""" + """Deletes the word to the right of the cursor and keeps the cursor at the same location.""" if self.cursor_at_end_of_document: return @@ -1030,15 +1031,15 @@ def action_delete_word_right(self) -> None: if matches: # If a word boundary is found, delete the word - to_position = (cursor_row, cursor_column + matches[0].end()) + to_location = (cursor_row, cursor_column + matches[0].end()) elif cursor_row < self._document.line_count - 1: # If no word boundary is found, and we're not on the last line, delete to the start of the next line - to_position = (cursor_row + 1, 0) + to_location = (cursor_row + 1, 0) else: # If we're already on the last line and no word boundary is found, delete to the end of the line - to_position = (cursor_row, len(self._document[cursor_row])) + to_location = (cursor_row, len(self._document[cursor_row])) - self.delete_range(end, to_position) + self.delete_range(end, to_location) # --- Debugging @dataclass From d8dcc5ecac3b1e152837c6e92eb59a89331737ef Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 25 Jul 2023 14:30:36 +0100 Subject: [PATCH 091/366] Moving insertion syntax stuff into document --- .../document/_syntax_aware_document.py | 66 ++++- src/textual/widgets/_text_area.py | 241 ++---------------- 2 files changed, 80 insertions(+), 227 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 5bc386d502..19b92f2e86 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -9,7 +9,7 @@ from tree_sitter import Language, Parser, Tree from tree_sitter.binding import Query -from textual import log +from textual._fix_direction import _fix_direction from textual.document._document import Document # TODO - remove hardcoded python.scm highlight query file @@ -80,24 +80,76 @@ def insert_range( ) -> tuple[int, int]: end_location = super().insert_range(start, end, text) + top, bottom = _fix_direction(start, end) + + start_byte = self._location_to_byte_offset(top) + text_byte_length = len(text.encode("utf-8")) + + if self._syntax_tree is not None: + self._syntax_tree.edit( + start_byte=start_byte, + old_end_byte=self._location_to_byte_offset(bottom), + new_end_byte=start_byte + text_byte_length, + start_point=top, + old_end_point=bottom, + new_end_point=end_location, + ) + self._syntax_tree = self._parser.parse( + self._read_callable, self._syntax_tree + ) + self._prepare_highlights() + + return end_location + def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: + """Delete text between `start` and `end`. + + This will update the internal syntax tree of the document, refreshing + the syntax highlighting data. Calling `get_line` will now return a Text + object with new highlights corresponding to this change. + + Returns: + A string containing the deleted text. + """ deleted_text = super().delete_range(start, end) - # TODO - apply edits to the ast + top, bottom = _fix_direction(start, end) + start_byte = self._location_to_byte_offset(top) + old_end_byte = self._location_to_byte_offset(bottom) + + if self._syntax_tree is not None: + self._syntax_tree.edit( + start_byte=start_byte, + old_end_byte=old_end_byte, + new_end_byte=old_end_byte - len(deleted_text), + start_point=top, + old_end_point=bottom, + new_end_point=top, + ) + self._syntax_tree = self._parser.parse( + self._read_callable, self._syntax_tree + ) + self._prepare_highlights() + + return deleted_text + + def _location_to_byte_offset(self, location: tuple[int, int]) -> int: + """Given a document coordinate, return the byte offset of that coordinate.""" + lines = self._lines + row, column = location + lines_above = lines[:row] + bytes_lines_above = sum(len(line.encode("utf-8")) + 1 for line in lines_above) + bytes_this_line_left_of_cursor = len(lines[row][:column].encode("utf-8")) + return bytes_lines_above + bytes_this_line_left_of_cursor def _prepare_highlights( self, start_point: tuple[int, int] | None = None, end_point: tuple[int, int] = None, ) -> None: - # TODO - we're ignoring get changed ranges for now. Either I'm misunderstanding - # it or I've made a mistake somewhere with AST editing. - highlights = self._highlights query: Query = self._language.query(self._highlights_query) - log.debug(f"capturing nodes in range {start_point!r} -> {end_point!r}") - captures_kwargs = {} if start_point is not None: captures_kwargs["start_point"] = start_point diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 08e2333949..469fea85d1 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1,23 +1,17 @@ from __future__ import annotations import re -from collections import defaultdict from dataclasses import dataclass, field -from pathlib import Path -from typing import ClassVar, NamedTuple +from typing import ClassVar -from rich.style import Style from rich.text import Text -from tree_sitter import Language, Parser, Tree -from tree_sitter.binding import Query -from textual import events, log +from textual import events from textual._cells import cell_len from textual._fix_direction import _fix_direction from textual._types import Protocol, runtime_checkable from textual.binding import Binding from textual.document._document import Document, Selection -from textual.document._syntax_aware_document import Highlight from textual.geometry import Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView @@ -80,15 +74,15 @@ class Delete: cursor_destination: tuple[int, int] | None = None """Where to move the cursor to after the deletion.""" - deleted_text: str | None = None + _deleted_text: str | None = field(init=False, default=None) """The text that was deleted, or None if the deletion hasn't occurred yet.""" def do(self, editor: TextArea) -> str: """Do the delete action and record the text that was deleted.""" - self.deleted_text = editor._document.delete_range( + self._deleted_text = editor._document.delete_range( self.from_location, self.to_location ) - return self.deleted_text + return self._deleted_text def undo(self, editor: TextArea) -> None: """Undo the delete action.""" @@ -100,12 +94,6 @@ def post_refresh(self, editor: TextArea) -> None: else: editor.selection = Selection.cursor(self.from_location) - def __rich_repr__(self): - yield "from_location", self.from_location - yield "to_location", self.to_location - if hasattr(self, "deleted_text"): - yield "deleted_text", self.deleted_text - class TextArea(ScrollView, can_focus=True): DEFAULT_CSS = """\ @@ -217,7 +205,6 @@ def __init__( """True if we're currently selecting text, otherwise False.""" def watch__document_size(self, size: Size) -> None: - log.debug(f"document size set to {size!r} ") document_width, document_height = size self.virtual_size = Size(document_width + self.gutter_width, document_height) @@ -230,26 +217,6 @@ def load_text(self, text: str) -> None: self._document.load_text(text) self._refresh_size() - # - # def load_lines(self, lines: list[str]) -> None: - # """Load text from a list of lines into the editor. - # - # This will replace any previously loaded lines.""" - # - # # TODO - migrate the highlighting stuff out of here into the Document subclass - # self.document_lines = lines - # self._document_size = self._get_document_size(lines) - # if self._parser is not None: - # self._syntax_tree = self._build_ast(self._parser) - # self._prepare_highlights() - # - # log.debug(f"loaded text. parser = {self._parser} ast = {self._syntax_tree}") - - def clear(self) -> None: - # TODO: Perform a delete on the whole document to ensure this is - # integrated with undo/redo. - self.load_text("") - def _refresh_size(self) -> None: width, height = self._document.size # +1 to reserve cursor end-of-line resting space. @@ -279,6 +246,7 @@ def render_line(self, widget_y: int) -> Strip: selection_bottom = max(start, end) selection_top_row, selection_top_column = selection_top selection_bottom_row, selection_bottom_column = selection_bottom + if start != end and selection_top_row <= line_index <= selection_bottom_row: # If this row is part of the selection if line_index == selection_top_row == selection_bottom_row: @@ -354,7 +322,6 @@ def gutter_width(self) -> int: return gutter_longest_number def edit(self, edit: Edit) -> object | None: - log.debug(f"performing edit {edit!r}") result = edit.do(self) # TODO: Think about this... @@ -362,7 +329,6 @@ def edit(self, edit: Edit) -> object | None: self._undo_stack = self._undo_stack[-20:] self._refresh_size() - edit.post_refresh(self) return result @@ -374,9 +340,10 @@ def undo(self) -> None: # --- Lower level event/key handling def _on_key(self, event: events.Key) -> None: - log.debug(f"{event!r}") key = event.key if event.is_printable or key == "tab" or key == "enter": + event.stop() + event.prevent_default() if key == "tab": insert = " " elif key == "enter": @@ -386,11 +353,6 @@ def _on_key(self, event: events.Key) -> None: assert event.character is not None start, end = self.selection self.insert_text_range(insert, start, end) - event.stop() - event.prevent_default() - elif key == "shift+tab": - event.stop() - self.dedent_line() def get_target_document_location(self, offset: Offset) -> tuple[int, int]: target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0) @@ -402,7 +364,6 @@ def get_target_document_location(self, offset: Offset) -> tuple[int, int]: return target_row, target_column def _on_mouse_down(self, event: events.MouseDown) -> None: - event.stop() offset = event.get_content_offset(self) if offset is not None: target = self.get_target_document_location(offset) @@ -410,7 +371,6 @@ def _on_mouse_down(self, event: events.MouseDown) -> None: self._selecting = True def _on_mouse_move(self, event: events.MouseMove) -> None: - event.stop() if self._selecting: offset = event.get_content_offset(self) if offset is not None: @@ -419,12 +379,10 @@ def _on_mouse_move(self, event: events.MouseMove) -> None: self.selection = Selection(selection_start, target) def _on_mouse_up(self, event: events.MouseUp) -> None: - event.stop() self._record_last_intentional_cell_width() self._selecting = False def _on_paste(self, event: events.Paste) -> None: - event.stop() text = event.text if text: self.insert_text(text, self.selection) @@ -750,194 +708,37 @@ def insert_text_range( from_location: tuple[int, int], to_location: tuple[int, int], cursor_destination: tuple[int, int] | None = None, - ): + ) -> None: self.edit(Insert(text, from_location, to_location, cursor_destination)) - # def _insert_text_range( - # self, - # text: str, - # from_location: tuple[int, int], - # to_location: tuple[int, int], - # move_cursor: bool = True, - # ) -> None: - # """Insert text at a given range and move the cursor to the end of the inserted text.""" - # - # inserted_text = text - # lines = self.document_lines - # - # from_row, from_column = from_location - # to_row, to_column = to_location - # - # if from_location > to_location: - # from_row, from_column, to_row, to_column = ( - # to_row, - # to_column, - # from_row, - # from_column, - # ) - # - # insert_lines = inserted_text.splitlines() - # if inserted_text.endswith("\n"): - # # Special case where a single newline character is inserted. - # insert_lines.append("") - # - # before_selection = lines[from_row][:from_column] - # after_selection = lines[to_row][to_column:] - # - # insert_lines[0] = before_selection + insert_lines[0] - # destination_column = len(insert_lines[-1]) - # insert_lines[-1] = insert_lines[-1] + after_selection - # lines[from_row : to_row + 1] = insert_lines - # destination_row = from_row + len(insert_lines) - 1 - # - # cursor_destination = (destination_row, destination_column) - # - # start_byte = self._location_to_byte_offset(from_location) - # if self._syntax_tree is not None: - # self._syntax_tree.edit( - # start_byte=start_byte, - # old_end_byte=self._location_to_byte_offset(to_location), - # new_end_byte=start_byte + len(inserted_text), - # start_point=from_location, - # old_end_point=to_location, - # new_end_point=cursor_destination, - # ) - # self._syntax_tree = self._parser.parse( - # self._read_callable, self._syntax_tree - # ) - # self._prepare_highlights() - # self._refresh_size() - # if move_cursor: - # self.selection = Selection.cursor(cursor_destination) - - # def _location_to_byte_offset(self, location: tuple[int, int]) -> int: - # """Given a document coordinate, return the byte offset of that coordinate.""" - # - # # TODO - this assumes all line endings are a single byte `\n` - # lines = self.document_lines - # row, column = location - # lines_above = lines[:row] - # bytes_lines_above = sum(len(line.encode("utf-8")) + 1 for line in lines_above) - # bytes_this_line_left_of_cursor = len(lines[row][:column].encode("utf-8")) - # return bytes_lines_above + bytes_this_line_left_of_cursor - - def dedent_line(self) -> None: - """Reduces the indentation of the current line by one level. - - A dedent is simply a Delete operation on some amount of whitespace - which may exist at the start of a line. - """ - # cursor_row, cursor_column = self.selection.end - # - # # Define one level of indentation as four spaces - # indent_level = " " * 4 - # - # current_line = self.document[cursor_row] - # - # # If the line is indented, reduce the indentation - # # TODO - if the line is less than the indent level we should just dedent as far as possible. - # if current_line.startswith(indent_level): - # self.document_lines[cursor_row] = current_line[len(indent_level) :] - # - # if cursor_column > len(current_line): - # self.selection = Selection.cursor((cursor_row, len(current_line))) - # - # self._refresh_size() - # self.refresh() - # TODO - reimplement with new Document API - pass - def delete_range( self, from_location: tuple[int, int], to_location: tuple[int, int], cursor_destination: tuple[int, int] | None = None, ) -> str: + """Delete text between from_location and to_location.""" top, bottom = _fix_direction(from_location, to_location) - return self.edit(Delete(top, bottom, cursor_destination)) - - # def _delete_range( - # self, - # from_location: tuple[int, int], - # to_location: tuple[int, int], - # cursor_destination: tuple[int, int] | None, - # ) -> str: - # """Delete text between `from_location` and `to_location`. - # - # `from_location` is inclusive. The `to_location` is exclusive. - # - # Returns: - # A string containing the deleted text. - # """ - # from_row, from_column = from_location - # to_row, to_column = to_location - # - # start_byte = self._location_to_byte_offset(from_location) - # old_end_byte = self._location_to_byte_offset(to_location) - # - # lines = self.document_lines - # - # # If the range is within a single line - # if from_row == to_row: - # line = lines[from_row] - # deleted_text = line[from_column:to_column] - # lines[from_row] = line[:from_column] + line[to_column:] - # else: - # # The range spans multiple lines - # start_line = lines[from_row] - # end_line = lines[to_row] - # - # deleted_text = start_line[from_column:] + "\n" - # for row in range(from_row + 1, to_row): - # deleted_text += lines[row] + "\n" - # - # deleted_text += end_line[:to_column] - # if to_column == len(end_line): - # deleted_text += "\n" - # - # # Update the lines at the start and end of the range - # lines[from_row] = start_line[:from_column] + end_line[to_column:] - # - # # Delete the lines in between - # del lines[from_row + 1 : to_row + 1] - # - # if self._syntax_tree is not None: - # self._syntax_tree.edit( - # start_byte=start_byte, - # old_end_byte=old_end_byte, - # new_end_byte=old_end_byte - len(deleted_text), - # start_point=from_location, - # old_end_point=to_location, - # new_end_point=from_location, - # ) - # self._syntax_tree = self._parser.parse( - # self._read_callable, self._syntax_tree - # ) - # self._prepare_highlights() - # - # self._refresh_size() - # - # if cursor_destination is not None: - # self.selection = Selection.cursor(cursor_destination) - # else: - # # Move the cursor to the start of the deleted range - # self.selection = Selection.cursor((from_row, from_column)) - # - # return deleted_text + deleted_text = self.edit(Delete(top, bottom, cursor_destination)) + return deleted_text + + def clear(self) -> None: + # TODO: Perform a delete on the whole document to ensure this is + # integrated with undo/redo. + self.delete_range() def action_delete_left(self) -> None: """Deletes the character to the left of the cursor and updates the cursor location.""" selection = self.selection - empty = selection.is_empty - - if self.cursor_at_start_of_document and empty: - return start, end = selection end_row, end_column = end - if empty: + if selection.is_empty: + if self.cursor_at_start_of_document: + return + if self.cursor_at_start_of_row: end = (end_row - 1, len(self._document[end_row - 1])) else: From 791d4e301131ea5afa9405f89912e57806724c1c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 25 Jul 2023 16:53:14 +0100 Subject: [PATCH 092/366] Making clear a delete operation --- src/textual/document/_document.py | 1 + src/textual/widgets/_text_area.py | 62 +++++++++++++------------- tests/document/test_document_delete.py | 10 ----- 3 files changed, 32 insertions(+), 41 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 6045ff0d0f..b793f2b8b6 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -159,6 +159,7 @@ def is_empty(self) -> bool: start, end = self return start == end + @property def range(self) -> tuple[tuple[int, int], tuple[int, int]]: start, end = self return _fix_direction(start, end) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 469fea85d1..7483fd5f01 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -206,7 +206,9 @@ def __init__( def watch__document_size(self, size: Size) -> None: document_width, document_height = size - self.virtual_size = Size(document_width + self.gutter_width, document_height) + self.virtual_size = Size( + document_width + self.gutter_width + 1, document_height + ) def load_text(self, text: str) -> None: """Load text from a string into the editor. @@ -218,9 +220,7 @@ def load_text(self, text: str) -> None: self._refresh_size() def _refresh_size(self) -> None: - width, height = self._document.size - # +1 to reserve cursor end-of-line resting space. - self._document_size = Size(width + 1, height) + self._document_size = self._document.size def render_line(self, widget_y: int) -> Strip: document = self._document @@ -234,21 +234,15 @@ def render_line(self, widget_y: int) -> Strip: codepoint_count = len(line) line.set_length(self.virtual_size.width) - start, end = self.selection - end_row, end_column = end - - selection_style = self.get_component_rich_style("text-area--selection") - - # Start and end can be before or after each other, depending on the direction - # you move the cursor during selecting text, but the "top" of the selection - # is always before the "bottom" of the selection. - selection_top = min(start, end) - selection_bottom = max(start, end) + selection = self.selection + start, end = selection + selection_top, selection_bottom = selection.range selection_top_row, selection_top_column = selection_top selection_bottom_row, selection_bottom_column = selection_bottom if start != end and selection_top_row <= line_index <= selection_bottom_row: - # If this row is part of the selection + # If this row intersects with the selection range + selection_style = self.get_component_rich_style("text-area--selection") if line_index == selection_top_row == selection_bottom_row: # Selection within a single line line.stylize_before( @@ -269,16 +263,17 @@ def render_line(self, widget_y: int) -> Strip: else: line.stylize_before(selection_style, end=codepoint_count) - # Show the cursor and the selection - if end_row == line_index: + # Highlight the cursor + cursor_row, cursor_column = end + if cursor_row == line_index: cursor_style = self.get_component_rich_style("text-area--cursor") - line.stylize(cursor_style, end_column, end_column + 1) + line.stylize(cursor_style, cursor_column, cursor_column + 1) active_line_style = self.get_component_rich_style("text-area--active-line") line.stylize_before(active_line_style) - # Show the gutter + # Build the gutter text for this line if self.show_line_numbers: - if end_row == line_index: + if cursor_row == line_index: gutter_style = self.get_component_rich_style( "text-area--active-line-gutter" ) @@ -294,11 +289,13 @@ def render_line(self, widget_y: int) -> Strip: else: gutter = Text("", end="") + # Render the gutter and the text of this line gutter_segments = self.app.console.render(gutter) text_segments = self.app.console.render( line, self.app.console.options.update_width(self.virtual_size.width) ) + # Crop the line to show only the visible part (some may be scrolled out of view) virtual_width, virtual_height = self.virtual_size text_crop_start = int(self.scroll_x) text_crop_end = text_crop_start + virtual_width @@ -306,8 +303,8 @@ def render_line(self, widget_y: int) -> Strip: gutter_strip = Strip(gutter_segments) text_strip = Strip(text_segments).crop(text_crop_start, text_crop_end) + # Join and return the gutter and the visible portion of this line strip = Strip.join([gutter_strip, text_strip]).simplify() - return strip @property @@ -385,7 +382,8 @@ def _on_mouse_up(self, event: events.MouseUp) -> None: def _on_paste(self, event: events.Paste) -> None: text = event.text if text: - self.insert_text(text, self.selection) + start, end = self.selection + self.insert_text_range(text, start, end, end) def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: """Return the column that the cell width corresponds to on the given row.""" @@ -440,7 +438,7 @@ def clamp_visitable(self, location: tuple[int, int]) -> tuple[int, int]: def scroll_cursor_visible(self): row, column = self.selection.end - text = self.active_line_text[:column] + text = self.cursor_line_text[:column] column_offset = cell_len(text) self.scroll_to_region( Region(x=column_offset, y=row, width=3, height=1), @@ -640,7 +638,7 @@ def action_cursor_left_word(self) -> None: # If a word boundary is found, move the cursor there cursor_column = matches[-1].start() elif cursor_row > 0: - # If no word boundary is found and we're not on the first line, move to the end of the previous line + # If no word boundary is found, and we're not on the first line, move to the end of the previous line cursor_row -= 1 cursor_column = len(self._document[cursor_row]) else: @@ -677,7 +675,7 @@ def action_cursor_right_word(self) -> None: self._record_last_intentional_cell_width() @property - def active_line_text(self) -> str: + def cursor_line_text(self) -> str: # TODO - consider empty documents return self._document[self.selection.end[0]] @@ -723,9 +721,11 @@ def delete_range( return deleted_text def clear(self) -> None: - # TODO: Perform a delete on the whole document to ensure this is - # integrated with undo/redo. - self.delete_range() + """Clear the document.""" + document = self._document + last_line = document[-1] + document_end_location = (document.line_count, len(last_line)) + self.delete_range((0, 0), document_end_location) def action_delete_left(self) -> None: """Deletes the character to the left of the cursor and updates the cursor location.""" @@ -855,7 +855,7 @@ class EditorDebug: active_line_text: str active_line_cell_len: int - def debug_state(self) -> "EditorDebug": + def _debug_state(self) -> "EditorDebug": return self.EditorDebug( cursor=self.selection, language=self.language, @@ -865,6 +865,6 @@ def debug_state(self) -> "EditorDebug": undo_stack=list(reversed(self._undo_stack)), # tree_sexp=self._syntax_tree.root_node.sexp(), tree_sexp="", - active_line_text=repr(self.active_line_text), - active_line_cell_len=cell_len(self.active_line_text), + active_line_text=repr(self.cursor_line_text), + active_line_cell_len=cell_len(self.cursor_line_text), ) diff --git a/tests/document/test_document_delete.py b/tests/document/test_document_delete.py index 4fb398bd83..bac71dc8c7 100644 --- a/tests/document/test_document_delete.py +++ b/tests/document/test_document_delete.py @@ -102,13 +102,3 @@ def test_delete_single_line_including_newline(document): "Fear is the mind-killer.", "Sorry Will.", ] - - -def test_delete_single_character_start_of_document(): - """Check deletion of the first character in the document""" - pass - - -def test_delete_single_character_end_of_document_newline(): - """Check deleting the newline character at the end of the document""" - pass From 58fa4b363b94e055d96afb870b17362ee40d7f42 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 26 Jul 2023 11:37:07 +0100 Subject: [PATCH 093/366] Test fixups --- src/textual/document/_document.py | 15 +++++--- .../widgets/{text_editor.py => text_area.py} | 0 tests/document/test_document_delete.py | 36 +++++++++++-------- 3 files changed, 32 insertions(+), 19 deletions(-) rename src/textual/widgets/{text_editor.py => text_area.py} (100%) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index b793f2b8b6..ffda70c056 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -13,6 +13,7 @@ class Document: def __init__(self) -> None: self._lines: list[str] = [] + self._eof_newline = False @property def lines(self) -> list[str]: @@ -26,7 +27,7 @@ def load_text(self, text: str) -> None: """ lines = text.splitlines(keepends=False) if text[-1] == "\n": - lines.append("") + self._eof_newline = True self._lines = lines @@ -94,14 +95,20 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: else: # The deletion range spans multiple lines. start_line = lines[top_row] - end_line = lines[bottom_row] + if bottom_row == self.line_count: + end_line = "" + else: + end_line = lines[bottom_row] deleted_text = start_line[top_column:] + "\n" for row in range(top_row + 1, bottom_row): - deleted_text += lines[row] + "\n" + deleted_text += lines[row] + # Never add the newline at the end without checking its presence: + if row != self.line_count - 1: + deleted_text += "\n" deleted_text += end_line[:bottom_column] - if bottom_column == len(end_line): + if bottom == (self.line_count, 0) and self._eof_newline: deleted_text += "\n" # Update the lines at the start and end of the range diff --git a/src/textual/widgets/text_editor.py b/src/textual/widgets/text_area.py similarity index 100% rename from src/textual/widgets/text_editor.py rename to src/textual/widgets/text_area.py diff --git a/tests/document/test_document_delete.py b/tests/document/test_document_delete.py index bac71dc8c7..c05cedaccb 100644 --- a/tests/document/test_document_delete.py +++ b/tests/document/test_document_delete.py @@ -18,7 +18,7 @@ def document(): def test_delete_single_character(document): deleted_text = document.delete_range((0, 0), (0, 1)) assert deleted_text == "I" - assert document._lines == [ + assert document.lines == [ " must not fear.", "Fear is the mind-killer.", "I forgot the rest of the quote.", @@ -30,29 +30,35 @@ def test_delete_single_newline(document): """Testing deleting newline from right to left""" deleted_text = document.delete_range((1, 0), (0, 16)) assert deleted_text == "\n" - assert document._lines == [ + assert document.lines == [ "I must not fear.Fear is the mind-killer.", "I forgot the rest of the quote.", "Sorry Will.", ] -def test_delete_single_character_end_of_document_newline(document): - """Check deleting the newline character at the end of the document""" - deleted_text = document.delete_range((1, 0), (0, 16)) - assert deleted_text == "\n" - assert document._lines == [ +def test_delete_near_end_of_document(document): + """Test deleting a range near the end of a document.""" + deleted_text = document.delete_range((1, 0), (3, 11)) + assert deleted_text == ( + "Fear is the mind-killer.\n" "I forgot the rest of the quote.\n" "Sorry Will." + ) + assert document.lines == [ "I must not fear.", - "Fear is the mind-killer.", - "I forgot the rest of the quote.", - "Sorry Will.", + "", ] +def test_delete_clearing_the_document(document): + deleted_text = document.delete_range((0, 0), (4, 0)) + assert deleted_text == TEXT + assert document.lines == [""] + + def test_delete_multiple_characters_on_one_line(document): deleted_text = document.delete_range((0, 2), (0, 7)) assert deleted_text == "must " - assert document._lines == [ + assert document.lines == [ "I not fear.", "Fear is the mind-killer.", "I forgot the rest of the quote.", @@ -64,7 +70,7 @@ def test_delete_multiple_lines_partially_spanned(document): """Deleting a selection that partially spans the first and final lines of the selection.""" deleted_text = document.delete_range((0, 2), (2, 2)) assert deleted_text == "must not fear.\nFear is the mind-killer.\nI " - assert document._lines == [ + assert document.lines == [ "I forgot the rest of the quote.", "Sorry Will.", ] @@ -74,7 +80,7 @@ def test_delete_end_of_line(document): """Testing deleting newline from left to right""" deleted_text = document.delete_range((0, 16), (1, 0)) assert deleted_text == "\n" - assert document._lines == [ + assert document.lines == [ "I must not fear.Fear is the mind-killer.", "I forgot the rest of the quote.", "Sorry Will.", @@ -85,7 +91,7 @@ def test_delete_single_line_excluding_newline(document): """Delete from the start to the end of the line.""" deleted_text = document.delete_range((2, 0), (2, 31)) assert deleted_text == "I forgot the rest of the quote." - assert document._lines == [ + assert document.lines == [ "I must not fear.", "Fear is the mind-killer.", "", @@ -97,7 +103,7 @@ def test_delete_single_line_including_newline(document): """Delete from the start of a line to the start of the line below.""" deleted_text = document.delete_range((2, 0), (3, 0)) assert deleted_text == "I forgot the rest of the quote.\n" - assert document._lines == [ + assert document.lines == [ "I must not fear.", "Fear is the mind-killer.", "Sorry Will.", From 24a44419a03352c96cd3f65e51b9c973926107c1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 26 Jul 2023 15:46:04 +0100 Subject: [PATCH 094/366] Page up and page down --- src/textual/widgets/_text_area.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 7483fd5f01..b04a68a0a6 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -132,19 +132,22 @@ class TextArea(ScrollView, can_focus=True): BINDINGS = [ # Cursor movement Binding("up", "cursor_up", "cursor up", show=False), - Binding("shift+up", "cursor_up_select", "cursor up select", show=False), Binding("down", "cursor_down", "cursor down", show=False), - Binding("shift+down", "cursor_down_select", "cursor down select", show=False), Binding("left", "cursor_left", "cursor left", show=False), - Binding("shift+left", "cursor_left_select", "cursor left select", show=False), Binding("ctrl+left", "cursor_left_word", "cursor left word", show=False), Binding("right", "cursor_right", "cursor right", show=False), - Binding( - "shift+right", "cursor_right_select", "cursor right select", show=False - ), Binding("ctrl+right", "cursor_right_word", "cursor right word", show=False), Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False), Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False), + Binding("pageup", "cursor_page_up", "cursor page up", show=False), + Binding("pagedown", "cursor_page_down", "cursor page down", show=False), + # Selection with the cursor + Binding("shift+up", "cursor_up_select", "cursor up select", show=False), + Binding("shift+down", "cursor_down_select", "cursor down select", show=False), + Binding("shift+left", "cursor_left_select", "cursor left select", show=False), + Binding( + "shift+right", "cursor_right_select", "cursor right select", show=False + ), # Deletion Binding("backspace", "delete_left", "delete left", show=False), Binding( @@ -674,6 +677,22 @@ def action_cursor_right_word(self) -> None: self.selection = Selection.cursor((cursor_row, cursor_column)) self._record_last_intentional_cell_width() + def action_cursor_page_up(self) -> None: + height = self.content_size.height + _, cursor_location = self.selection + row, column = cursor_location + target = (row - height, column) + self.scroll_y -= height + self.selection = Selection.cursor(target) + + def action_cursor_page_down(self) -> None: + height = self.content_size.height + _, cursor_location = self.selection + row, column = cursor_location + target = (row + height, column) + self.scroll_y += height + self.selection = Selection.cursor(target) + @property def cursor_line_text(self) -> str: # TODO - consider empty documents From e84f041ae308120958979c4fbd1c6e67410d211b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 26 Jul 2023 17:11:46 +0100 Subject: [PATCH 095/366] Implement reasonable behaviour for when drag-select goes out of bounds --- src/textual/events.py | 13 +++++++++++++ src/textual/widgets/_text_area.py | 10 ++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/textual/events.py b/src/textual/events.py index 4e2523535d..3d92356d61 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -416,6 +416,19 @@ def get_content_offset(self, widget: Widget) -> Offset | None: """ if self.screen_offset not in widget.content_region: return None + return self.get_content_offset_capture(widget) + + def get_content_offset_capture(self, widget: Widget) -> Offset: + """Get offset from a widget's content area. + + This method works even if the offset is outside the widget content region. + + Args: + widget: Widget receiving the event. + + Returns: + An offset where the origin is at the top left of the content area. + """ return self.offset - widget.gutter.top_left def _apply_offset(self, x: int, y: int) -> MouseEvent: diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index b04a68a0a6..1fc0efa435 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -364,23 +364,25 @@ def get_target_document_location(self, offset: Offset) -> tuple[int, int]: return target_row, target_column def _on_mouse_down(self, event: events.MouseDown) -> None: - offset = event.get_content_offset(self) + offset = event.get_content_offset_capture(self) if offset is not None: - target = self.get_target_document_location(offset) + target = self.get_target_document_location(event) self.selection = Selection.cursor(target) self._selecting = True + self.capture_mouse(True) def _on_mouse_move(self, event: events.MouseMove) -> None: if self._selecting: - offset = event.get_content_offset(self) + offset = event.get_content_offset_capture(self) if offset is not None: - target = self.get_target_document_location(offset) + target = self.get_target_document_location(event) selection_start, _ = self.selection self.selection = Selection(selection_start, target) def _on_mouse_up(self, event: events.MouseUp) -> None: self._record_last_intentional_cell_width() self._selecting = False + self.capture_mouse(False) def _on_paste(self, event: events.Paste) -> None: text = event.text From bf8189337a529660b064c7c8d5d3b963ca131b43 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 27 Jul 2023 13:32:44 +0100 Subject: [PATCH 096/366] Tab support --- src/textual/_types.py | 2 +- src/textual/app.py | 1 + src/textual/document/_document.py | 16 ++------- .../document/_syntax_aware_document.py | 2 +- src/textual/widgets/_text_area.py | 34 +++++++++++++------ 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/textual/_types.py b/src/textual/_types.py index e8ed4846cb..f4d9bbaca4 100644 --- a/src/textual/_types.py +++ b/src/textual/_types.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Union -from typing_extensions import Protocol, SupportsIndex, runtime_checkable +from typing_extensions import Literal, Protocol, SupportsIndex, runtime_checkable if TYPE_CHECKING: from rich.segment import Segment diff --git a/src/textual/app.py b/src/textual/app.py index 378bd9bcbe..3a198725f1 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -346,6 +346,7 @@ def __init__( _environ=environ, force_terminal=True, safe_box=False, + tab_size=0, ) self._workers = WorkerManager(self) self.error_console = Console(markup=False, stderr=True) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index ffda70c056..6d1fd83e21 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -119,28 +119,16 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: return deleted_text - @property - def size(self) -> Size: - """Returns the size (width, height) of the document.""" - lines = self._lines - text_width = max(cell_len(line) for line in lines) - height = len(lines) - # We add one to the text width to leave a space for the cursor, since it - # can rest at the end of a line where there isn't yet any character. - # Similarly, the cursor can rest below the bottom line of text, where - # a line doesn't currently exist. - return Size(text_width, height) - @property def line_count(self) -> int: """Returns the number of lines in the document""" return len(self._lines) - def get_line(self, index: int) -> Text: + def get_line_text(self, index: int) -> Text: """Returns the line with the given index from the document""" line_string = self[index] line_string = line_string.replace("\n", "").replace("\r", "") - return Text(line_string, end="", tab_size=4) + return Text(line_string, end="") def __getitem__(self, item: SupportsIndex | slice) -> str: return self._lines[item] diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 19b92f2e86..71496afdd1 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -217,7 +217,7 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No return return_value - def get_line(self, line_index: int) -> Text: + def get_line_text(self, line_index: int) -> Text: null_style = Style.null() line = Text(self[line_index]) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 1fc0efa435..8d62c3a282 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -9,7 +9,7 @@ from textual import events from textual._cells import cell_len from textual._fix_direction import _fix_direction -from textual._types import Protocol, runtime_checkable +from textual._types import Literal, Protocol, runtime_checkable from textual.binding import Binding from textual.document._document import Document, Selection from textual.geometry import Offset, Region, Size, Spacing, clamp @@ -173,6 +173,12 @@ class TextArea(ScrollView, can_focus=True): show_line_numbers: Reactive[bool] = reactive(True) """True to show line number gutter, otherwise False.""" + indent_type: Reactive[Literal["tabs", "spaces"]] = "spaces" + """Whether to indent using tabs or spaces.""" + + indent_width: Reactive[int] = reactive(4) + """The width of tabs or the number of spaces to insert on pressing the `tab` key.""" + _document_size: Reactive[Size] = reactive(Size(), init=False, always_update=True) """Tracks the size of the document. @@ -194,9 +200,9 @@ def __init__( """The document this widget is currently editing.""" self._last_intentional_cell_width: int = 0 - """Tracks the last column (measured in terms of cell length, since we care here about where - the cursor visually moves more than the logical characters) the user explicitly navigated to so that we can reset - to it whenever possible.""" + """Tracks the last column (measured in terms of cell length, since we care here about where the cursor + visually moves more than the logical characters) the user explicitly navigated to so that we can reset to it + whenever possible.""" self._word_pattern = re.compile(r"(?<=\W)(?=\w)|(?<=\w)(?=\W)") """Compiled regular expression for what we consider to be a 'word'.""" @@ -223,7 +229,11 @@ def load_text(self, text: str) -> None: self._refresh_size() def _refresh_size(self) -> None: - self._document_size = self._document.size + # Calculate document + lines = self._document.lines + text_width = max(cell_len(line.expandtabs(self.indent_width)) for line in lines) + height = len(lines) + self._document_size = Size(text_width, height) def render_line(self, widget_y: int) -> Strip: document = self._document @@ -233,7 +243,8 @@ def render_line(self, widget_y: int) -> Strip: if out_of_bounds: return Strip.blank(self.size.width) - line = document.get_line(line_index) + line = document.get_line_text(line_index) + line.tab_size = self.indent_width codepoint_count = len(line) line.set_length(self.virtual_size.width) @@ -345,7 +356,9 @@ def _on_key(self, event: events.Key) -> None: event.stop() event.prevent_default() if key == "tab": - insert = " " + insert = ( + " " * self.indent_width if self.indent_type == "spaces" else "\t" + ) elif key == "enter": insert = "\n" else: @@ -392,10 +405,11 @@ def _on_paste(self, event: events.Paste) -> None: def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: """Return the column that the cell width corresponds to on the given row.""" + tab_width = self.indent_width total_cell_offset = 0 line = self._document[row_index] for column_index, character in enumerate(line): - total_cell_offset += cell_len(character) + total_cell_offset += cell_len(character.expandtabs(tab_width)) if total_cell_offset >= cell_width + 1: return column_index return len(line) @@ -444,7 +458,7 @@ def clamp_visitable(self, location: tuple[int, int]) -> tuple[int, int]: def scroll_cursor_visible(self): row, column = self.selection.end text = self.cursor_line_text[:column] - column_offset = cell_len(text) + column_offset = cell_len(text.expandtabs(self.indent_width)) self.scroll_to_region( Region(x=column_offset, y=row, width=3, height=1), spacing=Spacing(right=self.gutter_width), @@ -705,7 +719,7 @@ def get_column_cell_width(self, row: int, column: int) -> int: of the column from the start of the row (the left edge of the editor content area). """ line = self._document[row] - return cell_len(line[:column]) + return cell_len(line[:column].expandtabs(self.indent_width)) def _record_last_intentional_cell_width(self) -> None: row, column = self.selection.end From 2b07a2b328b4f604911d2ae705063378f1e488c9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 27 Jul 2023 13:52:10 +0100 Subject: [PATCH 097/366] indent_type doesnt need to be reactive in TextArea --- src/textual/widgets/_text_area.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 8d62c3a282..fc7e8a202c 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -173,9 +173,6 @@ class TextArea(ScrollView, can_focus=True): show_line_numbers: Reactive[bool] = reactive(True) """True to show line number gutter, otherwise False.""" - indent_type: Reactive[Literal["tabs", "spaces"]] = "spaces" - """Whether to indent using tabs or spaces.""" - indent_width: Reactive[int] = reactive(4) """The width of tabs or the number of spaces to insert on pressing the `tab` key.""" @@ -199,6 +196,9 @@ def __init__( self._document = Document() """The document this widget is currently editing.""" + self.indent_type: Literal["tabs", "spaces"] = "spaces" + """Whether to indent using tabs or spaces.""" + self._last_intentional_cell_width: int = 0 """Tracks the last column (measured in terms of cell length, since we care here about where the cursor visually moves more than the logical characters) the user explicitly navigated to so that we can reset to it From a0ae114b3ebcc0587b64b0fa0a96636436f4f4bc Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 27 Jul 2023 13:59:48 +0100 Subject: [PATCH 098/366] Simplify a method in TextArea (on_key) --- src/textual/document/_document.py | 2 -- src/textual/widgets/_text_area.py | 28 +++++++++++++--------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 6d1fd83e21..df5af2e274 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -4,10 +4,8 @@ from rich.text import Text -from textual._cells import cell_len from textual._fix_direction import _fix_direction from textual._types import SupportsIndex -from textual.geometry import Size class Document: diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index fc7e8a202c..a23609aa30 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -336,33 +336,31 @@ def edit(self, edit: Edit) -> object | None: result = edit.do(self) # TODO: Think about this... - self._undo_stack.append(edit) - self._undo_stack = self._undo_stack[-20:] + # self._undo_stack.append(edit) + # self._undo_stack = self._undo_stack[-20:] self._refresh_size() edit.post_refresh(self) return result - def undo(self) -> None: - if self._undo_stack: - action = self._undo_stack.pop() - action.undo(self) + # def undo(self) -> None: + # if self._undo_stack: + # action = self._undo_stack.pop() + # action.undo(self) # --- Lower level event/key handling def _on_key(self, event: events.Key) -> None: key = event.key - if event.is_printable or key == "tab" or key == "enter": + insert_values = { + "tab": " " * self.indent_width if self.indent_type == "spaces" else "\t", + "enter": "\n", + } + + if event.is_printable or key in insert_values: event.stop() event.prevent_default() - if key == "tab": - insert = ( - " " * self.indent_width if self.indent_type == "spaces" else "\t" - ) - elif key == "enter": - insert = "\n" - else: - insert = event.character + insert = insert_values.get(key, event.character) assert event.character is not None start, end = self.selection self.insert_text_range(insert, start, end) From 929684ce6f96f9e1f88a46467943b55223845ecd Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 27 Jul 2023 15:23:23 +0100 Subject: [PATCH 099/366] Detecting newline characters --- src/textual/_types.py | 8 +++- src/textual/document/_document.py | 37 ++++++++++++++----- .../document/_syntax_aware_document.py | 8 ++-- src/textual/widgets/_text_area.py | 4 +- tests/document/test_document_delete.py | 3 +- tests/document/test_document_insert.py | 27 +++++--------- 6 files changed, 50 insertions(+), 37 deletions(-) diff --git a/src/textual/_types.py b/src/textual/_types.py index f4d9bbaca4..195d415cf9 100644 --- a/src/textual/_types.py +++ b/src/textual/_types.py @@ -1,6 +1,12 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Union -from typing_extensions import Literal, Protocol, SupportsIndex, runtime_checkable +from typing_extensions import ( + Literal, + Protocol, + SupportsIndex, + get_args, + runtime_checkable, +) if TYPE_CHECKING: from rich.segment import Segment diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index df5af2e274..91e678d909 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -5,13 +5,37 @@ from rich.text import Text from textual._fix_direction import _fix_direction -from textual._types import SupportsIndex +from textual._types import Literal, SupportsIndex, get_args + +NewlineStyle = Literal["\r\n", "\n", "\r"] +VALID_NEWLINE_STYLES = set(get_args(NewlineStyle)) + + +def _detect_newline_style(text: str) -> NewlineStyle: + """Return the newline type used in this document. + + Args: + text: The text to inspect. + + Returns: + The NewlineStyle used in the file. + """ + if "\r\n" in text: # Windows newline + return "\r\n" + elif "\n" in text: # Unix/Linux/MacOS newline + return "\n" + elif "\r" in text: # Old MacOS newline + return "\r" + else: + return "\n" # Default to Unix style newline class Document: - def __init__(self) -> None: - self._lines: list[str] = [] - self._eof_newline = False + def __init__(self, text: str) -> None: + self._newline_style = _detect_newline_style(text) + """The type of newline used in the text""" + self._eof_newline = text and text[-1] in VALID_NEWLINE_STYLES + self._lines: list[str] = text.splitlines(keepends=False) @property def lines(self) -> list[str]: @@ -23,11 +47,6 @@ def load_text(self, text: str) -> None: Args: text: The text to load into the document """ - lines = text.splitlines(keepends=False) - if text[-1] == "\n": - self._eof_newline = True - - self._lines = lines def insert_range( self, start: tuple[int, int], end: tuple[int, int], text: str diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 71496afdd1..8b76233351 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -49,8 +49,8 @@ class Highlight(NamedTuple): class SyntaxAwareDocument(Document): - def __init__(self, language: str | None = None): - super().__init__() + def __init__(self, text: str, language: str | None = None): + super().__init__(text) # TODO validate language string @@ -68,10 +68,8 @@ def __init__(self, language: str | None = None): """The tree-sitter query string for used to fetch highlighted ranges""" self._highlights: dict[int, list[Highlight]] = defaultdict(list) - """Mapping line numbers to the set of cached highlights for that line.""" - def load_text(self, text: str) -> None: - super().load_text(text) + """Mapping line numbers to the set of cached highlights for that line.""" self._build_ast(self._parser) self._prepare_highlights() diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index a23609aa30..1c22118398 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -193,7 +193,7 @@ def __init__( super().__init__(name=name, id=id, classes=classes, disabled=disabled) # --- Core editor data - self._document = Document() + self._document = Document("") """The document this widget is currently editing.""" self.indent_type: Literal["tabs", "spaces"] = "spaces" @@ -225,7 +225,7 @@ def load_text(self, text: str) -> None: Args: text: The text to load into the editor. """ - self._document.load_text(text) + self._document = Document(text) self._refresh_size() def _refresh_size(self) -> None: diff --git a/tests/document/test_document_delete.py b/tests/document/test_document_delete.py index c05cedaccb..4ca3a09c23 100644 --- a/tests/document/test_document_delete.py +++ b/tests/document/test_document_delete.py @@ -10,8 +10,7 @@ @pytest.fixture def document(): - document = Document() - document.load_text(TEXT) + document = Document(TEXT) return document diff --git a/tests/document/test_document_insert.py b/tests/document/test_document_insert.py index 237a1a1193..22dcfe3c32 100644 --- a/tests/document/test_document_insert.py +++ b/tests/document/test_document_insert.py @@ -7,8 +7,7 @@ def test_insert_no_newlines(): - document = Document() - document.load_text(TEXT) + document = Document(TEXT) document.insert_range((0, 1), (0, 1), " really") assert document._lines == [ "I really must not fear.", @@ -17,8 +16,7 @@ def test_insert_no_newlines(): def test_insert_empty_string(): - document = Document() - document.load_text(TEXT) + document = Document(TEXT) document.insert_range((0, 1), (0, 1), "") assert document._lines == ["I must not fear.", "Fear is the mind-killer."] @@ -27,8 +25,7 @@ def test_insert_empty_string(): def test_insert_invalid_column(): # TODO - what is the correct behaviour here? # right now it appends to the end of the line if the column is too large. - document = Document() - document.load_text(TEXT) + document = Document(TEXT) document.insert_range((0, 999), (0, 999), " really") assert document._lines == ["I must not fear.", "Fear is the mind-killer."] @@ -36,29 +33,25 @@ def test_insert_invalid_column(): @pytest.mark.xfail(reason="undecided on behaviour") def test_insert_invalid_row(): # TODO - this raises an IndexError for list index out of range - document = Document() - document.load_text(TEXT) + document = Document(TEXT) document.insert_range((999, 0), (999, 0), " really") assert document._lines == ["I must not fear.", "Fear is the mind-killer."] def test_insert_range_newline_file_start(): - document = Document() - document.load_text(TEXT) + document = Document(TEXT) document.insert_range((0, 0), (0, 0), "\n") assert document._lines == ["", "I must not fear.", "Fear is the mind-killer."] def test_insert_newline_splits_line(): - document = Document() - document.load_text(TEXT) + document = Document(TEXT) document.insert_range((0, 1), (0, 1), "\n") assert document._lines == ["I", " must not fear.", "Fear is the mind-killer."] def test_insert_multiple_lines_ends_with_newline(): - document = Document() - document.load_text(TEXT) + document = Document(TEXT) document.insert_range((0, 1), (0, 1), "Hello,\nworld!\n") assert document._lines == [ "IHello,", @@ -69,8 +62,7 @@ def test_insert_multiple_lines_ends_with_newline(): def test_insert_multiple_lines_starts_with_newline(): - document = Document() - document.load_text(TEXT) + document = Document(TEXT) document.insert_range((0, 1), (0, 1), "\nHello,\nworld!\n") assert document._lines == [ "I", @@ -83,8 +75,7 @@ def test_insert_multiple_lines_starts_with_newline(): def test_insert_range_text_no_newlines(): """Ensuring we can do a simple replacement of text.""" - document = Document() - document.load_text(TEXT) + document = Document(TEXT) document.insert_range((0, 2), (0, 6), "MUST") assert document._lines == [ "I MUST not fear.", From 763521a99f5566f09bee457c01cbe876c6c62633 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 27 Jul 2023 16:50:52 +0100 Subject: [PATCH 100/366] Testing newlines at end of file --- src/textual/document/_document.py | 30 ++++++++++++-------------- src/textual/widgets/_text_area.py | 5 ++--- tests/document/test_document_delete.py | 16 ++++++++++++++ tests/document/test_document_insert.py | 15 +++++++++++++ 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 91e678d909..de8b9cfa3f 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -7,11 +7,11 @@ from textual._fix_direction import _fix_direction from textual._types import Literal, SupportsIndex, get_args -NewlineStyle = Literal["\r\n", "\n", "\r"] -VALID_NEWLINE_STYLES = set(get_args(NewlineStyle)) +Newline = Literal["\r\n", "\n", "\r"] +VALID_NEWLINES = set(get_args(Newline)) -def _detect_newline_style(text: str) -> NewlineStyle: +def _detect_newline_style(text: str) -> Newline: """Return the newline type used in this document. Args: @@ -32,22 +32,20 @@ def _detect_newline_style(text: str) -> NewlineStyle: class Document: def __init__(self, text: str) -> None: - self._newline_style = _detect_newline_style(text) - """The type of newline used in the text""" - self._eof_newline = text and text[-1] in VALID_NEWLINE_STYLES + self._newline = _detect_newline_style(text) + """The type of newline used in the text.""" self._lines: list[str] = text.splitlines(keepends=False) + """The lines of the document, excluding newline characters. + + If there's a newline at the end of the file, the final line is an empty string. + """ + if text.endswith(tuple(VALID_NEWLINES)): + self._lines.append("") @property def lines(self) -> list[str]: return self._lines - def load_text(self, text: str) -> None: - """Load text from a string into the document. - - Args: - text: The text to load into the document - """ - def insert_range( self, start: tuple[int, int], end: tuple[int, int], text: str ) -> tuple[int, int]: @@ -69,7 +67,7 @@ def insert_range( bottom_row, bottom_column = bottom insert_lines = text.splitlines() - if text.endswith("\n"): + if text.endswith(tuple(VALID_NEWLINES)): # Special case where a single newline character is inserted. insert_lines.append("") @@ -119,13 +117,13 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: deleted_text = start_line[top_column:] + "\n" for row in range(top_row + 1, bottom_row): - deleted_text += lines[row] + deleted_text += lines[row] + self._newline # Never add the newline at the end without checking its presence: if row != self.line_count - 1: deleted_text += "\n" deleted_text += end_line[:bottom_column] - if bottom == (self.line_count, 0) and self._eof_newline: + if bottom == (self.line_count, 0): deleted_text += "\n" # Update the lines at the start and end of the range diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 1c22118398..5f698d1db2 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -245,7 +245,6 @@ def render_line(self, widget_y: int) -> Strip: line = document.get_line_text(line_index) line.tab_size = self.indent_width - codepoint_count = len(line) line.set_length(self.virtual_size.width) selection = self.selection @@ -270,12 +269,12 @@ def render_line(self, widget_y: int) -> Strip: line.stylize_before( selection_style, start=selection_top_column, - end=codepoint_count, + end=line_character_count, ) elif line_index == selection_bottom_row: line.stylize_before(selection_style, end=selection_bottom_column) else: - line.stylize_before(selection_style, end=codepoint_count) + line.stylize_before(selection_style, end=line_character_count) # Highlight the cursor cursor_row, cursor_column = end diff --git a/tests/document/test_document_delete.py b/tests/document/test_document_delete.py index 4ca3a09c23..442ddcf7ea 100644 --- a/tests/document/test_document_delete.py +++ b/tests/document/test_document_delete.py @@ -107,3 +107,19 @@ def test_delete_single_line_including_newline(document): "Fear is the mind-killer.", "Sorry Will.", ] + + +TEXT_NEWLINE_EOF = """\ +I must not fear. +Fear is the mind-killer. +""" + + +def test_delete_end_of_file_newline(): + document = Document(TEXT_NEWLINE_EOF) + deleted_text = document.delete_range((2, 0), (1, 24)) + assert deleted_text == "\n" + assert document.lines == [ + "I must not fear.", + "Fear is the mind-killer.", + ] diff --git a/tests/document/test_document_insert.py b/tests/document/test_document_insert.py index 22dcfe3c32..6a9ce30d12 100644 --- a/tests/document/test_document_insert.py +++ b/tests/document/test_document_insert.py @@ -81,3 +81,18 @@ def test_insert_range_text_no_newlines(): "I MUST not fear.", "Fear is the mind-killer.", ] + + +TEXT_NEWLINE_EOF = """\ +I must not fear. +Fear is the mind-killer. +""" + + +def test_newline_eof(): + document = Document(TEXT_NEWLINE_EOF) + assert document._lines == [ + "I must not fear.", + "Fear is the mind-killer.", + "", + ] From 4be829fdbe918650e3319077e8b5752ef1abd58e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 27 Jul 2023 16:52:15 +0100 Subject: [PATCH 101/366] Restore accidentally deleted variable --- src/textual/widgets/_text_area.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 5f698d1db2..3a7421fc43 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -244,6 +244,7 @@ def render_line(self, widget_y: int) -> Strip: return Strip.blank(self.size.width) line = document.get_line_text(line_index) + line_character_count = len(line) line.tab_size = self.indent_width line.set_length(self.virtual_size.width) From 2dd99860e1a85db90a27fdd37fa58acd501d3f68 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 11:32:10 +0100 Subject: [PATCH 102/366] Fix deletion logic --- src/textual/document/_document.py | 23 ++++++++++------------- src/textual/widgets/_text_area.py | 29 +---------------------------- 2 files changed, 11 insertions(+), 41 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index de8b9cfa3f..2e157070a7 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -110,21 +110,18 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: else: # The deletion range spans multiple lines. start_line = lines[top_row] - if bottom_row == self.line_count: - end_line = "" - else: - end_line = lines[bottom_row] - deleted_text = start_line[top_column:] + "\n" + deleted_text = start_line[top_column:] for row in range(top_row + 1, bottom_row): - deleted_text += lines[row] + self._newline - # Never add the newline at the end without checking its presence: - if row != self.line_count - 1: - deleted_text += "\n" - - deleted_text += end_line[:bottom_column] - if bottom == (self.line_count, 0): - deleted_text += "\n" + deleted_text += self._newline + lines[row] + + # Now handle the bottom line of the selection + end_line = lines[bottom_row] if bottom_row <= self.line_count - 1 else "" + + # Only include the newline if the endline actually exists + if bottom_row < self.line_count: + deleted_text += self._newline + deleted_text += end_line[:bottom_column] # Update the lines at the start and end of the range lines[top_row] = start_line[:top_column] + end_line[bottom_column:] diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 3a7421fc43..41084ecf0a 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -758,7 +758,7 @@ def clear(self) -> None: document = self._document last_line = document[-1] document_end_location = (document.line_count, len(last_line)) - self.delete_range((0, 0), document_end_location) + self.delete_range((0, 0), document_end_location, cursor_destination=(0, 0)) def action_delete_left(self) -> None: """Deletes the character to the left of the cursor and updates the cursor location.""" @@ -874,30 +874,3 @@ def action_delete_word_right(self) -> None: to_location = (cursor_row, len(self._document[cursor_row])) self.delete_range(end, to_location) - - # --- Debugging - @dataclass - class EditorDebug: - cursor: tuple[int, int] - language: str - document_size: Size - virtual_size: Size - scroll: Offset - undo_stack: list[Edit] - tree_sexp: str - active_line_text: str - active_line_cell_len: int - - def _debug_state(self) -> "EditorDebug": - return self.EditorDebug( - cursor=self.selection, - language=self.language, - document_size=self._document_size, - virtual_size=self.virtual_size, - scroll=self.scroll_offset, - undo_stack=list(reversed(self._undo_stack)), - # tree_sexp=self._syntax_tree.root_node.sexp(), - tree_sexp="", - active_line_text=repr(self.cursor_line_text), - active_line_cell_len=cell_len(self.cursor_line_text), - ) From f38d4af5756e04c4a5a7113f9a10a9f2837dc73a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 12:28:00 +0100 Subject: [PATCH 103/366] Update dependencies, add docstrings --- poetry.lock | 258 +++++++++++++++--------------- src/textual/widgets/_text_area.py | 61 ++++--- 2 files changed, 174 insertions(+), 145 deletions(-) diff --git a/poetry.lock b/poetry.lock index e0ab43b37f..5d741ea5c1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "aiohttp" -version = "3.8.4" +version = "3.8.5" description = "Async http client/server framework (asyncio)" category = "dev" optional = false @@ -121,7 +121,7 @@ python-versions = "*" [[package]] name = "certifi" -version = "2023.5.7" +version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false @@ -396,16 +396,17 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "markdown" -version = "3.3.7" -description = "Python implementation of Markdown." +version = "3.4.4" +description = "Python implementation of John Gruber's Markdown." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} [package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] testing = ["coverage", "pyyaml"] [[package]] @@ -474,7 +475,7 @@ python-versions = ">=3.6" [[package]] name = "mkdocs" -version = "1.4.3" +version = "1.5.1" description = "Project documentation with Markdown." category = "dev" optional = false @@ -486,9 +487,12 @@ colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} jinja2 = ">=2.11.1" -markdown = ">=3.2.1,<3.4" +markdown = ">=3.2.1" +markupsafe = ">=2.0.1" mergedeep = ">=1.3.4" packaging = ">=20.5" +pathspec = ">=0.11.1" +platformdirs = ">=2.2.0" pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} @@ -496,7 +500,7 @@ watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" @@ -523,7 +527,7 @@ mkdocs = "*" [[package]] name = "mkdocs-material" -version = "9.1.19" +version = "9.1.21" description = "Documentation that simply works" category = "dev" optional = false @@ -533,7 +537,7 @@ python-versions = ">=3.7" colorama = ">=0.4" jinja2 = ">=3.0" markdown = ">=3.2" -mkdocs = ">=1.4.2" +mkdocs = ">=1.5.0" mkdocs-material-extensions = ">=1.1" pygments = ">=2.14" pymdown-extensions = ">=9.9.1" @@ -665,7 +669,7 @@ python-versions = ">=3.7" [[package]] name = "pathspec" -version = "0.11.1" +version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -673,18 +677,18 @@ python-versions = ">=3.7" [[package]] name = "platformdirs" -version = "3.9.1" +version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""} [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] name = "pluggy" @@ -903,7 +907,7 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "13.4.2" +version = "13.5.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false @@ -1061,7 +1065,7 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "urllib3" -version = "2.0.3" +version = "2.0.4" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false @@ -1075,21 +1079,21 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.24.0" +version = "20.24.2" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.12,<4" +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} -platformdirs = ">=3.5.1,<4" +platformdirs = ">=3.9.1,<4" [package.extras] docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] name = "watchdog" @@ -1134,93 +1138,93 @@ content-hash = "811fccd3413e15a412c4e4de3ff954851f797a6828dc90fd260dcdab5db88ada [metadata.files] aiohttp = [ - {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1"}, - {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a"}, - {file = "aiohttp-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea"}, - {file = "aiohttp-3.8.4-cp310-cp310-win32.whl", hash = "sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1"}, - {file = "aiohttp-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24"}, - {file = "aiohttp-3.8.4-cp311-cp311-win32.whl", hash = "sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d"}, - {file = "aiohttp-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc"}, - {file = "aiohttp-3.8.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff"}, - {file = "aiohttp-3.8.4-cp36-cp36m-win32.whl", hash = "sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777"}, - {file = "aiohttp-3.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e"}, - {file = "aiohttp-3.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241"}, - {file = "aiohttp-3.8.4-cp37-cp37m-win32.whl", hash = "sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a"}, - {file = "aiohttp-3.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d"}, - {file = "aiohttp-3.8.4-cp38-cp38-win32.whl", hash = "sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54"}, - {file = "aiohttp-3.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4"}, - {file = "aiohttp-3.8.4-cp39-cp39-win32.whl", hash = "sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a"}, - {file = "aiohttp-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04"}, - {file = "aiohttp-3.8.4.tar.gz", hash = "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"}, + {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8"}, + {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84"}, + {file = "aiohttp-3.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96943e5dcc37a6529d18766597c491798b7eb7a61d48878611298afc1fca946c"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ad5c3c4590bb3cc28b4382f031f3783f25ec223557124c68754a2231d989e2b"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c413c633d0512df4dc7fd2373ec06cc6a815b7b6d6c2f208ada7e9e93a5061d"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df72ac063b97837a80d80dec8d54c241af059cc9bb42c4de68bd5b61ceb37caa"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c48c5c0271149cfe467c0ff8eb941279fd6e3f65c9a388c984e0e6cf57538e14"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:368a42363c4d70ab52c2c6420a57f190ed3dfaca6a1b19afda8165ee16416a82"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7607ec3ce4993464368505888af5beb446845a014bc676d349efec0e05085905"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0d21c684808288a98914e5aaf2a7c6a3179d4df11d249799c32d1808e79503b5"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:312fcfbacc7880a8da0ae8b6abc6cc7d752e9caa0051a53d217a650b25e9a691"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad093e823df03bb3fd37e7dec9d4670c34f9e24aeace76808fc20a507cace825"}, + {file = "aiohttp-3.8.5-cp310-cp310-win32.whl", hash = "sha256:33279701c04351a2914e1100b62b2a7fdb9a25995c4a104259f9a5ead7ed4802"}, + {file = "aiohttp-3.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:6e4a280e4b975a2e7745573e3fc9c9ba0d1194a3738ce1cbaa80626cc9b4f4df"}, + {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae871a964e1987a943d83d6709d20ec6103ca1eaf52f7e0d36ee1b5bebb8b9b9"}, + {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:461908b2578955045efde733719d62f2b649c404189a09a632d245b445c9c975"}, + {file = "aiohttp-3.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72a860c215e26192379f57cae5ab12b168b75db8271f111019509a1196dfc780"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc14be025665dba6202b6a71cfcdb53210cc498e50068bc088076624471f8bb9"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af740fc2711ad85f1a5c034a435782fbd5b5f8314c9a3ef071424a8158d7f6b"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:841cd8233cbd2111a0ef0a522ce016357c5e3aff8a8ce92bcfa14cef890d698f"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed1c46fb119f1b59304b5ec89f834f07124cd23ae5b74288e364477641060ff"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84f8ae3e09a34f35c18fa57f015cc394bd1389bce02503fb30c394d04ee6b938"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62360cb771707cb70a6fd114b9871d20d7dd2163a0feafe43fd115cfe4fe845e"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:23fb25a9f0a1ca1f24c0a371523546366bb642397c94ab45ad3aedf2941cec6a"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0ba0d15164eae3d878260d4c4df859bbdc6466e9e6689c344a13334f988bb53"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5d20003b635fc6ae3f96d7260281dfaf1894fc3aa24d1888a9b2628e97c241e5"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0175d745d9e85c40dcc51c8f88c74bfbaef9e7afeeeb9d03c37977270303064c"}, + {file = "aiohttp-3.8.5-cp311-cp311-win32.whl", hash = "sha256:2e1b1e51b0774408f091d268648e3d57f7260c1682e7d3a63cb00d22d71bb945"}, + {file = "aiohttp-3.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:043d2299f6dfdc92f0ac5e995dfc56668e1587cea7f9aa9d8a78a1b6554e5755"}, + {file = "aiohttp-3.8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cae533195e8122584ec87531d6df000ad07737eaa3c81209e85c928854d2195c"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f21e83f355643c345177a5d1d8079f9f28b5133bcd154193b799d380331d5d3"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a75ef35f2df54ad55dbf4b73fe1da96f370e51b10c91f08b19603c64004acc"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e2e9839e14dd5308ee773c97115f1e0a1cb1d75cbeeee9f33824fa5144c7634"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44e65da1de4403d0576473e2344828ef9c4c6244d65cf4b75549bb46d40b8dd"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d847e4cde6ecc19125ccbc9bfac4a7ab37c234dd88fbb3c5c524e8e14da543"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c7a815258e5895d8900aec4454f38dca9aed71085f227537208057853f9d13f2"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8b929b9bd7cd7c3939f8bcfffa92fae7480bd1aa425279d51a89327d600c704d"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5db3a5b833764280ed7618393832e0853e40f3d3e9aa128ac0ba0f8278d08649"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a0215ce6041d501f3155dc219712bc41252d0ab76474615b9700d63d4d9292af"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824"}, + {file = "aiohttp-3.8.5-cp36-cp36m-win32.whl", hash = "sha256:6e6783bcc45f397fdebc118d772103d751b54cddf5b60fbcc958382d7dd64f3e"}, + {file = "aiohttp-3.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b5411d82cddd212644cf9360879eb5080f0d5f7d809d03262c50dad02f01421a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:01d4c0c874aa4ddfb8098e85d10b5e875a70adc63db91f1ae65a4b04d3344cda"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5980a746d547a6ba173fd5ee85ce9077e72d118758db05d229044b469d9029a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a482e6da906d5e6e653be079b29bc173a48e381600161c9932d89dfae5942ef"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80bd372b8d0715c66c974cf57fe363621a02f359f1ec81cba97366948c7fc873"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1161b345c0a444ebcf46bf0a740ba5dcf50612fd3d0528883fdc0eff578006a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd56db019015b6acfaaf92e1ac40eb8434847d9bf88b4be4efe5bfd260aee692"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:153c2549f6c004d2754cc60603d4668899c9895b8a89397444a9c4efa282aaf4"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4a01951fabc4ce26ab791da5f3f24dca6d9a6f24121746eb19756416ff2d881b"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bfb9162dcf01f615462b995a516ba03e769de0789de1cadc0f916265c257e5d8"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7dde0009408969a43b04c16cbbe252c4f5ef4574ac226bc8815cd7342d2028b6"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4149d34c32f9638f38f544b3977a4c24052042affa895352d3636fa8bffd030a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-win32.whl", hash = "sha256:68c5a82c8779bdfc6367c967a4a1b2aa52cd3595388bf5961a62158ee8a59e22"}, + {file = "aiohttp-3.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2cf57fb50be5f52bda004b8893e63b48530ed9f0d6c96c84620dc92fe3cd9b9d"}, + {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eca4bf3734c541dc4f374ad6010a68ff6c6748f00451707f39857f429ca36ced"}, + {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1274477e4c71ce8cfe6c1ec2f806d57c015ebf84d83373676036e256bc55d690"}, + {file = "aiohttp-3.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28c543e54710d6158fc6f439296c7865b29e0b616629767e685a7185fab4a6b9"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:910bec0c49637d213f5d9877105d26e0c4a4de2f8b1b29405ff37e9fc0ad52b8"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5443910d662db951b2e58eb70b0fbe6b6e2ae613477129a5805d0b66c54b6cb7"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e460be6978fc24e3df83193dc0cc4de46c9909ed92dd47d349a452ef49325b7"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1558def481d84f03b45888473fc5a1f35747b5f334ef4e7a571bc0dfcb11f8"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34dd0c107799dcbbf7d48b53be761a013c0adf5571bf50c4ecad5643fe9cfcd0"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa1990247f02a54185dc0dff92a6904521172a22664c863a03ff64c42f9b5410"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0e584a10f204a617d71d359fe383406305a4b595b333721fa50b867b4a0a1548"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a3cf433f127efa43fee6b90ea4c6edf6c4a17109d1d037d1a52abec84d8f2e42"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c11f5b099adafb18e65c2c997d57108b5bbeaa9eeee64a84302c0978b1ec948b"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:84de26ddf621d7ac4c975dbea4c945860e08cccde492269db4e1538a6a6f3c35"}, + {file = "aiohttp-3.8.5-cp38-cp38-win32.whl", hash = "sha256:ab88bafedc57dd0aab55fa728ea10c1911f7e4d8b43e1d838a1739f33712921c"}, + {file = "aiohttp-3.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:5798a9aad1879f626589f3df0f8b79b3608a92e9beab10e5fda02c8a2c60db2e"}, + {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a6ce61195c6a19c785df04e71a4537e29eaa2c50fe745b732aa937c0c77169f3"}, + {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:773dd01706d4db536335fcfae6ea2440a70ceb03dd3e7378f3e815b03c97ab51"}, + {file = "aiohttp-3.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f83a552443a526ea38d064588613aca983d0ee0038801bc93c0c916428310c28"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f7372f7341fcc16f57b2caded43e81ddd18df53320b6f9f042acad41f8e049a"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea353162f249c8097ea63c2169dd1aa55de1e8fecbe63412a9bc50816e87b761"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d47ae48db0b2dcf70bc8a3bc72b3de86e2a590fc299fdbbb15af320d2659de"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d827176898a2b0b09694fbd1088c7a31836d1a505c243811c87ae53a3f6273c1"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3562b06567c06439d8b447037bb655ef69786c590b1de86c7ab81efe1c9c15d8"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4e874cbf8caf8959d2adf572a78bba17cb0e9d7e51bb83d86a3697b686a0ab4d"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6809a00deaf3810e38c628e9a33271892f815b853605a936e2e9e5129762356c"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:33776e945d89b29251b33a7e7d006ce86447b2cfd66db5e5ded4e5cd0340585c"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eaeed7abfb5d64c539e2db173f63631455f1196c37d9d8d873fc316470dfbacd"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e91d635961bec2d8f19dfeb41a539eb94bd073f075ca6dae6c8dc0ee89ad6f91"}, + {file = "aiohttp-3.8.5-cp39-cp39-win32.whl", hash = "sha256:00ad4b6f185ec67f3e6562e8a1d2b69660be43070bd0ef6fcec5211154c7df67"}, + {file = "aiohttp-3.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c"}, + {file = "aiohttp-3.8.5.tar.gz", hash = "sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc"}, ] aiosignal = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, @@ -1274,8 +1278,8 @@ cached-property = [ {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, ] certifi = [ - {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, - {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, @@ -1572,8 +1576,8 @@ linkify-it-py = [ {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"}, ] markdown = [ - {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, - {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, + {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, + {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, ] markdown-it-py = [ {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, @@ -1644,8 +1648,8 @@ mergedeep = [ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] mkdocs = [ - {file = "mkdocs-1.4.3-py3-none-any.whl", hash = "sha256:6ee46d309bda331aac915cd24aab882c179a933bd9e77b80ce7d2eaaa3f689dd"}, - {file = "mkdocs-1.4.3.tar.gz", hash = "sha256:5955093bbd4dd2e9403c5afaf57324ad8b04f16886512a3ee6ef828956481c57"}, + {file = "mkdocs-1.5.1-py3-none-any.whl", hash = "sha256:67e889f8d8ba1fe5decdfc59f5f8f21d6a8925a129339e93dede303bdea03a98"}, + {file = "mkdocs-1.5.1.tar.gz", hash = "sha256:f2f323c62fffdf1b71b84849e39aef56d6852b3f0a5571552bca32cefc650209"}, ] mkdocs-autorefs = [ {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, @@ -1655,8 +1659,8 @@ mkdocs-exclude = [ {file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"}, ] mkdocs-material = [ - {file = "mkdocs_material-9.1.19-py3-none-any.whl", hash = "sha256:fb0a149294b319aedf36983919d8c40c9e566db21ead16258e20ebd2e6c0961c"}, - {file = "mkdocs_material-9.1.19.tar.gz", hash = "sha256:73b94b08c765e92a80645aac58d6a741fc5f587deec2b715489c714827b15a6f"}, + {file = "mkdocs_material-9.1.21-py3-none-any.whl", hash = "sha256:58bb2f11ef240632e176d6f0f7d1cff06be1d11c696a5a1b553b808b4280ed47"}, + {file = "mkdocs_material-9.1.21.tar.gz", hash = "sha256:71940cdfca84ab296b6362889c25395b1621273fb16c93deda257adb7ff44ec8"}, ] mkdocs-material-extensions = [ {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, @@ -1856,12 +1860,12 @@ packaging = [ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] pathspec = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] platformdirs = [ - {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, - {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, ] pluggy = [ {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, @@ -2052,8 +2056,8 @@ rfc3986 = [ {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] rich = [ - {file = "rich-13.4.2-py3-none-any.whl", hash = "sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec"}, - {file = "rich-13.4.2.tar.gz", hash = "sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898"}, + {file = "rich-13.5.1-py3-none-any.whl", hash = "sha256:b97381b204a206e1be618f5e1215a57174a1a7732490b3bf6668cf41d30bc72d"}, + {file = "rich-13.5.1.tar.gz", hash = "sha256:881653ee7037803559d8eae98f145e0a4c4b0ec3ff0300d2cc8d479c71fc6819"}, ] setuptools = [ {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, @@ -2207,12 +2211,12 @@ uc-micro-py = [ {file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"}, ] urllib3 = [ - {file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"}, - {file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"}, + {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, + {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, ] virtualenv = [ - {file = "virtualenv-20.24.0-py3-none-any.whl", hash = "sha256:18d1b37fc75cc2670625702d76849a91ebd383768b4e91382a8d51be3246049e"}, - {file = "virtualenv-20.24.0.tar.gz", hash = "sha256:e2a7cef9da880d693b933db7654367754f14e20650dc60e8ee7385571f8593a3"}, + {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, + {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, ] watchdog = [ {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 41084ecf0a..dbd279270c 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -20,15 +20,20 @@ @runtime_checkable class Edit(Protocol): - """Protocol for actions performed in the text editor that can be done and undone.""" + """Protocol for actions performed in the text editor which can be done and undone. - def do(self, editor: TextArea) -> object | None: + These are typically actions which affect the document (e.g. inserting and deleting + text), but they can really be anything. + + To perform an edit operation, pass the Edit to `TextArea.edit()`""" + + def do(self, text_area: TextArea) -> object | None: """Do the action.""" - def undo(self, editor: TextArea) -> object | None: + def undo(self, text_area: TextArea) -> object | None: """Undo the action.""" - def post_refresh(self, editor: TextArea) -> None: + def post_refresh(self, text_area: TextArea) -> None: """Code to execute after content size recalculated and repainted.""" @@ -37,28 +42,46 @@ class Insert: """Implements the Edit protocol for inserting text at some location.""" text: str + """The text to insert.""" from_location: tuple[int, int] + """The start location of the insert.""" to_location: tuple[int, int] + """The end location of the insert""" cursor_destination: tuple[int, int] | None = None + """The location to move the cursor to after the operation completes.""" _edit_end: tuple[int, int] | None = field(init=False, default=None) + """Computed location to move the cursor to if `cursor_destination` is None.""" - def do(self, editor: TextArea) -> None: - self._edit_end = editor._document.insert_range( + def do(self, text_area: TextArea) -> None: + """Perform the Insert operation. + + Args: + text_area: The TextArea to perform the insert on. + """ + self._edit_end = text_area._document.insert_range( self.from_location, self.to_location, self.text, ) - def undo(self, editor: TextArea) -> None: - """Undo the action.""" + def undo(self, text_area: TextArea) -> None: + """Undo the Insert operation. + + Args: + text_area: The TextArea to undo the insert operation on. + """ - def post_refresh(self, editor: TextArea) -> None: - # Update the cursor location + def post_refresh(self, text_area: TextArea) -> None: + """Update the cursor location after the widget has been refreshed. + + Args: + text_area: The TextArea this operation was performed on. + """ cursor_destination = self.cursor_destination if cursor_destination is not None: - editor.selection = cursor_destination + text_area.selection = cursor_destination else: - editor.selection = Selection.cursor(self._edit_end) + text_area.selection = Selection.cursor(self._edit_end) @dataclass @@ -77,22 +100,22 @@ class Delete: _deleted_text: str | None = field(init=False, default=None) """The text that was deleted, or None if the deletion hasn't occurred yet.""" - def do(self, editor: TextArea) -> str: + def do(self, text_area: TextArea) -> str: """Do the delete action and record the text that was deleted.""" - self._deleted_text = editor._document.delete_range( + self._deleted_text = text_area._document.delete_range( self.from_location, self.to_location ) return self._deleted_text - def undo(self, editor: TextArea) -> None: + def undo(self, text_area: TextArea) -> None: """Undo the delete action.""" - def post_refresh(self, editor: TextArea) -> None: + def post_refresh(self, text_area: TextArea) -> None: cursor_destination = self.cursor_destination if cursor_destination is not None: - editor.selection = Selection.cursor(cursor_destination) + text_area.selection = Selection.cursor(cursor_destination) else: - editor.selection = Selection.cursor(self.from_location) + text_area.selection = Selection.cursor(self.from_location) class TextArea(ScrollView, can_focus=True): @@ -523,6 +546,8 @@ def cursor_to_line_start(self, select: bool = False) -> None: else: self.selection = Selection.cursor((cursor_row, 0)) + self._record_last_intentional_cell_width() + # ------ Cursor movement actions def action_cursor_left(self) -> None: """Move the cursor one location to the left. From 5a10230fdaa56b413660c6d6aef1470985f1c983 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 13:03:01 +0100 Subject: [PATCH 104/366] More Document insert tests --- src/textual/widgets/_text_area.py | 4 ++-- tests/document/test_document_insert.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index dbd279270c..4d9d239f7a 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -435,10 +435,10 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: return column_index return len(line) - def watch_selection(self) -> None: + def _watch_selection(self) -> None: self.scroll_cursor_visible() - def validate_selection(self, selection: Selection) -> Selection: + def _validate_selection(self, selection: Selection) -> Selection: start, end = selection clamp_visitable = self.clamp_visitable return Selection(clamp_visitable(start), clamp_visitable(end)) diff --git a/tests/document/test_document_insert.py b/tests/document/test_document_insert.py index 6a9ce30d12..39b8d77529 100644 --- a/tests/document/test_document_insert.py +++ b/tests/document/test_document_insert.py @@ -50,6 +50,12 @@ def test_insert_newline_splits_line(): assert document._lines == ["I", " must not fear.", "Fear is the mind-killer."] +def test_insert_newline_splits_line_selection(): + document = Document(TEXT) + document.insert_range((0, 1), (0, 6), "\n") + assert document._lines == ["I", " not fear.", "Fear is the mind-killer."] + + def test_insert_multiple_lines_ends_with_newline(): document = Document(TEXT) document.insert_range((0, 1), (0, 1), "Hello,\nworld!\n") @@ -61,6 +67,16 @@ def test_insert_multiple_lines_ends_with_newline(): ] +def test_insert_multiple_lines_ends_with_no_newline(): + document = Document(TEXT) + document.insert_range((0, 1), (0, 1), "Hello,\nworld!") + assert document._lines == [ + "IHello,", + "world! must not fear.", + "Fear is the mind-killer.", + ] + + def test_insert_multiple_lines_starts_with_newline(): document = Document(TEXT) document.insert_range((0, 1), (0, 1), "\nHello,\nworld!\n") From 4610ea975b80d9d37babc7c42cfc2d3f36ffbf17 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 13:13:17 +0100 Subject: [PATCH 105/366] Handling delete right when theres a selection --- src/textual/widgets/_text_area.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 4d9d239f7a..96e776a04e 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -786,7 +786,9 @@ def clear(self) -> None: self.delete_range((0, 0), document_end_location, cursor_destination=(0, 0)) def action_delete_left(self) -> None: - """Deletes the character to the left of the cursor and updates the cursor location.""" + """Deletes the character to the left of the cursor and updates the cursor location. + + If there's a selection, then the selected range is deleted.""" selection = self.selection @@ -805,19 +807,23 @@ def action_delete_left(self) -> None: self.delete_range(start, end) def action_delete_right(self) -> None: - """Deletes the character to the right of the cursor and keeps the cursor at the same location.""" + """Deletes the character to the right of the cursor and keeps the cursor at the same location. + + If there's a selection, then the selected range is deleted.""" if self.cursor_at_end_of_document: return - start, end = self.selection + selection = self.selection + start, end = selection end_row, end_column = end - if self.cursor_at_end_of_row: - to_location = (end_row + 1, 0) - else: - to_location = (end_row, end_column + 1) + if selection.is_empty: + if self.cursor_at_end_of_row: + end = (end_row + 1, 0) + else: + end = (end_row, end_column + 1) - self.delete_range(start, to_location) + self.delete_range(start, end) def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" From f704e3190ec4e8667ab44687eb38b05b65a402ce Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 13:17:15 +0100 Subject: [PATCH 106/366] Recording cursor position as intentional after insert and delete --- src/textual/widgets/_text_area.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 96e776a04e..6fee934390 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -757,6 +757,7 @@ def insert_text( cursor_destination: tuple[int, int] | None = None, ) -> None: self.edit(Insert(text, location, location, cursor_destination)) + self._record_last_intentional_cell_width() def insert_text_range( self, @@ -766,6 +767,7 @@ def insert_text_range( cursor_destination: tuple[int, int] | None = None, ) -> None: self.edit(Insert(text, from_location, to_location, cursor_destination)) + self._record_last_intentional_cell_width() def delete_range( self, @@ -776,6 +778,7 @@ def delete_range( """Delete text between from_location and to_location.""" top, bottom = _fix_direction(from_location, to_location) deleted_text = self.edit(Delete(top, bottom, cursor_destination)) + self._record_last_intentional_cell_width() return deleted_text def clear(self) -> None: From 0a2159d0f8169fd4436c9ed8e896cd0da79a7d0b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 13:27:47 +0100 Subject: [PATCH 107/366] Add type alias for Location --- src/textual/document/_document.py | 32 ++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 2e157070a7..eabc882604 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -46,6 +46,10 @@ def __init__(self, text: str) -> None: def lines(self) -> list[str]: return self._lines + @property + def content(self) -> str: + return self._newline.join(self._lines) + def insert_range( self, start: tuple[int, int], end: tuple[int, int], text: str ) -> tuple[int, int]: @@ -146,18 +150,34 @@ def __getitem__(self, item: SupportsIndex | slice) -> str: return self._lines[item] +Location = tuple[int, int] +"""A location (row, column) within the document.""" + + class Selection(NamedTuple): """A range of characters within a document from a start point to the end point. The location of the cursor is always considered to be the `end` point of the selection. The selection is inclusive of the minimum point and exclusive of the maximum point. """ - start: tuple[int, int] = (0, 0) - end: tuple[int, int] = (0, 0) + start: Location = (0, 0) + """The start location of the selection. + + If you were to click and drag a selection inside a text-editor, this is where you *started* dragging. + """ + end: Location = (0, 0) + """The end location of the selection. + + If you were to click and drag a selection inside a text-editor, this is where you *finished* dragging. + """ @classmethod - def cursor(cls, location: tuple[int, int]) -> "Selection": - """Create a Selection with the same start and end point.""" + def cursor(cls, location: Location) -> "Selection": + """Create a Selection with the same start and end point - a "cursor". + + Args: + location: The location to create the zero-width Selection. + """ return cls(location, location) @property @@ -167,6 +187,8 @@ def is_empty(self) -> bool: return start == end @property - def range(self) -> tuple[tuple[int, int], tuple[int, int]]: + def range(self) -> tuple[Location, Location]: + """Return the Selection as a "standard" range, from top to bottom i.e. (minimum point, maximum point) + where the minimum point is inclusive and the maximum point is exclusive.""" start, end = self return _fix_direction(start, end) From 5546f184e792c19af6db6aa3693debda43ffbfc9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 13:29:26 +0100 Subject: [PATCH 108/366] use the Tuple type in a type alias --- src/textual/document/_document.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index eabc882604..559fe314e3 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import NamedTuple +from typing import NamedTuple, Tuple from rich.text import Text @@ -150,7 +150,7 @@ def __getitem__(self, item: SupportsIndex | slice) -> str: return self._lines[item] -Location = tuple[int, int] +Location = Tuple[int, int] """A location (row, column) within the document.""" From cb6c1ba6ed351f8e785d4ef1e2a728f19f651e9e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 13:31:02 +0100 Subject: [PATCH 109/366] Using Location type alias in TextArea --- src/textual/widgets/_text_area.py | 44 +++++++++++++++---------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 6fee934390..5c133a6467 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -11,7 +11,7 @@ from textual._fix_direction import _fix_direction from textual._types import Literal, Protocol, runtime_checkable from textual.binding import Binding -from textual.document._document import Document, Selection +from textual.document._document import Document, Location, Selection from textual.geometry import Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView @@ -43,13 +43,13 @@ class Insert: text: str """The text to insert.""" - from_location: tuple[int, int] + from_location: Location """The start location of the insert.""" - to_location: tuple[int, int] + to_location: Location """The end location of the insert""" - cursor_destination: tuple[int, int] | None = None + cursor_destination: Location | None = None """The location to move the cursor to after the operation completes.""" - _edit_end: tuple[int, int] | None = field(init=False, default=None) + _edit_end: Location | None = field(init=False, default=None) """Computed location to move the cursor to if `cursor_destination` is None.""" def do(self, text_area: TextArea) -> None: @@ -88,13 +88,13 @@ def post_refresh(self, text_area: TextArea) -> None: class Delete: """Performs a delete operation.""" - from_location: tuple[int, int] + from_location: Location """The location to delete from (inclusive).""" - to_location: tuple[int, int] + to_location: Location """The location to delete to (exclusive).""" - cursor_destination: tuple[int, int] | None = None + cursor_destination: Location | None = None """Where to move the cursor to after the deletion.""" _deleted_text: str | None = field(init=False, default=None) @@ -388,7 +388,7 @@ def _on_key(self, event: events.Key) -> None: start, end = self.selection self.insert_text_range(insert, start, end) - def get_target_document_location(self, offset: Offset) -> tuple[int, int]: + def get_target_document_location(self, offset: Offset) -> Location: target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0) target_row = clamp( offset.y + int(self.scroll_y), 0, self._document.line_count - 1 @@ -444,7 +444,7 @@ def _validate_selection(self, selection: Selection) -> Selection: return Selection(clamp_visitable(start), clamp_visitable(end)) # --- Cursor/selection utilities - def is_visitable(self, location: tuple[int, int]) -> bool: + def is_visitable(self, location: Location) -> bool: """Return True if the location is somewhere that can naturally be reached by the cursor. Generally this means it's at a row within the document, and a column which contains a character, @@ -462,7 +462,7 @@ def is_visitable_selection(self, selection: Selection) -> bool: start, end = selection return visitable(start) and visitable(end) - def clamp_visitable(self, location: tuple[int, int]) -> tuple[int, int]: + def clamp_visitable(self, location: Location) -> Location: document = self._document row, column = location @@ -569,7 +569,7 @@ def action_cursor_left_select(self): self.selection = Selection(selection_start, new_cursor_location) self._record_last_intentional_cell_width() - def get_cursor_left_location(self) -> tuple[int, int]: + def get_cursor_left_location(self) -> Location: """Get the location the cursor will move to if it moves left.""" if self.cursor_at_start_of_document: return 0, 0 @@ -598,7 +598,7 @@ def action_cursor_right_select(self): self.selection = Selection(selection_start, new_cursor_location) self._record_last_intentional_cell_width() - def get_cursor_right_location(self) -> tuple[int, int]: + def get_cursor_right_location(self) -> Location: """Get the location the cursor will move to if it moves right.""" if self.cursor_at_end_of_document: return self.selection.end @@ -643,7 +643,7 @@ def action_cursor_up_select(self) -> None: start, end = self.selection self.selection = Selection(start, target) - def get_cursor_up_location(self) -> tuple[int, int]: + def get_cursor_up_location(self) -> Location: """Get the location the cursor will move to if it moves up.""" if self.cursor_at_first_row: return 0, 0 @@ -753,8 +753,8 @@ def _record_last_intentional_cell_width(self) -> None: def insert_text( self, text: str, - location: tuple[int, int], - cursor_destination: tuple[int, int] | None = None, + location: Location, + cursor_destination: Location | None = None, ) -> None: self.edit(Insert(text, location, location, cursor_destination)) self._record_last_intentional_cell_width() @@ -762,18 +762,18 @@ def insert_text( def insert_text_range( self, text: str, - from_location: tuple[int, int], - to_location: tuple[int, int], - cursor_destination: tuple[int, int] | None = None, + from_location: Location, + to_location: Location, + cursor_destination: Location | None = None, ) -> None: self.edit(Insert(text, from_location, to_location, cursor_destination)) self._record_last_intentional_cell_width() def delete_range( self, - from_location: tuple[int, int], - to_location: tuple[int, int], - cursor_destination: tuple[int, int] | None = None, + from_location: Location, + to_location: Location, + cursor_destination: Location | None = None, ) -> str: """Delete text between from_location and to_location.""" top, bottom = _fix_direction(from_location, to_location) From 0a42af6998ee6604ad05201bd148fefafba6f868 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 13:33:15 +0100 Subject: [PATCH 110/366] Using Location alias in Document --- src/textual/document/_document.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 559fe314e3..9294a1c6c7 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -50,9 +50,7 @@ def lines(self) -> list[str]: def content(self) -> str: return self._newline.join(self._lines) - def insert_range( - self, start: tuple[int, int], end: tuple[int, int], text: str - ) -> tuple[int, int]: + def insert_range(self, start: Location, end: Location, text: str) -> Location: """Insert text at the given range. Args: @@ -90,7 +88,7 @@ def insert_range( end_point = destination_row, destination_column return end_point - def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: + def delete_range(self, start: Location, end: Location) -> str: """Delete the text at the given range. Args: From 006e9c4f07bd2e04872de5e7bfa116b4fe2e0c9f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 13:44:04 +0100 Subject: [PATCH 111/366] Simplifying logic, add get_selected_text --- src/textual/document/_document.py | 50 ++++++++++++++++++++----------- src/textual/widgets/_text_area.py | 1 - 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 9294a1c6c7..6eb24c3c9a 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -47,7 +47,7 @@ def lines(self) -> list[str]: return self._lines @property - def content(self) -> str: + def text(self) -> str: return self._newline.join(self._lines) def insert_range(self, start: Location, end: Location, text: str) -> Location: @@ -104,34 +104,48 @@ def delete_range(self, start: Location, end: Location) -> str: lines = self._lines + deleted_text = self.get_selection(top, bottom) + if top_row == bottom_row: - # The deletion range is within a single line. line = lines[top_row] - deleted_text = line[top_column:bottom_column] lines[top_row] = line[:top_column] + line[bottom_column:] else: - # The deletion range spans multiple lines. start_line = lines[top_row] + end_line = lines[bottom_row] if bottom_row <= self.line_count - 1 else "" + lines[top_row] = start_line[:top_column] + end_line[bottom_column:] + del lines[top_row + 1 : bottom_row + 1] - deleted_text = start_line[top_column:] - for row in range(top_row + 1, bottom_row): - deleted_text += self._newline + lines[row] + return deleted_text - # Now handle the bottom line of the selection - end_line = lines[bottom_row] if bottom_row <= self.line_count - 1 else "" + def get_selection(self, start: Location, end: Location) -> str: + """Get the text that falls between the start and end locations. - # Only include the newline if the endline actually exists - if bottom_row < self.line_count: - deleted_text += self._newline - deleted_text += end_line[:bottom_column] + Args: + start: The start location of the selection. + end: The end location of the selection. - # Update the lines at the start and end of the range - lines[top_row] = start_line[:top_column] + end_line[bottom_column:] + Returns: + The text between start (inclusive) and end (exclusive). + """ + top, bottom = _fix_direction(start, end) + top_row, top_column = top + bottom_row, bottom_column = bottom + lines = self._lines + if top_row == bottom_row: + line = lines[top_row] + selected_text = line[top_column:bottom_column] + else: + start_line = lines[top_row] + end_line = lines[bottom_row] if bottom_row <= self.line_count - 1 else "" + selected_text = start_line[top_column:] + for row in range(top_row + 1, bottom_row): + selected_text += self._newline + lines[row] - # Delete the lines in between - del lines[top_row + 1 : bottom_row + 1] + if bottom_row < self.line_count: + selected_text += self._newline + selected_text += end_line[:bottom_column] - return deleted_text + return selected_text @property def line_count(self) -> int: diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 5c133a6467..13bf32f07f 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -734,7 +734,6 @@ def action_cursor_page_down(self) -> None: @property def cursor_line_text(self) -> str: - # TODO - consider empty documents return self._document[self.selection.end[0]] def get_column_cell_width(self, row: int, column: int) -> int: From db5f144202e457ecb713f1b6b2ba8f75bea594e7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 14:13:22 +0100 Subject: [PATCH 112/366] More testing and docstrings --- src/textual/document/_document.py | 16 ++++++++-- tests/document/test_document.py | 52 +++++++++++++++++++++++++++++++ tests/document/test_selection.py | 0 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/document/test_document.py delete mode 100644 tests/document/test_selection.py diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 6eb24c3c9a..78e5302b4c 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -44,12 +44,24 @@ def __init__(self, text: str) -> None: @property def lines(self) -> list[str]: + """Get the document as a list of strings, where each string represents a line. + + Newline characters are not included in at the end of the strings. + + The newline character used in this document can be found via the `Document.newline` property. + """ return self._lines @property def text(self) -> str: + """Get the text from the document.""" return self._newline.join(self._lines) + @property + def newline(self) -> Newline: + """Get the Newline used in this document (e.g. '\r\n', '\n'. etc.)""" + return self._newline + def insert_range(self, start: Location, end: Location, text: str) -> Location: """Insert text at the given range. @@ -104,7 +116,7 @@ def delete_range(self, start: Location, end: Location) -> str: lines = self._lines - deleted_text = self.get_selection(top, bottom) + deleted_text = self.get_selected_text(top, bottom) if top_row == bottom_row: line = lines[top_row] @@ -117,7 +129,7 @@ def delete_range(self, start: Location, end: Location) -> str: return deleted_text - def get_selection(self, start: Location, end: Location) -> str: + def get_selected_text(self, start: Location, end: Location) -> str: """Get the text that falls between the start and end locations. Args: diff --git a/tests/document/test_document.py b/tests/document/test_document.py new file mode 100644 index 0000000000..598622965c --- /dev/null +++ b/tests/document/test_document.py @@ -0,0 +1,52 @@ +import pytest + +from textual.document._document import Document + +TEXT = """I must not fear. +Fear is the mind-killer.""" + +TEXT_NEWLINE = TEXT + "\n" +TEXT_WINDOWS = TEXT.replace("\n", "\r\n") +TEXT_WINDOWS_NEWLINE = TEXT_NEWLINE.replace("\n", "\r\n") + + +@pytest.mark.parametrize( + "text", [TEXT, TEXT_NEWLINE, TEXT_WINDOWS, TEXT_WINDOWS_NEWLINE] +) +def test_text(text): + """The text we put in is the text we get out.""" + document = Document(text) + assert document.text == text + + +def test_lines_newline_eof(): + document = Document(TEXT_NEWLINE) + assert document.lines == ["I must not fear.", "Fear is the mind-killer.", ""] + + +def test_lines_no_newline_eof(): + document = Document(TEXT) + assert document.lines == [ + "I must not fear.", + "Fear is the mind-killer.", + ] + + +def test_lines_windows(): + document = Document(TEXT_WINDOWS) + assert document.lines == ["I must not fear.", "Fear is the mind-killer."] + + +def test_lines_windows_newline(): + document = Document(TEXT_WINDOWS_NEWLINE) + assert document.lines == ["I must not fear.", "Fear is the mind-killer.", ""] + + +def test_newline_unix(): + document = Document(TEXT) + assert document.newline == "\n" + + +def test_newline_windows(): + document = Document(TEXT_WINDOWS) + assert document.newline == "\r\n" diff --git a/tests/document/test_selection.py b/tests/document/test_selection.py deleted file mode 100644 index e69de29bb2..0000000000 From 93b5e71a7b34a8b62b722efa4d9989fd388bc347 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 14:24:07 +0100 Subject: [PATCH 113/366] Tests for selections in the Document --- tests/document/test_document.py | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/document/test_document.py b/tests/document/test_document.py index 598622965c..69a87e10bf 100644 --- a/tests/document/test_document.py +++ b/tests/document/test_document.py @@ -50,3 +50,51 @@ def test_newline_unix(): def test_newline_windows(): document = Document(TEXT_WINDOWS) assert document.newline == "\r\n" + + +def test_get_selected_text_no_selection(): + document = Document(TEXT) + selection = document.get_selected_text((0, 0), (0, 0)) + assert selection == "" + + +def test_get_selected_text_single_line(): + document = Document(TEXT_WINDOWS) + selection = document.get_selected_text((0, 2), (0, 6)) + assert selection == "must" + + +def test_get_selected_text_multiple_lines_unix(): + document = Document(TEXT) + selection = document.get_selected_text((0, 2), (1, 2)) + assert selection == "must not fear.\nFe" + + +def test_get_selected_text_multiple_lines_windows(): + document = Document(TEXT_WINDOWS) + selection = document.get_selected_text((0, 2), (1, 2)) + assert selection == "must not fear.\r\nFe" + + +def test_get_selected_text_including_final_newline_unix(): + document = Document(TEXT_NEWLINE) + selection = document.get_selected_text((0, 0), (2, 0)) + assert selection == TEXT_NEWLINE + + +def test_get_selected_text_including_final_newline_windows(): + document = Document(TEXT_WINDOWS_NEWLINE) + selection = document.get_selected_text((0, 0), (2, 0)) + assert selection == TEXT_WINDOWS_NEWLINE + + +def test_get_selected_text_no_newline_at_end_of_file(): + document = Document(TEXT) + selection = document.get_selected_text((0, 0), (2, 0)) + assert selection == TEXT + + +def test_get_selected_text_no_newline_at_end_of_file_windows(): + document = Document(TEXT_WINDOWS) + selection = document.get_selected_text((0, 0), (2, 0)) + assert selection == TEXT_WINDOWS From a1afc36216ac0e70dcf3c53fe1cccf5bd2ef51f8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 14:26:08 +0100 Subject: [PATCH 114/366] Add `selected_text` property to TextArea --- src/textual/widgets/_text_area.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 13bf32f07f..7800f7c6e7 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -344,6 +344,12 @@ def render_line(self, widget_y: int) -> Strip: strip = Strip.join([gutter_strip, text_strip]).simplify() return strip + @property + def selected_text(self) -> str: + start, end = self.selection + start, end = _fix_direction(start, end) + return self._document.get_selected_text(start, end) + @property def gutter_width(self) -> int: # The longest number in the gutter plus two extra characters: `│ `. From dfe5622794ab144b21093e956de7122e19b4ebe6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 15:22:17 +0100 Subject: [PATCH 115/366] Account for padding when computing offset --- src/textual/widgets/_text_area.py | 49 ++++++++++++------------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 7800f7c6e7..b53df4a265 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -6,7 +6,7 @@ from rich.text import Text -from textual import events +from textual import events, log from textual._cells import cell_len from textual._fix_direction import _fix_direction from textual._types import Literal, Protocol, runtime_checkable @@ -199,13 +199,6 @@ class TextArea(ScrollView, can_focus=True): indent_width: Reactive[int] = reactive(4) """The width of tabs or the number of spaces to insert on pressing the `tab` key.""" - _document_size: Reactive[Size] = reactive(Size(), init=False, always_update=True) - """Tracks the size of the document. - - This is the width and height of the bounding box of the text. - Used to update virtual size. - """ - def __init__( self, name: str | None = None, @@ -214,34 +207,26 @@ def __init__( disabled: bool = False, ) -> None: super().__init__(name=name, id=id, classes=classes, disabled=disabled) - - # --- Core editor data self._document = Document("") """The document this widget is currently editing.""" self.indent_type: Literal["tabs", "spaces"] = "spaces" """Whether to indent using tabs or spaces.""" + self.word_pattern = re.compile(r"(?<=\W)(?=\w)|(?<=\w)(?=\W)") + """Compiled regular expression for what we consider to be a 'word'.""" + self._last_intentional_cell_width: int = 0 """Tracks the last column (measured in terms of cell length, since we care here about where the cursor visually moves more than the logical characters) the user explicitly navigated to so that we can reset to it whenever possible.""" - self._word_pattern = re.compile(r"(?<=\W)(?=\w)|(?<=\w)(?=\W)") - """Compiled regular expression for what we consider to be a 'word'.""" - self._undo_stack: list[Edit] = [] """A stack (the end of the list is the top of the stack) for tracking edits.""" self._selecting = False """True if we're currently selecting text, otherwise False.""" - def watch__document_size(self, size: Size) -> None: - document_width, document_height = size - self.virtual_size = Size( - document_width + self.gutter_width + 1, document_height - ) - def load_text(self, text: str) -> None: """Load text from a string into the editor. @@ -256,7 +241,7 @@ def _refresh_size(self) -> None: lines = self._document.lines text_width = max(cell_len(line.expandtabs(self.indent_width)) for line in lines) height = len(lines) - self._document_size = Size(text_width, height) + self.virtual_size = Size(text_width + self.gutter_width + 1, height) def render_line(self, widget_y: int) -> Strip: document = self._document @@ -354,12 +339,12 @@ def selected_text(self) -> str: def gutter_width(self) -> int: # The longest number in the gutter plus two extra characters: `│ `. gutter_margin = 2 - gutter_longest_number = ( + gutter_width = ( len(str(self._document.line_count + 1)) + gutter_margin if self.show_line_numbers else 0 ) - return gutter_longest_number + return gutter_width def edit(self, edit: Edit) -> object | None: result = edit.do(self) @@ -395,12 +380,16 @@ def _on_key(self, event: events.Key) -> None: self.insert_text_range(insert, start, end) def get_target_document_location(self, offset: Offset) -> Location: - target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0) + target_x = max( + offset.x - self.gutter_width + int(self.scroll_x) - self.gutter.top - 1, 0 + ) target_row = clamp( - offset.y + int(self.scroll_y), 0, self._document.line_count - 1 + offset.y + int(self.scroll_y) - self.gutter.top, + 0, + self._document.line_count - 1, ) target_column = self.cell_width_to_column_index(target_x, target_row) - + print(target_column) return target_row, target_column def _on_mouse_down(self, event: events.MouseDown) -> None: @@ -487,7 +476,7 @@ def scroll_cursor_visible(self): text = self.cursor_line_text[:column] column_offset = cell_len(text.expandtabs(self.indent_width)) self.scroll_to_region( - Region(x=column_offset, y=row, width=3, height=1), + Region(x=column_offset, y=row, width=1, height=1), spacing=Spacing(right=self.gutter_width), animate=False, force=True, @@ -680,7 +669,7 @@ def action_cursor_left_word(self) -> None: # Check the current line for a word boundary line = self._document[cursor_row][:cursor_column] - matches = list(re.finditer(self._word_pattern, line)) + matches = list(re.finditer(self.word_pattern, line)) if matches: # If a word boundary is found, move the cursor there @@ -706,7 +695,7 @@ def action_cursor_right_word(self) -> None: # Check the current line for a word boundary line = self._document[cursor_row][cursor_column:] - matches = list(re.finditer(self._word_pattern, line)) + matches = list(re.finditer(self.word_pattern, line)) if matches: # If a word boundary is found, move the cursor there @@ -873,7 +862,7 @@ def action_delete_word_left(self) -> None: # Check the current line for a word boundary line = self._document[cursor_row][:cursor_column] - matches = list(re.finditer(self._word_pattern, line)) + matches = list(re.finditer(self.word_pattern, line)) if matches: # If a word boundary is found, delete the word @@ -900,7 +889,7 @@ def action_delete_word_right(self) -> None: # Check the current line for a word boundary line = self._document[cursor_row][cursor_column:] - matches = list(re.finditer(self._word_pattern, line)) + matches = list(re.finditer(self.word_pattern, line)) if matches: # If a word boundary is found, delete the word From 5e542e87f417b083e6714f8373665f9434dffefe Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 15:23:13 +0100 Subject: [PATCH 116/366] Fix an off-by-one --- src/textual/widgets/_text_area.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index b53df4a265..1448a1f1f3 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -381,7 +381,7 @@ def _on_key(self, event: events.Key) -> None: def get_target_document_location(self, offset: Offset) -> Location: target_x = max( - offset.x - self.gutter_width + int(self.scroll_x) - self.gutter.top - 1, 0 + offset.x - self.gutter_width + int(self.scroll_x) - self.gutter.top, 0 ) target_row = clamp( offset.y + int(self.scroll_y) - self.gutter.top, From 80c83e60ae1ea329013d8293b7c170b9d2bcda4b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 15:35:38 +0100 Subject: [PATCH 117/366] Accounting for padding --- src/textual/widgets/_text_area.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 1448a1f1f3..4fc23eb652 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -381,7 +381,7 @@ def _on_key(self, event: events.Key) -> None: def get_target_document_location(self, offset: Offset) -> Location: target_x = max( - offset.x - self.gutter_width + int(self.scroll_x) - self.gutter.top, 0 + offset.x - self.gutter_width + int(self.scroll_x) - self.gutter.left, 0 ) target_row = clamp( offset.y + int(self.scroll_y) - self.gutter.top, @@ -393,25 +393,21 @@ def get_target_document_location(self, offset: Offset) -> Location: return target_row, target_column def _on_mouse_down(self, event: events.MouseDown) -> None: - offset = event.get_content_offset_capture(self) - if offset is not None: - target = self.get_target_document_location(event) - self.selection = Selection.cursor(target) - self._selecting = True - self.capture_mouse(True) + target = self.get_target_document_location(event) + self.selection = Selection.cursor(target) + self._selecting = True + self.capture_mouse(True) def _on_mouse_move(self, event: events.MouseMove) -> None: if self._selecting: - offset = event.get_content_offset_capture(self) - if offset is not None: - target = self.get_target_document_location(event) - selection_start, _ = self.selection - self.selection = Selection(selection_start, target) + target = self.get_target_document_location(event) + selection_start, _ = self.selection + self.selection = Selection(selection_start, target) def _on_mouse_up(self, event: events.MouseUp) -> None: - self._record_last_intentional_cell_width() self._selecting = False self.capture_mouse(False) + self._record_last_intentional_cell_width() def _on_paste(self, event: events.Paste) -> None: text = event.text From c570ee53338390454c048be49a40f095a33e48c8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 31 Jul 2023 16:56:36 +0100 Subject: [PATCH 118/366] Using scroll_offset.x instead of int(scroll_x) --- src/textual/widgets/_text_area.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 4fc23eb652..7a80c0661c 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -236,12 +236,17 @@ def load_text(self, text: str) -> None: self._document = Document(text) self._refresh_size() + def watch_scroll_x(self, old_value: float, new_value: float) -> None: + super().watch_scroll_x(old_value, new_value) + print(new_value) + def _refresh_size(self) -> None: # Calculate document lines = self._document.lines text_width = max(cell_len(line.expandtabs(self.indent_width)) for line in lines) height = len(lines) self.virtual_size = Size(text_width + self.gutter_width + 1, height) + print(f"new virtual_size = {self.virtual_size}") def render_line(self, widget_y: int) -> Strip: document = self._document @@ -319,7 +324,7 @@ def render_line(self, widget_y: int) -> Strip: # Crop the line to show only the visible part (some may be scrolled out of view) virtual_width, virtual_height = self.virtual_size - text_crop_start = int(self.scroll_x) + text_crop_start = self.scroll_offset.x text_crop_end = text_crop_start + virtual_width gutter_strip = Strip(gutter_segments) @@ -380,16 +385,21 @@ def _on_key(self, event: events.Key) -> None: self.insert_text_range(insert, start, end) def get_target_document_location(self, offset: Offset) -> Location: - target_x = max( - offset.x - self.gutter_width + int(self.scroll_x) - self.gutter.left, 0 - ) + print(f"offset.x = {offset.x}") + target_x = offset.x - self.gutter_width + int(self.scroll_x) - self.gutter.left + + # TODO: target_x looks wrong here! + print(f"prior target x = {target_x}") + target_x = max(target_x, 0) target_row = clamp( offset.y + int(self.scroll_y) - self.gutter.top, 0, self._document.line_count - 1, ) target_column = self.cell_width_to_column_index(target_x, target_row) - print(target_column) + print(f"scroll_x = {self.scroll_x}") + print(f"target_x = {target_x!r}") + print(f"target_column = {target_column!r}") return target_row, target_column def _on_mouse_down(self, event: events.MouseDown) -> None: @@ -471,12 +481,12 @@ def scroll_cursor_visible(self): row, column = self.selection.end text = self.cursor_line_text[:column] column_offset = cell_len(text.expandtabs(self.indent_width)) - self.scroll_to_region( + scrolled_amount = self.scroll_to_region( Region(x=column_offset, y=row, width=1, height=1), spacing=Spacing(right=self.gutter_width), animate=False, - force=True, ) + print(scrolled_amount) @property def cursor_at_first_row(self) -> bool: From 5fbdb149519d426463266d2deb50613928888f67 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 10:10:18 +0100 Subject: [PATCH 119/366] Using scroll_offset instead of scroll_x and scroll_y floats --- src/textual/widgets/_text_area.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 7a80c0661c..9d47fc07bd 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -236,17 +236,12 @@ def load_text(self, text: str) -> None: self._document = Document(text) self._refresh_size() - def watch_scroll_x(self, old_value: float, new_value: float) -> None: - super().watch_scroll_x(old_value, new_value) - print(new_value) - def _refresh_size(self) -> None: # Calculate document lines = self._document.lines text_width = max(cell_len(line.expandtabs(self.indent_width)) for line in lines) height = len(lines) self.virtual_size = Size(text_width + self.gutter_width + 1, height) - print(f"new virtual_size = {self.virtual_size}") def render_line(self, widget_y: int) -> Strip: document = self._document @@ -385,21 +380,15 @@ def _on_key(self, event: events.Key) -> None: self.insert_text_range(insert, start, end) def get_target_document_location(self, offset: Offset) -> Location: - print(f"offset.x = {offset.x}") - target_x = offset.x - self.gutter_width + int(self.scroll_x) - self.gutter.left - - # TODO: target_x looks wrong here! - print(f"prior target x = {target_x}") + scroll_x, scroll_y = self.scroll_offset + target_x = offset.x - self.gutter_width + scroll_x - self.gutter.left target_x = max(target_x, 0) target_row = clamp( - offset.y + int(self.scroll_y) - self.gutter.top, + offset.y + scroll_y - self.gutter.top, 0, self._document.line_count - 1, ) target_column = self.cell_width_to_column_index(target_x, target_row) - print(f"scroll_x = {self.scroll_x}") - print(f"target_x = {target_x!r}") - print(f"target_column = {target_column!r}") return target_row, target_column def _on_mouse_down(self, event: events.MouseDown) -> None: @@ -486,7 +475,6 @@ def scroll_cursor_visible(self): spacing=Spacing(right=self.gutter_width), animate=False, ) - print(scrolled_amount) @property def cursor_at_first_row(self) -> bool: From e0217c38c4736486410314029b586bb74db296ab Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 10:42:15 +0100 Subject: [PATCH 120/366] Pinning 0.2.0 for snapshot reports while issue is resolved --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5d741ea5c1..2b19fac77f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -814,7 +814,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-textual-snapshot" -version = "0.3.0" +version = "0.2.0" description = "Snapshot testing for Textual apps" category = "dev" optional = false @@ -1134,7 +1134,7 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "811fccd3413e15a412c4e4de3ff954851f797a6828dc90fd260dcdab5db88ada" +content-hash = "bf1b5c8ce33bd98176613966dc6c84eca62502b9d87af80165d6be9e6780cf6f" [metadata.files] aiohttp = [ @@ -1900,8 +1900,8 @@ pytest-cov = [ {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] pytest-textual-snapshot = [ - {file = "pytest_textual_snapshot-0.3.0-py3-none-any.whl", hash = "sha256:21f7775284f5b37d78b07f38d1718b57f94b788b613353a0754bee5ce250d552"}, - {file = "pytest_textual_snapshot-0.3.0.tar.gz", hash = "sha256:38c4ebc12d6122353069dde9ff0b55ae480c0dfc2dbadf9c4ab9bc577af2453b"}, + {file = "pytest_textual_snapshot-0.2.0-py3-none-any.whl", hash = "sha256:663fe07bf62181ec0c63139daaeaf50eb8088164037eb30d721f028adc9edc8c"}, + {file = "pytest_textual_snapshot-0.2.0.tar.gz", hash = "sha256:5e9f8c4b1b011bdae67d4f1129530afd6611f3f8bcf03cf06699402179bc12cf"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, diff --git a/pyproject.toml b/pyproject.toml index f83ef8a766..0957116744 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ httpx = "^0.23.1" types-setuptools = "^67.2.0.1" textual-dev = ">=1.0.0" pytest-asyncio = "*" -pytest-textual-snapshot = ">=0.1.0" +pytest-textual-snapshot = "0.2.0" # TODO: pinned while while we resolve issue [tool.black] includes = "src" From 61b92f0a60b82b004d00999adfa2b15f30c17062 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 10:47:58 +0100 Subject: [PATCH 121/366] Update snapshot tests --- .../__snapshots__/test_snapshots.ambr | 2732 ++++++++--------- 1 file changed, 1366 insertions(+), 1366 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 3dc7e52277..b7c3ee527d 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -21,138 +21,138 @@ font-weight: 700; } - .terminal-644510384-matrix { + .terminal-2137082507-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-644510384-title { + .terminal-2137082507-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-644510384-r1 { fill: #c5c8c6 } - .terminal-644510384-r2 { fill: #7ae998 } - .terminal-644510384-r3 { fill: #4ebf71;font-weight: bold } - .terminal-644510384-r4 { fill: #008139 } - .terminal-644510384-r5 { fill: #e3dbce } - .terminal-644510384-r6 { fill: #e1e1e1 } - .terminal-644510384-r7 { fill: #e76580 } - .terminal-644510384-r8 { fill: #f5e5e9;font-weight: bold } - .terminal-644510384-r9 { fill: #780028 } + .terminal-2137082507-r1 { fill: #c5c8c6 } + .terminal-2137082507-r2 { fill: #7ae998 } + .terminal-2137082507-r3 { fill: #4ebf71;font-weight: bold } + .terminal-2137082507-r4 { fill: #008139 } + .terminal-2137082507-r5 { fill: #e3dbce } + .terminal-2137082507-r6 { fill: #e1e1e1 } + .terminal-2137082507-r7 { fill: #e76580 } + .terminal-2137082507-r8 { fill: #f5e5e9;font-weight: bold } + .terminal-2137082507-r9 { fill: #780028 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AlignContainersApp + AlignContainersApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - center - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - middle - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  center  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  middle  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + @@ -1051,162 +1051,162 @@ font-weight: 700; } - .terminal-3842397750-matrix { + .terminal-3315449210-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3842397750-title { + .terminal-3315449210-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3842397750-r1 { fill: #e1e1e1 } - .terminal-3842397750-r2 { fill: #c5c8c6 } - .terminal-3842397750-r3 { fill: #e1e1e1;font-weight: bold } - .terminal-3842397750-r4 { fill: #454a50 } - .terminal-3842397750-r5 { fill: #303336 } - .terminal-3842397750-r6 { fill: #24292f;font-weight: bold } - .terminal-3842397750-r7 { fill: #a7a7a7;font-weight: bold } - .terminal-3842397750-r8 { fill: #000000 } - .terminal-3842397750-r9 { fill: #0f0f0f } - .terminal-3842397750-r10 { fill: #507bb3 } - .terminal-3842397750-r11 { fill: #364b66 } - .terminal-3842397750-r12 { fill: #dde6ed;font-weight: bold } - .terminal-3842397750-r13 { fill: #a5a9ac;font-weight: bold } - .terminal-3842397750-r14 { fill: #001541 } - .terminal-3842397750-r15 { fill: #0f192e } - .terminal-3842397750-r16 { fill: #7ae998 } - .terminal-3842397750-r17 { fill: #4a8159 } - .terminal-3842397750-r18 { fill: #0a180e;font-weight: bold } - .terminal-3842397750-r19 { fill: #0e1510;font-weight: bold } - .terminal-3842397750-r20 { fill: #008139 } - .terminal-3842397750-r21 { fill: #0f4e2a } - .terminal-3842397750-r22 { fill: #ffcf56 } - .terminal-3842397750-r23 { fill: #8b7439 } - .terminal-3842397750-r24 { fill: #211505;font-weight: bold } - .terminal-3842397750-r25 { fill: #19140c;font-weight: bold } - .terminal-3842397750-r26 { fill: #b86b00 } - .terminal-3842397750-r27 { fill: #68430f } - .terminal-3842397750-r28 { fill: #e76580 } - .terminal-3842397750-r29 { fill: #80404d } - .terminal-3842397750-r30 { fill: #f5e5e9;font-weight: bold } - .terminal-3842397750-r31 { fill: #b0a8aa;font-weight: bold } - .terminal-3842397750-r32 { fill: #780028 } - .terminal-3842397750-r33 { fill: #4a0f22 } + .terminal-3315449210-r1 { fill: #e1e1e1 } + .terminal-3315449210-r2 { fill: #c5c8c6 } + .terminal-3315449210-r3 { fill: #e1e1e1;font-weight: bold } + .terminal-3315449210-r4 { fill: #454a50 } + .terminal-3315449210-r5 { fill: #303336 } + .terminal-3315449210-r6 { fill: #24292f;font-weight: bold } + .terminal-3315449210-r7 { fill: #a7a7a7;font-weight: bold } + .terminal-3315449210-r8 { fill: #000000 } + .terminal-3315449210-r9 { fill: #0f0f0f } + .terminal-3315449210-r10 { fill: #507bb3 } + .terminal-3315449210-r11 { fill: #364b66 } + .terminal-3315449210-r12 { fill: #dde6ed;font-weight: bold } + .terminal-3315449210-r13 { fill: #a5a9ac;font-weight: bold } + .terminal-3315449210-r14 { fill: #001541 } + .terminal-3315449210-r15 { fill: #0f192e } + .terminal-3315449210-r16 { fill: #7ae998 } + .terminal-3315449210-r17 { fill: #4a8159 } + .terminal-3315449210-r18 { fill: #0a180e;font-weight: bold } + .terminal-3315449210-r19 { fill: #0e1510;font-weight: bold } + .terminal-3315449210-r20 { fill: #008139 } + .terminal-3315449210-r21 { fill: #0f4e2a } + .terminal-3315449210-r22 { fill: #ffcf56 } + .terminal-3315449210-r23 { fill: #8b7439 } + .terminal-3315449210-r24 { fill: #211505;font-weight: bold } + .terminal-3315449210-r25 { fill: #19140c;font-weight: bold } + .terminal-3315449210-r26 { fill: #b86b00 } + .terminal-3315449210-r27 { fill: #68430f } + .terminal-3315449210-r28 { fill: #e76580 } + .terminal-3315449210-r29 { fill: #80404d } + .terminal-3315449210-r30 { fill: #f5e5e9;font-weight: bold } + .terminal-3315449210-r31 { fill: #b0a8aa;font-weight: bold } + .terminal-3315449210-r32 { fill: #780028 } + .terminal-3315449210-r33 { fill: #4a0f22 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ButtonsApp + ButtonsApp - - - - - Standard ButtonsDisabled Buttons - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - DefaultDefault - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Primary!Primary! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Success!Success! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Warning!Warning! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Error!Error! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - + + + + + Standard ButtonsDisabled Buttons + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Default  Default  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Primary!  Primary!  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Success!  Success!  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Warning!  Warning!  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Error!  Error!  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + @@ -1237,143 +1237,143 @@ font-weight: 700; } - .terminal-1541091233-matrix { + .terminal-3477807222-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1541091233-title { + .terminal-3477807222-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1541091233-r1 { fill: #e1e1e1 } - .terminal-1541091233-r2 { fill: #c5c8c6 } - .terminal-1541091233-r3 { fill: #262626 } - .terminal-1541091233-r4 { fill: #4a4a4a } - .terminal-1541091233-r5 { fill: #2e2e2e;font-weight: bold } - .terminal-1541091233-r6 { fill: #e3e3e3 } - .terminal-1541091233-r7 { fill: #e3e3e3;font-weight: bold } - .terminal-1541091233-r8 { fill: #98729f } - .terminal-1541091233-r9 { fill: #4ebf71;font-weight: bold } - .terminal-1541091233-r10 { fill: #0178d4 } - .terminal-1541091233-r11 { fill: #14191f } - .terminal-1541091233-r12 { fill: #5d5d5d } - .terminal-1541091233-r13 { fill: #e3e3e3;text-decoration: underline; } + .terminal-3477807222-r1 { fill: #e1e1e1 } + .terminal-3477807222-r2 { fill: #c5c8c6 } + .terminal-3477807222-r3 { fill: #262626 } + .terminal-3477807222-r4 { fill: #4a4a4a } + .terminal-3477807222-r5 { fill: #2e2e2e;font-weight: bold } + .terminal-3477807222-r6 { fill: #e3e3e3 } + .terminal-3477807222-r7 { fill: #e3e3e3;font-weight: bold } + .terminal-3477807222-r8 { fill: #98729f } + .terminal-3477807222-r9 { fill: #4ebf71;font-weight: bold } + .terminal-3477807222-r10 { fill: #0178d4 } + .terminal-3477807222-r11 { fill: #14191f } + .terminal-3477807222-r12 { fill: #5d5d5d } + .terminal-3477807222-r13 { fill: #e3e3e3;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CheckboxApp + CheckboxApp - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - XArrakis 😓 - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔ - XCaladan - ▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔ - XChusuk - ▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - XGiedi Prime - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔ - XGinaz - ▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔ - XGrumman - ▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▃▃ - XKaitain - ▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + X Arrakis 😓 + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔ + X Caladan + ▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔ + X Chusuk + ▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + XGiedi Prime + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔ + XGinaz + ▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔ + X Grumman + ▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▃▃ + XKaitain + ▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ @@ -1560,140 +1560,140 @@ font-weight: 700; } - .terminal-78223076-matrix { + .terminal-4120014803-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-78223076-title { + .terminal-4120014803-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-78223076-r1 { fill: #c5c8c6 } - .terminal-78223076-r2 { fill: #e1e1e1 } - .terminal-78223076-r3 { fill: #454a50 } - .terminal-78223076-r4 { fill: #24292f;font-weight: bold } - .terminal-78223076-r5 { fill: #e2e3e3;font-weight: bold } - .terminal-78223076-r6 { fill: #000000 } - .terminal-78223076-r7 { fill: #004578 } - .terminal-78223076-r8 { fill: #dde6ed;font-weight: bold } - .terminal-78223076-r9 { fill: #dde6ed } - .terminal-78223076-r10 { fill: #211505 } - .terminal-78223076-r11 { fill: #e2e3e3 } + .terminal-4120014803-r1 { fill: #c5c8c6 } + .terminal-4120014803-r2 { fill: #e1e1e1 } + .terminal-4120014803-r3 { fill: #454a50 } + .terminal-4120014803-r4 { fill: #24292f;font-weight: bold } + .terminal-4120014803-r5 { fill: #e2e3e3;font-weight: bold } + .terminal-4120014803-r6 { fill: #000000 } + .terminal-4120014803-r7 { fill: #004578 } + .terminal-4120014803-r8 { fill: #dde6ed;font-weight: bold } + .terminal-4120014803-r9 { fill: #dde6ed } + .terminal-4120014803-r10 { fill: #211505 } + .terminal-4120014803-r11 { fill: #e2e3e3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ContentSwitcherApp + ContentSwitcherApp - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - DataTableMarkdown - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ──────────────────────────────────────────────────────────────────── -  Book                                 Year  -  Dune                                 1965  -  Dune Messiah                         1969  -  Children of Dune                     1976  -  God Emperor of Dune                  1981  -  Heretics of Dune                     1984  -  Chapterhouse: Dune                   1985  - - - - - - - - - - - ──────────────────────────────────────────────────────────────────── + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  DataTable  Markdown  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ──────────────────────────────────────────────────────────────────── +  Book                                 Year  +  Dune                                 1965  +  Dune Messiah                         1969  +  Children of Dune                     1976  +  God Emperor of Dune                  1981  +  Heretics of Dune                     1984  +  Chapterhouse: Dune                   1985  + + + + + + + + + + + ──────────────────────────────────────────────────────────────────── @@ -1724,246 +1724,246 @@ font-weight: 700; } - .terminal-3490228541-matrix { + .terminal-4016107209-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3490228541-title { + .terminal-4016107209-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3490228541-r1 { fill: #c5c8c6 } - .terminal-3490228541-r2 { fill: #e1e1e1 } - .terminal-3490228541-r3 { fill: #454a50 } - .terminal-3490228541-r4 { fill: #e2e3e3;font-weight: bold } - .terminal-3490228541-r5 { fill: #24292f;font-weight: bold } - .terminal-3490228541-r6 { fill: #000000 } - .terminal-3490228541-r7 { fill: #004578 } - .terminal-3490228541-r8 { fill: #121212 } - .terminal-3490228541-r9 { fill: #e2e3e3 } - .terminal-3490228541-r10 { fill: #0053aa } - .terminal-3490228541-r11 { fill: #dde8f3;font-weight: bold } - .terminal-3490228541-r12 { fill: #ffff00;font-weight: bold } - .terminal-3490228541-r13 { fill: #24292f } + .terminal-4016107209-r1 { fill: #c5c8c6 } + .terminal-4016107209-r2 { fill: #e1e1e1 } + .terminal-4016107209-r3 { fill: #454a50 } + .terminal-4016107209-r4 { fill: #e2e3e3;font-weight: bold } + .terminal-4016107209-r5 { fill: #24292f;font-weight: bold } + .terminal-4016107209-r6 { fill: #000000 } + .terminal-4016107209-r7 { fill: #004578 } + .terminal-4016107209-r8 { fill: #121212 } + .terminal-4016107209-r9 { fill: #e2e3e3 } + .terminal-4016107209-r10 { fill: #0053aa } + .terminal-4016107209-r11 { fill: #dde8f3;font-weight: bold } + .terminal-4016107209-r12 { fill: #ffff00;font-weight: bold } + .terminal-4016107209-r13 { fill: #24292f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ContentSwitcherApp + ContentSwitcherApp - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - DataTableMarkdown - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ───────────────────────────────────────── - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - Three Flavours Cornetto - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - The Three Flavours Cornetto  - trilogy is an anthology series  - of Britishcomedic genre films  - directed by Edgar Wright. - - Shaun of the Dead - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - UK  - Release  - FlavourDateDirector -  ━━━━━━━━━━━━━━━━━━━━━━━━━━━  - Strawbe…2004-04…Edgar  - Wright - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - Hot Fuzz - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - UK  - Release  - FlavourDateDirector -  ━━━━━━━━━━━━━━━━━━━━━━━━━━━  - Classico2007-02…Edgar  - Wright - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - The World's End - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - UK  - Release  - FlavourDateDirector - ───────────────────────────────────────── + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  DataTable  Markdown  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ───────────────────────────────────────── + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Three Flavours Cornetto + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + The Three Flavours Cornetto  + trilogy is an anthology series  + of British comedic genre films  + directed by Edgar Wright. + +        Shaun of the Dead        + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + UK       + Release  + Flavour Date    Director  +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━  + Strawbe…2004-04…Edgar     + Wright    + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +            Hot Fuzz             + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + UK       + Release  + Flavour Date    Director  +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━  + Classico2007-02…Edgar     + Wright    + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +         The World's End         + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + UK        + Release   + FlavourDate     Director  + ───────────────────────────────────────── @@ -13551,168 +13551,168 @@ font-weight: 700; } - .terminal-3589000431-matrix { + .terminal-1845817647-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3589000431-title { + .terminal-1845817647-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3589000431-r1 { fill: #c5c8c6 } - .terminal-3589000431-r2 { fill: #e3e3e3 } - .terminal-3589000431-r3 { fill: #e1e1e1 } - .terminal-3589000431-r4 { fill: #e2e2e2 } - .terminal-3589000431-r5 { fill: #14191f } - .terminal-3589000431-r6 { fill: #004578 } - .terminal-3589000431-r7 { fill: #262626 } - .terminal-3589000431-r8 { fill: #e2e2e2;font-weight: bold;text-decoration: underline; } - .terminal-3589000431-r9 { fill: #e2e2e2;font-weight: bold } - .terminal-3589000431-r10 { fill: #7ae998 } - .terminal-3589000431-r11 { fill: #4ebf71;font-weight: bold } - .terminal-3589000431-r12 { fill: #008139 } - .terminal-3589000431-r13 { fill: #dde8f3;font-weight: bold } - .terminal-3589000431-r14 { fill: #ddedf9 } + .terminal-1845817647-r1 { fill: #c5c8c6 } + .terminal-1845817647-r2 { fill: #e3e3e3 } + .terminal-1845817647-r3 { fill: #e1e1e1 } + .terminal-1845817647-r4 { fill: #e2e2e2 } + .terminal-1845817647-r5 { fill: #14191f } + .terminal-1845817647-r6 { fill: #004578 } + .terminal-1845817647-r7 { fill: #262626 } + .terminal-1845817647-r8 { fill: #e2e2e2;font-weight: bold;text-decoration: underline; } + .terminal-1845817647-r9 { fill: #e2e2e2;font-weight: bold } + .terminal-1845817647-r10 { fill: #7ae998 } + .terminal-1845817647-r11 { fill: #4ebf71;font-weight: bold } + .terminal-1845817647-r12 { fill: #008139 } + .terminal-1845817647-r13 { fill: #dde8f3;font-weight: bold } + .terminal-1845817647-r14 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Textual Demo + Textual Demo - - - - Textual Demo - - - TOP - - ▆▆ - - Widgets - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - Rich contentTextual Demo - - Welcome! Textual is a framework for creating sophisticated - applications with the terminal. - CSS - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Start - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - - - - - - - -  CTRL+C  Quit  CTRL+B  Sidebar  CTRL+T  Toggle Dark mode  CTRL+S  Screenshot  F1  Notes  + + + + Textual Demo + + + TOP + + ▆▆ + + Widgets + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + Rich contentTextual Demo + + Welcome! Textual is a framework for creating sophisticated + applications with the terminal.                            + CSS + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Start  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + +  CTRL+C  Quit  CTRL+B  Sidebar  CTRL+T  Toggle Dark mode  CTRL+S  Screenshot  F1  Notes  @@ -13742,162 +13742,162 @@ font-weight: 700; } - .terminal-3209943725-matrix { + .terminal-3864303289-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3209943725-title { + .terminal-3864303289-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3209943725-r1 { fill: #454a50 } - .terminal-3209943725-r2 { fill: #507bb3 } - .terminal-3209943725-r3 { fill: #7ae998 } - .terminal-3209943725-r4 { fill: #ffcf56 } - .terminal-3209943725-r5 { fill: #e76580 } - .terminal-3209943725-r6 { fill: #c5c8c6 } - .terminal-3209943725-r7 { fill: #24292f;font-weight: bold } - .terminal-3209943725-r8 { fill: #dde6ed;font-weight: bold } - .terminal-3209943725-r9 { fill: #0a180e;font-weight: bold } - .terminal-3209943725-r10 { fill: #211505;font-weight: bold } - .terminal-3209943725-r11 { fill: #f5e5e9;font-weight: bold } - .terminal-3209943725-r12 { fill: #000000 } - .terminal-3209943725-r13 { fill: #001541 } - .terminal-3209943725-r14 { fill: #008139 } - .terminal-3209943725-r15 { fill: #b86b00 } - .terminal-3209943725-r16 { fill: #780028 } - .terminal-3209943725-r17 { fill: #303336 } - .terminal-3209943725-r18 { fill: #364b66 } - .terminal-3209943725-r19 { fill: #4a8159 } - .terminal-3209943725-r20 { fill: #8b7439 } - .terminal-3209943725-r21 { fill: #80404d } - .terminal-3209943725-r22 { fill: #a7a7a7;font-weight: bold } - .terminal-3209943725-r23 { fill: #a5a9ac;font-weight: bold } - .terminal-3209943725-r24 { fill: #0e1510;font-weight: bold } - .terminal-3209943725-r25 { fill: #19140c;font-weight: bold } - .terminal-3209943725-r26 { fill: #b0a8aa;font-weight: bold } - .terminal-3209943725-r27 { fill: #0f0f0f } - .terminal-3209943725-r28 { fill: #0f192e } - .terminal-3209943725-r29 { fill: #0f4e2a } - .terminal-3209943725-r30 { fill: #68430f } - .terminal-3209943725-r31 { fill: #4a0f22 } - .terminal-3209943725-r32 { fill: #e2e3e3;font-weight: bold } + .terminal-3864303289-r1 { fill: #454a50 } + .terminal-3864303289-r2 { fill: #507bb3 } + .terminal-3864303289-r3 { fill: #7ae998 } + .terminal-3864303289-r4 { fill: #ffcf56 } + .terminal-3864303289-r5 { fill: #e76580 } + .terminal-3864303289-r6 { fill: #c5c8c6 } + .terminal-3864303289-r7 { fill: #24292f;font-weight: bold } + .terminal-3864303289-r8 { fill: #dde6ed;font-weight: bold } + .terminal-3864303289-r9 { fill: #0a180e;font-weight: bold } + .terminal-3864303289-r10 { fill: #211505;font-weight: bold } + .terminal-3864303289-r11 { fill: #f5e5e9;font-weight: bold } + .terminal-3864303289-r12 { fill: #000000 } + .terminal-3864303289-r13 { fill: #001541 } + .terminal-3864303289-r14 { fill: #008139 } + .terminal-3864303289-r15 { fill: #b86b00 } + .terminal-3864303289-r16 { fill: #780028 } + .terminal-3864303289-r17 { fill: #303336 } + .terminal-3864303289-r18 { fill: #364b66 } + .terminal-3864303289-r19 { fill: #4a8159 } + .terminal-3864303289-r20 { fill: #8b7439 } + .terminal-3864303289-r21 { fill: #80404d } + .terminal-3864303289-r22 { fill: #a7a7a7;font-weight: bold } + .terminal-3864303289-r23 { fill: #a5a9ac;font-weight: bold } + .terminal-3864303289-r24 { fill: #0e1510;font-weight: bold } + .terminal-3864303289-r25 { fill: #19140c;font-weight: bold } + .terminal-3864303289-r26 { fill: #b0a8aa;font-weight: bold } + .terminal-3864303289-r27 { fill: #0f0f0f } + .terminal-3864303289-r28 { fill: #0f192e } + .terminal-3864303289-r29 { fill: #0f4e2a } + .terminal-3864303289-r30 { fill: #68430f } + .terminal-3864303289-r31 { fill: #4a0f22 } + .terminal-3864303289-r32 { fill: #e2e3e3;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WidgetDisableTestApp + WidgetDisableTestApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Button  Button  Button  Button  Button  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Button  Button  Button  Button  Button  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Button  Button  Button  Button  Button  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Button  Button  Button  Button  Button  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Button  Button  Button  Button  Button  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Button  Button  Button  Button  Button  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Button  Button  Button  Button  Button  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Button  Button  Button  Button  Button  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -14412,141 +14412,141 @@ font-weight: 700; } - .terminal-3279266708-matrix { + .terminal-1431734718-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3279266708-title { + .terminal-1431734718-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3279266708-r1 { fill: #1e1e1e } - .terminal-3279266708-r2 { fill: #e1e1e1 } - .terminal-3279266708-r3 { fill: #c5c8c6 } - .terminal-3279266708-r4 { fill: #434343 } - .terminal-3279266708-r5 { fill: #262626;font-weight: bold } - .terminal-3279266708-r6 { fill: #e2e2e2 } - .terminal-3279266708-r7 { fill: #23568b } - .terminal-3279266708-r8 { fill: #ddedf9 } + .terminal-1431734718-r1 { fill: #1e1e1e } + .terminal-1431734718-r2 { fill: #e1e1e1 } + .terminal-1431734718-r3 { fill: #c5c8c6 } + .terminal-1431734718-r4 { fill: #434343 } + .terminal-1431734718-r5 { fill: #262626;font-weight: bold } + .terminal-1431734718-r6 { fill: #e2e2e2 } + .terminal-1431734718-r7 { fill: #23568b } + .terminal-1431734718-r8 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScrollOffByOne + ScrollOffByOne - - - - ▔▔▔▔▔▔▔▔ - X92 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X93 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X94 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X95 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X96 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X97 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X98 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X99▁▁ - ▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔ + X 92 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 93 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 94 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 95 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 96 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 97 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 98 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 99▁▁ + ▁▁▁▁▁▁▁▁ @@ -17420,144 +17420,144 @@ font-weight: 700; } - .terminal-3743315821-matrix { + .terminal-2420307368-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3743315821-title { + .terminal-2420307368-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3743315821-r1 { fill: #7ae998 } - .terminal-3743315821-r2 { fill: #e76580 } - .terminal-3743315821-r3 { fill: #1e1e1e } - .terminal-3743315821-r4 { fill: #121212 } - .terminal-3743315821-r5 { fill: #c5c8c6 } - .terminal-3743315821-r6 { fill: #4ebf71;font-weight: bold } - .terminal-3743315821-r7 { fill: #f5e5e9;font-weight: bold } - .terminal-3743315821-r8 { fill: #e2e2e2 } - .terminal-3743315821-r9 { fill: #0a180e;font-weight: bold } - .terminal-3743315821-r10 { fill: #008139 } - .terminal-3743315821-r11 { fill: #780028 } - .terminal-3743315821-r12 { fill: #e1e1e1 } - .terminal-3743315821-r13 { fill: #23568b } - .terminal-3743315821-r14 { fill: #14191f } + .terminal-2420307368-r1 { fill: #7ae998 } + .terminal-2420307368-r2 { fill: #e76580 } + .terminal-2420307368-r3 { fill: #1e1e1e } + .terminal-2420307368-r4 { fill: #121212 } + .terminal-2420307368-r5 { fill: #c5c8c6 } + .terminal-2420307368-r6 { fill: #4ebf71;font-weight: bold } + .terminal-2420307368-r7 { fill: #f5e5e9;font-weight: bold } + .terminal-2420307368-r8 { fill: #e2e2e2 } + .terminal-2420307368-r9 { fill: #0a180e;font-weight: bold } + .terminal-2420307368-r10 { fill: #008139 } + .terminal-2420307368-r11 { fill: #780028 } + .terminal-2420307368-r12 { fill: #e1e1e1 } + .terminal-2420307368-r13 { fill: #23568b } + .terminal-2420307368-r14 { fill: #14191f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + MyApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - AcceptDeclineAcceptDecline - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - AcceptAccept - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - DeclineDecline - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆▆ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - 00 - - 10000001000000 + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Accept  Decline  Accept  Decline  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Accept  Accept  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Decline  Decline  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆▆ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + 00 + + 10000001000000 @@ -17900,140 +17900,140 @@ font-weight: 700; } - .terminal-1636374768-matrix { + .terminal-1909492357-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1636374768-title { + .terminal-1909492357-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1636374768-r1 { fill: #e1e1e1 } - .terminal-1636374768-r2 { fill: #121212 } - .terminal-1636374768-r3 { fill: #c5c8c6 } - .terminal-1636374768-r4 { fill: #0053aa } - .terminal-1636374768-r5 { fill: #dde8f3;font-weight: bold } - .terminal-1636374768-r6 { fill: #939393;font-weight: bold } - .terminal-1636374768-r7 { fill: #24292f } - .terminal-1636374768-r8 { fill: #e2e3e3;font-weight: bold } - .terminal-1636374768-r9 { fill: #4ebf71;font-weight: bold } - .terminal-1636374768-r10 { fill: #e1e1e1;font-style: italic; } - .terminal-1636374768-r11 { fill: #e1e1e1;font-weight: bold } + .terminal-1909492357-r1 { fill: #e1e1e1 } + .terminal-1909492357-r2 { fill: #121212 } + .terminal-1909492357-r3 { fill: #c5c8c6 } + .terminal-1909492357-r4 { fill: #0053aa } + .terminal-1909492357-r5 { fill: #dde8f3;font-weight: bold } + .terminal-1909492357-r6 { fill: #939393;font-weight: bold } + .terminal-1909492357-r7 { fill: #24292f } + .terminal-1909492357-r8 { fill: #e2e3e3;font-weight: bold } + .terminal-1909492357-r9 { fill: #4ebf71;font-weight: bold } + .terminal-1909492357-r10 { fill: #e1e1e1;font-style: italic; } + .terminal-1909492357-r11 { fill: #e1e1e1;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MarkdownExampleApp + MarkdownExampleApp - - - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - Markdown Document - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - This is an example of Textual's Markdown widget. - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - Features - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Markdown syntax and extensions are supported. - - ● Typography emphasisstronginline code etc. - ● Headers - ● Lists (bullet and ordered) - ● Syntax highlighted code blocks - ● Tables! - - - - + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Markdown Document + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This is an example of Textual's Markdown widget. + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +                               Features                               + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Markdown syntax and extensions are supported. + + ● Typography emphasisstronginline code etc. + ● Headers + ● Lists (bullet and ordered) + ● Syntax highlighted code blocks + ● Tables! + + + + @@ -18064,145 +18064,145 @@ font-weight: 700; } - .terminal-2414919819-matrix { + .terminal-1944007215-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2414919819-title { + .terminal-1944007215-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2414919819-r1 { fill: #c5c8c6 } - .terminal-2414919819-r2 { fill: #24292f } - .terminal-2414919819-r3 { fill: #121212 } - .terminal-2414919819-r4 { fill: #e1e1e1 } - .terminal-2414919819-r5 { fill: #e2e3e3 } - .terminal-2414919819-r6 { fill: #96989b } - .terminal-2414919819-r7 { fill: #0053aa } - .terminal-2414919819-r8 { fill: #008139 } - .terminal-2414919819-r9 { fill: #dde8f3;font-weight: bold } - .terminal-2414919819-r10 { fill: #939393;font-weight: bold } - .terminal-2414919819-r11 { fill: #14191f } - .terminal-2414919819-r12 { fill: #e2e3e3;font-weight: bold } - .terminal-2414919819-r13 { fill: #4ebf71;font-weight: bold } - .terminal-2414919819-r14 { fill: #e1e1e1;font-style: italic; } - .terminal-2414919819-r15 { fill: #e1e1e1;font-weight: bold } + .terminal-1944007215-r1 { fill: #c5c8c6 } + .terminal-1944007215-r2 { fill: #24292f } + .terminal-1944007215-r3 { fill: #121212 } + .terminal-1944007215-r4 { fill: #e1e1e1 } + .terminal-1944007215-r5 { fill: #e2e3e3 } + .terminal-1944007215-r6 { fill: #96989b } + .terminal-1944007215-r7 { fill: #0053aa } + .terminal-1944007215-r8 { fill: #008139 } + .terminal-1944007215-r9 { fill: #dde8f3;font-weight: bold } + .terminal-1944007215-r10 { fill: #939393;font-weight: bold } + .terminal-1944007215-r11 { fill: #14191f } + .terminal-1944007215-r12 { fill: #e2e3e3;font-weight: bold } + .terminal-1944007215-r13 { fill: #4ebf71;font-weight: bold } + .terminal-1944007215-r14 { fill: #e1e1e1;font-style: italic; } + .terminal-1944007215-r15 { fill: #e1e1e1;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MarkdownExampleApp + MarkdownExampleApp - - - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▼  Markdown Viewer - ├──  FeaturesMarkdown Viewer - ├──  Tables - └──  Code Blocks▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - This is an example of Textual's MarkdownViewer - widget. - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅▅ - - Features - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Markdown syntax and extensions are supported. - - ● Typography emphasisstronginline code - etc. - ● Headers - ● Lists (bullet and ordered) - ● Syntax highlighted code blocks - ● Tables! - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▼  Markdown Viewer + ├──  FeaturesMarkdown Viewer + ├──  Tables + └──  Code Blocks▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This is an example of Textual's MarkdownViewer + widget. + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅▅ + +                  Features                  + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Markdown syntax and extensions are supported. + + ● Typography emphasisstronginline code + etc. + ● Headers + ● Lists (bullet and ordered) + ● Syntax highlighted code blocks + ● Tables! + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + @@ -18390,141 +18390,141 @@ font-weight: 700; } - .terminal-2766044148-matrix { + .terminal-764470079-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2766044148-title { + .terminal-764470079-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2766044148-r1 { fill: #e0e0e0 } - .terminal-2766044148-r2 { fill: #656565 } - .terminal-2766044148-r3 { fill: #c5c8c6 } - .terminal-2766044148-r4 { fill: #121212 } - .terminal-2766044148-r5 { fill: #e1e1e1 } - .terminal-2766044148-r6 { fill: #454a50 } - .terminal-2766044148-r7 { fill: #646464 } - .terminal-2766044148-r8 { fill: #24292f;font-weight: bold } - .terminal-2766044148-r9 { fill: #000000 } - .terminal-2766044148-r10 { fill: #63676c;font-weight: bold } - .terminal-2766044148-r11 { fill: #63696e } + .terminal-764470079-r1 { fill: #e0e0e0 } + .terminal-764470079-r2 { fill: #656565 } + .terminal-764470079-r3 { fill: #c5c8c6 } + .terminal-764470079-r4 { fill: #121212 } + .terminal-764470079-r5 { fill: #e1e1e1 } + .terminal-764470079-r6 { fill: #454a50 } + .terminal-764470079-r7 { fill: #646464 } + .terminal-764470079-r8 { fill: #24292f;font-weight: bold } + .terminal-764470079-r9 { fill: #000000 } + .terminal-764470079-r10 { fill: #63676c;font-weight: bold } + .terminal-764470079-r11 { fill: #63696e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ModalApp + ModalApp - - - - DialogModalApp - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - hi! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - OK - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - -  ⏎  Open Dialog  + + + + DialogModalApp + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + hi! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  OK  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + +  ⏎  Open Dialog  @@ -21913,139 +21913,139 @@ font-weight: 700; } - .terminal-3235965674-matrix { + .terminal-398709012-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3235965674-title { + .terminal-398709012-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3235965674-r1 { fill: #e1e1e1 } - .terminal-3235965674-r2 { fill: #c5c8c6 } - .terminal-3235965674-r3 { fill: #1e1e1e } - .terminal-3235965674-r4 { fill: #0178d4 } - .terminal-3235965674-r5 { fill: #575757 } - .terminal-3235965674-r6 { fill: #262626;font-weight: bold } - .terminal-3235965674-r7 { fill: #e2e2e2 } - .terminal-3235965674-r8 { fill: #e2e2e2;text-decoration: underline; } - .terminal-3235965674-r9 { fill: #434343 } - .terminal-3235965674-r10 { fill: #4ebf71;font-weight: bold } + .terminal-398709012-r1 { fill: #e1e1e1 } + .terminal-398709012-r2 { fill: #c5c8c6 } + .terminal-398709012-r3 { fill: #1e1e1e } + .terminal-398709012-r4 { fill: #0178d4 } + .terminal-398709012-r5 { fill: #575757 } + .terminal-398709012-r6 { fill: #262626;font-weight: bold } + .terminal-398709012-r7 { fill: #e2e2e2 } + .terminal-398709012-r8 { fill: #e2e2e2;text-decoration: underline; } + .terminal-398709012-r9 { fill: #434343 } + .terminal-398709012-r10 { fill: #4ebf71;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + RadioChoicesApp - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Battlestar Galactica - Dune 1984 - Dune 2021 - Serenity - Star Trek: The Motion Picture - Star Wars: A New Hope - The Last Starfighter - Total Recall 👉 🔴 - Wing Commander - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Battlestar Galactica +  Dune 1984 +  Dune 2021 +  Serenity +  Star Trek: The Motion Picture +  Star Wars: A New Hope +  The Last Starfighter +  Total Recall 👉 🔴 +  Wing Commander + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + @@ -22076,140 +22076,140 @@ font-weight: 700; } - .terminal-2849727264-matrix { + .terminal-2369252398-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2849727264-title { + .terminal-2369252398-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2849727264-r1 { fill: #e1e1e1 } - .terminal-2849727264-r2 { fill: #c5c8c6 } - .terminal-2849727264-r3 { fill: #1e1e1e } - .terminal-2849727264-r4 { fill: #0178d4 } - .terminal-2849727264-r5 { fill: #575757 } - .terminal-2849727264-r6 { fill: #262626;font-weight: bold } - .terminal-2849727264-r7 { fill: #e2e2e2 } - .terminal-2849727264-r8 { fill: #e2e2e2;text-decoration: underline; } - .terminal-2849727264-r9 { fill: #434343 } - .terminal-2849727264-r10 { fill: #4ebf71;font-weight: bold } - .terminal-2849727264-r11 { fill: #cc555a;font-weight: bold;font-style: italic; } + .terminal-2369252398-r1 { fill: #e1e1e1 } + .terminal-2369252398-r2 { fill: #c5c8c6 } + .terminal-2369252398-r3 { fill: #1e1e1e } + .terminal-2369252398-r4 { fill: #0178d4 } + .terminal-2369252398-r5 { fill: #575757 } + .terminal-2369252398-r6 { fill: #262626;font-weight: bold } + .terminal-2369252398-r7 { fill: #e2e2e2 } + .terminal-2369252398-r8 { fill: #e2e2e2;text-decoration: underline; } + .terminal-2369252398-r9 { fill: #434343 } + .terminal-2369252398-r10 { fill: #4ebf71;font-weight: bold } + .terminal-2369252398-r11 { fill: #cc555a;font-weight: bold;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + RadioChoicesApp - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Battlestar GalacticaAmanda - Dune 1984Connor MacLeod - Dune 2021Duncan MacLeod - SerenityHeather MacLeod - Star Trek: The Motion PictureJoe Dawson - Star Wars: A New HopeKurgan, The - The Last StarfighterMethos - Total Recall 👉 🔴Rachel Ellenstein - Wing CommanderRamírez - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Battlestar Galactica Amanda +  Dune 1984 Connor MacLeod +  Dune 2021 Duncan MacLeod +  Serenity Heather MacLeod +  Star Trek: The Motion Picture Joe Dawson +  Star Wars: A New Hope Kurgan, The +  The Last Starfighter Methos +  Total Recall 👉 🔴 Rachel Ellenstein +  Wing Commander Ramírez + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + @@ -22558,142 +22558,142 @@ font-weight: 700; } - .terminal-2367026169-matrix { + .terminal-2006637091-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2367026169-title { + .terminal-2006637091-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2367026169-r1 { fill: #1e1e1e } - .terminal-2367026169-r2 { fill: #c5c8c6 } - .terminal-2367026169-r3 { fill: #434343 } - .terminal-2367026169-r4 { fill: #262626;font-weight: bold } - .terminal-2367026169-r5 { fill: #e2e2e2 } - .terminal-2367026169-r6 { fill: #e1e1e1 } - .terminal-2367026169-r7 { fill: #23568b } - .terminal-2367026169-r8 { fill: #14191f } - .terminal-2367026169-r9 { fill: #ddedf9 } + .terminal-2006637091-r1 { fill: #1e1e1e } + .terminal-2006637091-r2 { fill: #c5c8c6 } + .terminal-2006637091-r3 { fill: #434343 } + .terminal-2006637091-r4 { fill: #262626;font-weight: bold } + .terminal-2006637091-r5 { fill: #e2e2e2 } + .terminal-2006637091-r6 { fill: #e1e1e1 } + .terminal-2006637091-r7 { fill: #23568b } + .terminal-2006637091-r8 { fill: #14191f } + .terminal-2006637091-r9 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScrollOffByOne + ScrollOffByOne - - - - X43 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X44 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X45 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X46 - ▁▁▁▁▁▁▁▁▃▃ - ▔▔▔▔▔▔▔▔ - X47▂▂ - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X48 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X49 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X50 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ + + + + X 43 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 44 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 45 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 46 + ▁▁▁▁▁▁▁▁▃▃ + ▔▔▔▔▔▔▔▔ + X 47▂▂ + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 48 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 49 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 50 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ @@ -26237,141 +26237,141 @@ font-weight: 700; } - .terminal-1791594084-matrix { + .terminal-4254142758-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1791594084-title { + .terminal-4254142758-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1791594084-r1 { fill: #05080f } - .terminal-1791594084-r2 { fill: #e1e1e1 } - .terminal-1791594084-r3 { fill: #c5c8c6 } - .terminal-1791594084-r4 { fill: #1e2226;font-weight: bold } - .terminal-1791594084-r5 { fill: #35393d } - .terminal-1791594084-r6 { fill: #454a50 } - .terminal-1791594084-r7 { fill: #fea62b } - .terminal-1791594084-r8 { fill: #e2e3e3;font-weight: bold } - .terminal-1791594084-r9 { fill: #000000 } - .terminal-1791594084-r10 { fill: #e2e3e3 } - .terminal-1791594084-r11 { fill: #14191f } + .terminal-4254142758-r1 { fill: #05080f } + .terminal-4254142758-r2 { fill: #e1e1e1 } + .terminal-4254142758-r3 { fill: #c5c8c6 } + .terminal-4254142758-r4 { fill: #1e2226;font-weight: bold } + .terminal-4254142758-r5 { fill: #35393d } + .terminal-4254142758-r6 { fill: #454a50 } + .terminal-4254142758-r7 { fill: #fea62b } + .terminal-4254142758-r8 { fill: #e2e3e3;font-weight: bold } + .terminal-4254142758-r9 { fill: #000000 } + .terminal-4254142758-r10 { fill: #e2e3e3 } + .terminal-4254142758-r11 { fill: #14191f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BorderApp + BorderApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ascii - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔+------------------- ascii --------------------+ - blank|| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|I must not fear.| - dashed|Fear is the mind-killer.| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|Fear is the little-death that brings | - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|total obliteration.| - double|I will face my fear.| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅▅|I will permit it to pass over me and | - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|through me.| - heavy|And when it has gone past, I will turn| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|the inner eye to see its path.| - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|Where the fear has gone there will be | - hidden|nothing. Only I will remain.| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|| - hkey+----------------------------------------------+ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - inner - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  ascii  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔+------------------- ascii --------------------+ +  blank || + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|I must not fear.| +  dashed |Fear is the mind-killer.| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|Fear is the little-death that brings | + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|total obliteration.| +  double |I will face my fear.| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅▅|I will permit it to pass over me and | + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|through me.| +  heavy |And when it has gone past, I will turn| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|the inner eye to see its path.| + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|Where the fear has gone there will be | +  hidden |nothing. Only I will remain.| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|| +  hkey +----------------------------------------------+ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  inner  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -26401,152 +26401,152 @@ font-weight: 700; } - .terminal-3434478773-matrix { + .terminal-3272247705-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3434478773-title { + .terminal-3272247705-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3434478773-r1 { fill: #454a50 } - .terminal-3434478773-r2 { fill: #e1e1e1 } - .terminal-3434478773-r3 { fill: #c5c8c6 } - .terminal-3434478773-r4 { fill: #e0e0e0 } - .terminal-3434478773-r5 { fill: #e2e3e3;font-weight: bold } - .terminal-3434478773-r6 { fill: #14191f } - .terminal-3434478773-r7 { fill: #000000 } - .terminal-3434478773-r8 { fill: #1e1e1e } - .terminal-3434478773-r9 { fill: #e1e1e1;font-weight: bold } - .terminal-3434478773-r10 { fill: #dde0e6 } - .terminal-3434478773-r11 { fill: #99a1b3 } - .terminal-3434478773-r12 { fill: #dde2e8 } - .terminal-3434478773-r13 { fill: #99a7b9 } - .terminal-3434478773-r14 { fill: #dde4ea } - .terminal-3434478773-r15 { fill: #99adc1 } - .terminal-3434478773-r16 { fill: #dde6ed } - .terminal-3434478773-r17 { fill: #99b4c9 } - .terminal-3434478773-r18 { fill: #e2e9ef } - .terminal-3434478773-r19 { fill: #a7bbd0 } - .terminal-3434478773-r20 { fill: #23568b } - .terminal-3434478773-r21 { fill: #dde8f3;font-weight: bold } - .terminal-3434478773-r22 { fill: #ddedf9 } + .terminal-3272247705-r1 { fill: #454a50 } + .terminal-3272247705-r2 { fill: #e1e1e1 } + .terminal-3272247705-r3 { fill: #c5c8c6 } + .terminal-3272247705-r4 { fill: #e0e0e0 } + .terminal-3272247705-r5 { fill: #e2e3e3;font-weight: bold } + .terminal-3272247705-r6 { fill: #14191f } + .terminal-3272247705-r7 { fill: #000000 } + .terminal-3272247705-r8 { fill: #1e1e1e } + .terminal-3272247705-r9 { fill: #e1e1e1;font-weight: bold } + .terminal-3272247705-r10 { fill: #dde0e6 } + .terminal-3272247705-r11 { fill: #99a1b3 } + .terminal-3272247705-r12 { fill: #dde2e8 } + .terminal-3272247705-r13 { fill: #99a7b9 } + .terminal-3272247705-r14 { fill: #dde4ea } + .terminal-3272247705-r15 { fill: #99adc1 } + .terminal-3272247705-r16 { fill: #dde6ed } + .terminal-3272247705-r17 { fill: #99b4c9 } + .terminal-3272247705-r18 { fill: #e2e9ef } + .terminal-3272247705-r19 { fill: #a7bbd0 } + .terminal-3272247705-r20 { fill: #23568b } + .terminal-3272247705-r21 { fill: #dde8f3;font-weight: bold } + .terminal-3272247705-r22 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ColorsApp + ColorsApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - primary▅▅ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - secondary"primary" - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - background$primary-darken-3$text- - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - primary-background$primary-darken-2$text- - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - secondary-background$primary-darken-1$text- - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂▂ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - surface$primary$text- - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - panel$primary-lighten-1$text- - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - boost -  D  Toggle dark mode  + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  primary ▅▅ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  secondary "primary" + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  background $primary-darken-3$text- + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  primary-background $primary-darken-2$text- + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  secondary-background $primary-darken-1$text- + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂▂ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  surface $primary$text- + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  panel $primary-lighten-1$text- + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  boost  +  D  Toggle dark mode  @@ -26576,148 +26576,148 @@ font-weight: 700; } - .terminal-740882655-matrix { + .terminal-456227705-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-740882655-title { + .terminal-456227705-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-740882655-r1 { fill: #454a50 } - .terminal-740882655-r2 { fill: #e1e1e1 } - .terminal-740882655-r3 { fill: #c5c8c6 } - .terminal-740882655-r4 { fill: #24292f;font-weight: bold } - .terminal-740882655-r5 { fill: #262626 } - .terminal-740882655-r6 { fill: #000000 } - .terminal-740882655-r7 { fill: #e2e2e2 } - .terminal-740882655-r8 { fill: #e3e3e3 } - .terminal-740882655-r9 { fill: #e2e3e3;font-weight: bold } - .terminal-740882655-r10 { fill: #14191f } - .terminal-740882655-r11 { fill: #b93c5b } - .terminal-740882655-r12 { fill: #121212 } - .terminal-740882655-r13 { fill: #1e1e1e } - .terminal-740882655-r14 { fill: #fea62b } - .terminal-740882655-r15 { fill: #211505;font-weight: bold } - .terminal-740882655-r16 { fill: #211505 } - .terminal-740882655-r17 { fill: #dde8f3;font-weight: bold } - .terminal-740882655-r18 { fill: #ddedf9 } + .terminal-456227705-r1 { fill: #454a50 } + .terminal-456227705-r2 { fill: #e1e1e1 } + .terminal-456227705-r3 { fill: #c5c8c6 } + .terminal-456227705-r4 { fill: #24292f;font-weight: bold } + .terminal-456227705-r5 { fill: #262626 } + .terminal-456227705-r6 { fill: #000000 } + .terminal-456227705-r7 { fill: #e2e2e2 } + .terminal-456227705-r8 { fill: #e3e3e3 } + .terminal-456227705-r9 { fill: #e2e3e3;font-weight: bold } + .terminal-456227705-r10 { fill: #14191f } + .terminal-456227705-r11 { fill: #b93c5b } + .terminal-456227705-r12 { fill: #121212 } + .terminal-456227705-r13 { fill: #1e1e1e } + .terminal-456227705-r14 { fill: #fea62b } + .terminal-456227705-r15 { fill: #211505;font-weight: bold } + .terminal-456227705-r16 { fill: #211505 } + .terminal-456227705-r17 { fill: #dde8f3;font-weight: bold } + .terminal-456227705-r18 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - EasingApp + EasingApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - round▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Animation Duration:1.0 - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - out_sine - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - out_quint - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Welcome to Textual! - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - out_quartI must not fear. - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Fear is the  - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔mind-killer. - out_quadFear is the  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁little-death that  - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔brings total  - out_expoobliteration. - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I will face my fear. - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔I will permit it to  - out_elasticpass over me and  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁through me. - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔And when it has gone  - out_cubic - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ CTRL+P  Focus: Duration Input  CTRL+B  Toggle Dark  + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  round ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Animation Duration:1.0 + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +  out_sine  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +  out_quint  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Welcome to Textual! + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  out_quart I must not fear. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Fear is the  + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔mind-killer. +  out_quad Fear is the  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁little-death that  + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔brings total  +  out_expo obliteration. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I will face my fear. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔I will permit it to  +  out_elastic pass over me and  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁through me. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔And when it has gone  +  out_cubic  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ CTRL+P  Focus: Duration Input  CTRL+B  Toggle Dark  @@ -26747,146 +26747,146 @@ font-weight: 700; } - .terminal-4085160594-matrix { + .terminal-391476017-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4085160594-title { + .terminal-391476017-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4085160594-r1 { fill: #c5c8c6 } - .terminal-4085160594-r2 { fill: #e3e3e3 } - .terminal-4085160594-r3 { fill: #e1e1e1 } - .terminal-4085160594-r4 { fill: #e1e1e1;text-decoration: underline; } - .terminal-4085160594-r5 { fill: #e1e1e1;font-weight: bold } - .terminal-4085160594-r6 { fill: #e1e1e1;font-style: italic; } - .terminal-4085160594-r7 { fill: #98729f;font-weight: bold } - .terminal-4085160594-r8 { fill: #d0b344 } - .terminal-4085160594-r9 { fill: #98a84b } - .terminal-4085160594-r10 { fill: #00823d;font-style: italic; } - .terminal-4085160594-r11 { fill: #ffcf56 } - .terminal-4085160594-r12 { fill: #e76580 } - .terminal-4085160594-r13 { fill: #fea62b;font-weight: bold } - .terminal-4085160594-r14 { fill: #f5e5e9;font-weight: bold } - .terminal-4085160594-r15 { fill: #b86b00 } - .terminal-4085160594-r16 { fill: #780028 } + .terminal-391476017-r1 { fill: #c5c8c6 } + .terminal-391476017-r2 { fill: #e3e3e3 } + .terminal-391476017-r3 { fill: #e1e1e1 } + .terminal-391476017-r4 { fill: #e1e1e1;text-decoration: underline; } + .terminal-391476017-r5 { fill: #e1e1e1;font-weight: bold } + .terminal-391476017-r6 { fill: #e1e1e1;font-style: italic; } + .terminal-391476017-r7 { fill: #98729f;font-weight: bold } + .terminal-391476017-r8 { fill: #d0b344 } + .terminal-391476017-r9 { fill: #98a84b } + .terminal-391476017-r10 { fill: #00823d;font-style: italic; } + .terminal-391476017-r11 { fill: #ffcf56 } + .terminal-391476017-r12 { fill: #e76580 } + .terminal-391476017-r13 { fill: #fea62b;font-weight: bold } + .terminal-391476017-r14 { fill: #f5e5e9;font-weight: bold } + .terminal-391476017-r15 { fill: #b86b00 } + .terminal-391476017-r16 { fill: #780028 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Textual Keys + Textual Keys - - - - Textual Keys - ╭────────────────────────────────────────────────────────────────────────────╮ - Press some keys! - - To quit the app press ctrl+ctwice or press the Quit button below. - ╰────────────────────────────────────────────────────────────────────────────╯ - Key(key='a'character='a'name='a'is_printable=True) - Key(key='b'character='b'name='b'is_printable=True) - - - - - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ClearQuit - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + Textual Keys + ╭────────────────────────────────────────────────────────────────────────────╮ + Press some keys! + + To quit the app press ctrl+ctwice or press the Quit button below. + ╰────────────────────────────────────────────────────────────────────────────╯ + Key(key='a'character='a'name='a'is_printable=True) + Key(key='b'character='b'name='b'is_printable=True) + + + + + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Clear  Quit  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ From 9d6959881489cc4afb52812bb5cc129994156734 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 11:06:30 +0100 Subject: [PATCH 122/366] Update comment --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0957116744..39fee9fa66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ httpx = "^0.23.1" types-setuptools = "^67.2.0.1" textual-dev = ">=1.0.0" pytest-asyncio = "*" -pytest-textual-snapshot = "0.2.0" # TODO: pinned while while we resolve issue +pytest-textual-snapshot = "0.2.0" # TODO: pinned while while we resolve issue #3042 [tool.black] includes = "src" From 641ea35cd2384988ef1f30c66b0edc1b32e7ba04 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 11:50:19 +0100 Subject: [PATCH 123/366] Remove unused variable --- src/textual/widgets/_text_area.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 9d47fc07bd..ff5a25176b 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -470,7 +470,7 @@ def scroll_cursor_visible(self): row, column = self.selection.end text = self.cursor_line_text[:column] column_offset = cell_len(text.expandtabs(self.indent_width)) - scrolled_amount = self.scroll_to_region( + self.scroll_to_region( Region(x=column_offset, y=row, width=1, height=1), spacing=Spacing(right=self.gutter_width), animate=False, From cbd462313bb9cf34eedc985e5259b702d0285fb1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 11:53:55 +0100 Subject: [PATCH 124/366] Scrolling to cursor leeway --- src/textual/widgets/_text_area.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index ff5a25176b..1f019a755a 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -471,7 +471,7 @@ def scroll_cursor_visible(self): text = self.cursor_line_text[:column] column_offset = cell_len(text.expandtabs(self.indent_width)) self.scroll_to_region( - Region(x=column_offset, y=row, width=1, height=1), + Region(x=column_offset, y=row, width=2, height=1), spacing=Spacing(right=self.gutter_width), animate=False, ) From 8452a3b5aff5a1e2aa57ccadb542fa95b72fd2b6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 12:02:20 +0100 Subject: [PATCH 125/366] Add tree_sitter_languages wheel --- poetry.lock | 61 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 2b19fac77f..7efbfcf548 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1020,6 +1020,17 @@ category = "main" optional = false python-versions = ">=3.3" +[[package]] +name = "tree-sitter-languages" +version = "1.7.0" +description = "Binary Python wheels for all tree sitter languages." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +tree-sitter = "*" + [[package]] name = "typed-ast" version = "1.5.5" @@ -1134,7 +1145,7 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "bf1b5c8ce33bd98176613966dc6c84eca62502b9d87af80165d6be9e6780cf6f" +content-hash = "faffc64aa9c2ff4fa595d3a6481c2d6456f88253edbd46795048fc8d760e87d1" [metadata.files] aiohttp = [ @@ -2151,6 +2162,54 @@ tree-sitter = [ {file = "tree_sitter-0.20.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:6f11a1fd909dcf569e7b1d98861a837436799e757bbbc5cd5280989050929e12"}, {file = "tree_sitter-0.20.1.tar.gz", hash = "sha256:e93f082c545d6649bcfb5d681ed255eb004a6ce22988971a128f40692feec60d"}, ] +tree-sitter-languages = [ + {file = "tree_sitter_languages-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fd8b856c224a74c395ed9495761c3ef8ba86014dbf6037d73634436ae683c808"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:277d1bec6e101a26a4445cd7cb1eb8f8cf5a9bbad1ca80692bfae1af63568272"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0473bd896799ccc87f428766813ddedd3506cad8430dbe863b663c81d7387680"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6799419bc7e3029112f2a3f8b77b6c299f94f03bb70e5c31a437b3180486be"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e5b705c8ce6ef47fc461484878956ecd42a67cbeb0a17e323b86a4439a8fdc3d"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:28a732be6fced2f70184c1b34f64961e3b6259fe6d5f7540c91028c2a43a7109"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-win32.whl", hash = "sha256:f5cdb1ec88f0b8c617330c953555a20cc7e96ca6b1f5c68ab6db347e869cfeeb"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:26cb344a75798fce1a73b690504d8e7789f6ba25a178efcd203444d7868caf38"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:433b56cb3dca02b30f21c596f431a2cff90905326be1f8913c3515acb984b21e"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96686390e1a01af44aedef7b33d6be82de3cf674a98a5c7b417e540e6afa62cc"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25a4b6d559fbd76c6ec1b73cf03d09f53aaa5a1b61078a3f518b162866d9d97e"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e504f199c7a4c8b1b1efb05a063450aa23234feea6fa6c06f4077f7248ea9c98"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6b29856e9314b5f68f05dfa45e6674f47535229dda32294ba6d129077a97759c"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:786fdaf3d2120eef9384b0f22d7e2e42a561073ba753c7b438e90a1e7b351650"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-win32.whl", hash = "sha256:a55a7007056d0927b78481b437d79ea0487cc991c7f9c19d67adcceac3d47f53"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:4b01d3bdf7ce2aeee4d0df62071a0ca91e618a29845686a5bd714d93c5ef3b36"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b603f1ad01bfb9d178f965125e2528cb7da9666d180f4a9a1acfaedbf5862ea"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70610aa26dd985d2fb9eb07ea8eacc3ceb0cc9c2e91416f51305120cfd919e28"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0444ebc8bdb7dc0d66a816050cfd52376c4e62a94a9c54fde90b29acf3e4bab1"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7eeb5a3307ff1c0994ffff5ea37ec656a716a728b8c9359374104da521a76ded"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:6c319cef16f2df667f1c165fe4eee160f2b51a0c4b61db1e70de2ab86420ca9a"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-win32.whl", hash = "sha256:b216650126d95d494f927393903e836a7ef5f0c4db0834f3a0b576f97c13abaf"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6c96e5785d164a205962a10256808b3d12dccee9827ec88a46899063a2a2d28"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adafeabbd8d47b80122fad18bb61c25ed3da04f5347b7d774b53826accb27b7a"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50e2bc5d2da770ecd5af94f9d716faa4764f890fd61bc0a488e9269653d9fb71"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac773097cff7de6cf265c5be9990b4c6690161452da1d9fc41021d4bf7e8c73a"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b233bfc48cf0f16436200afc7d7643cd87101c321de25b919b61f21f1693aa52"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:eab3caedf50467045ed5cab776a57b494332616376d387c6600fd7ea4f5483cf"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-win32.whl", hash = "sha256:d533f743a22f5696494d3a5a60adb4cfbef63d58b8b5622993d93d6d0a602444"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:aab96f64be30c9f73d6dc958ec22bb1a9fe70e90b2d2a3d233d537b347cea729"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1bf89d771621e28847036b377f865f947e555a6654356d21beab738bb2531a69"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b2f171089ec3c4f1de275edc8f0722e1e3dc7a54e83107098315ea2f0952cfcd"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a091577d3a8454c40f813ee2834314c73cc504522f70f9e33d7c2268d33973f9"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8287efa87d080b340b583a6e81266cc3d8266deb61b8f3312649a9d1562e665a"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9c5080c06a2df7a59c69d2422a6ae83a5e37e92d57c4bd5e572d0eb5226ab3b0"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ca8f629cfb406a2f9b9f8a3a5c804d4d1ba4cdca41cccba63f51fc1bab13e5de"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-win32.whl", hash = "sha256:fd3561b37a99c9d501719819a8736529ae3a6d597128c15be432d1855f3cb0d9"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:377ad60f7a7bf27315676c4fa84cc766aa0019c1e556083763136ed951e934c0"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1dc71b68e48f58cd5b6a9ab7a541714201815629a6554a969cfc579a6ee6e53"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fb1521367b14c275bef70997ea90526e7049f840ba1bbd3ef56c72f5b15596e9"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f73651f7e78371dc3d455e8aba510cc6fb9e1ac1d648c3334157950781eb295"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:049b0dd63be721fe3f9642a2b5a044bea2852de2b35818467996242ae4b7f01f"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c428a8e1f5ecc4eb5c79abff3eb2881123446cde16fd1d8866d527470a6fdd2f"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:40fb3fc11ff90caf65b4713feeb6c4852e5d2a04ef8ae6a2ac734a702a6a6c7e"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-win32.whl", hash = "sha256:f28e9904833b7a909f8227c4560401049bd3310cebe3e0a884d9461f783b9af2"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ea47ee390ec2e1c9bf96d7b418775263766021a834910c9f2d578f95a3e27d0f"}, +] typed-ast = [ {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, diff --git a/pyproject.toml b/pyproject.toml index 39fee9fa66..861de6619b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ markdown-it-py = {extras = ["plugins", "linkify"], version = ">=2.1.0"} importlib-metadata = ">=4.11.3" typing-extensions = "^4.4.0" tree-sitter = "^0.20.1" +tree_sitter_languages = {version = ">=1.7.0", python = "^3.8"} [tool.poetry.group.dev.dependencies] pytest = "^7.1.3" From 2f24712cc30bf54f08c78481893a9e65c75371b1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 12:17:49 +0100 Subject: [PATCH 126/366] Docstring updates --- src/textual/widgets/_text_area.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 1f019a755a..91acbecfb0 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -191,10 +191,10 @@ class TextArea(ScrollView, can_focus=True): """The language to use for syntax highlighting (via tree-sitter).""" selection: Reactive[Selection] = reactive(Selection()) - """The selection location (zero-based line_index, offset).""" + """The selection start and end locations (zero-based line_index, offset).""" show_line_numbers: Reactive[bool] = reactive(True) - """True to show line number gutter, otherwise False.""" + """True to show the line number column, otherwise False.""" indent_width: Reactive[int] = reactive(4) """The width of tabs or the number of spaces to insert on pressing the `tab` key.""" From 8218807a16ed9f99ad9cbfba418d9e3b87f296b1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 15:51:57 +0100 Subject: [PATCH 127/366] Renaming things, fix subtle scrolling issue at end of document when scrollbar appears after insert --- src/textual/document/_document.py | 4 +- src/textual/widgets/_text_area.py | 95 +++++++++++++++++++++---------- tests/document/test_document.py | 16 +++--- 3 files changed, 74 insertions(+), 41 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 78e5302b4c..852949cd26 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -116,7 +116,7 @@ def delete_range(self, start: Location, end: Location) -> str: lines = self._lines - deleted_text = self.get_selected_text(top, bottom) + deleted_text = self.get_text_range(top, bottom) if top_row == bottom_row: line = lines[top_row] @@ -129,7 +129,7 @@ def delete_range(self, start: Location, end: Location) -> str: return deleted_text - def get_selected_text(self, start: Location, end: Location) -> str: + def get_text_range(self, start: Location, end: Location) -> str: """Get the text that falls between the start and end locations. Args: diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 91acbecfb0..85e75dbf2d 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -2,7 +2,7 @@ import re from dataclasses import dataclass, field -from typing import ClassVar +from typing import Any, ClassVar from rich.text import Text @@ -12,7 +12,8 @@ from textual._types import Literal, Protocol, runtime_checkable from textual.binding import Binding from textual.document._document import Document, Location, Selection -from textual.geometry import Offset, Region, Size, Spacing, clamp +from textual.events import MouseEvent +from textual.geometry import Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView from textual.strip import Strip @@ -33,7 +34,7 @@ def do(self, text_area: TextArea) -> object | None: def undo(self, text_area: TextArea) -> object | None: """Undo the action.""" - def post_refresh(self, text_area: TextArea) -> None: + def post_resize(self, text_area: TextArea) -> None: """Code to execute after content size recalculated and repainted.""" @@ -83,6 +84,8 @@ def post_refresh(self, text_area: TextArea) -> None: else: text_area.selection = Selection.cursor(self._edit_end) + text_area.record_cursor_offset() + @dataclass class Delete: @@ -117,6 +120,8 @@ def post_refresh(self, text_area: TextArea) -> None: else: text_area.selection = Selection.cursor(self.from_location) + text_area.record_cursor_offset() + class TextArea(ScrollView, can_focus=True): DEFAULT_CSS = """\ @@ -207,6 +212,7 @@ def __init__( disabled: bool = False, ) -> None: super().__init__(name=name, id=id, classes=classes, disabled=disabled) + self._document = Document("") """The document this widget is currently editing.""" @@ -227,6 +233,14 @@ def __init__( self._selecting = False """True if we're currently selecting text, otherwise False.""" + def _watch_selection(self) -> None: + self.scroll_cursor_visible() + + def _validate_selection(self, selection: Selection) -> Selection: + start, end = selection + clamp_visitable = self.clamp_visitable + return Selection(clamp_visitable(start), clamp_visitable(end)) + def load_text(self, text: str) -> None: """Load text from a string into the editor. @@ -329,14 +343,33 @@ def render_line(self, widget_y: int) -> Strip: strip = Strip.join([gutter_strip, text_strip]).simplify() return strip + @property + def text(self) -> str: + """The entire text content of the document.""" + return self._document.text + @property def selected_text(self) -> str: + """The text between the start and end points of the current selection.""" start, end = self.selection + return self.get_text_range(start, end) + + def get_text_range(self, start: Location, end: Location) -> str: + """Get the text between a start and end location. + + Args: + start: The start location. + end: The end location. + + Returns: + The text between start and end. + """ start, end = _fix_direction(start, end) - return self._document.get_selected_text(start, end) + return self._document.get_text_range(start, end) @property def gutter_width(self) -> int: + """The width of the gutter (the left column containing line numbers).""" # The longest number in the gutter plus two extra characters: `│ `. gutter_margin = 2 gutter_width = ( @@ -346,7 +379,16 @@ def gutter_width(self) -> int: ) return gutter_width - def edit(self, edit: Edit) -> object | None: + def edit(self, edit: Edit) -> Any: + """Perform an Edit. + + Args: + edit: The Edit to perform. + + Returns: + Data relating to the edit that may be useful. The data returned + may be different depending on the edit performed. + """ result = edit.do(self) # TODO: Think about this... @@ -354,7 +396,7 @@ def edit(self, edit: Edit) -> object | None: # self._undo_stack = self._undo_stack[-20:] self._refresh_size() - edit.post_refresh(self) + edit.post_resize(self) return result @@ -379,12 +421,12 @@ def _on_key(self, event: events.Key) -> None: start, end = self.selection self.insert_text_range(insert, start, end) - def get_target_document_location(self, offset: Offset) -> Location: + def get_target_document_location(self, event: MouseEvent) -> Location: scroll_x, scroll_y = self.scroll_offset - target_x = offset.x - self.gutter_width + scroll_x - self.gutter.left + target_x = event.x - self.gutter_width + scroll_x - self.gutter.left target_x = max(target_x, 0) target_row = clamp( - offset.y + scroll_y - self.gutter.top, + event.y + scroll_y - self.gutter.top, 0, self._document.line_count - 1, ) @@ -406,13 +448,13 @@ def _on_mouse_move(self, event: events.MouseMove) -> None: def _on_mouse_up(self, event: events.MouseUp) -> None: self._selecting = False self.capture_mouse(False) - self._record_last_intentional_cell_width() + self.record_cursor_offset() def _on_paste(self, event: events.Paste) -> None: text = event.text if text: start, end = self.selection - self.insert_text_range(text, start, end, end) + self.insert_text_range(text, start, end) def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: """Return the column that the cell width corresponds to on the given row.""" @@ -425,14 +467,6 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: return column_index return len(line) - def _watch_selection(self) -> None: - self.scroll_cursor_visible() - - def _validate_selection(self, selection: Selection) -> Selection: - start, end = selection - clamp_visitable = self.clamp_visitable - return Selection(clamp_visitable(start), clamp_visitable(end)) - # --- Cursor/selection utilities def is_visitable(self, location: Location) -> bool: """Return True if the location is somewhere that can naturally be reached by the cursor. @@ -474,7 +508,9 @@ def scroll_cursor_visible(self): Region(x=column_offset, y=row, width=2, height=1), spacing=Spacing(right=self.gutter_width), animate=False, + force=True, ) + log.debug("scrolling cursor visible") @property def cursor_at_first_row(self) -> bool: @@ -520,7 +556,7 @@ def cursor_to_line_end(self, select: bool = False) -> None: else: self.selection = Selection.cursor((cursor_row, target_column)) - self._record_last_intentional_cell_width() + self.record_cursor_offset() def cursor_to_line_start(self, select: bool = False) -> None: """Move the cursor to the start of the line. @@ -535,7 +571,7 @@ def cursor_to_line_start(self, select: bool = False) -> None: else: self.selection = Selection.cursor((cursor_row, 0)) - self._record_last_intentional_cell_width() + self.record_cursor_offset() # ------ Cursor movement actions def action_cursor_left(self) -> None: @@ -546,7 +582,7 @@ def action_cursor_left(self) -> None: """ target = self.get_cursor_left_location() self.selection = Selection.cursor(target) - self._record_last_intentional_cell_width() + self.record_cursor_offset() def action_cursor_left_select(self): """Move the end of the selection one location to the left. @@ -556,7 +592,7 @@ def action_cursor_left_select(self): new_cursor_location = self.get_cursor_left_location() selection_start, selection_end = self.selection self.selection = Selection(selection_start, new_cursor_location) - self._record_last_intentional_cell_width() + self.record_cursor_offset() def get_cursor_left_location(self) -> Location: """Get the location the cursor will move to if it moves left.""" @@ -575,7 +611,7 @@ def action_cursor_right(self) -> None: """ target = self.get_cursor_right_location() self.selection = Selection.cursor(target) - self._record_last_intentional_cell_width() + self.record_cursor_offset() def action_cursor_right_select(self): """Move the end of the selection one location to the right. @@ -585,7 +621,7 @@ def action_cursor_right_select(self): new_cursor_location = self.get_cursor_right_location() selection_start, selection_end = self.selection self.selection = Selection(selection_start, new_cursor_location) - self._record_last_intentional_cell_width() + self.record_cursor_offset() def get_cursor_right_location(self) -> Location: """Get the location the cursor will move to if it moves right.""" @@ -677,7 +713,7 @@ def action_cursor_left_word(self) -> None: cursor_column = 0 self.selection = Selection.cursor((cursor_row, cursor_column)) - self._record_last_intentional_cell_width() + self.record_cursor_offset() def action_cursor_right_word(self) -> None: """Move the cursor right by a single word, skipping spaces.""" @@ -703,7 +739,7 @@ def action_cursor_right_word(self) -> None: cursor_column = len(self._document[cursor_row]) self.selection = Selection.cursor((cursor_row, cursor_column)) - self._record_last_intentional_cell_width() + self.record_cursor_offset() def action_cursor_page_up(self) -> None: height = self.content_size.height @@ -732,7 +768,7 @@ def get_column_cell_width(self, row: int, column: int) -> int: line = self._document[row] return cell_len(line[:column].expandtabs(self.indent_width)) - def _record_last_intentional_cell_width(self) -> None: + def record_cursor_offset(self) -> None: row, column = self.selection.end column_cell_length = self.get_column_cell_width(row, column) self._last_intentional_cell_width = column_cell_length @@ -745,7 +781,6 @@ def insert_text( cursor_destination: Location | None = None, ) -> None: self.edit(Insert(text, location, location, cursor_destination)) - self._record_last_intentional_cell_width() def insert_text_range( self, @@ -755,7 +790,6 @@ def insert_text_range( cursor_destination: Location | None = None, ) -> None: self.edit(Insert(text, from_location, to_location, cursor_destination)) - self._record_last_intentional_cell_width() def delete_range( self, @@ -766,7 +800,6 @@ def delete_range( """Delete text between from_location and to_location.""" top, bottom = _fix_direction(from_location, to_location) deleted_text = self.edit(Delete(top, bottom, cursor_destination)) - self._record_last_intentional_cell_width() return deleted_text def clear(self) -> None: diff --git a/tests/document/test_document.py b/tests/document/test_document.py index 69a87e10bf..ca4e2036f4 100644 --- a/tests/document/test_document.py +++ b/tests/document/test_document.py @@ -54,47 +54,47 @@ def test_newline_windows(): def test_get_selected_text_no_selection(): document = Document(TEXT) - selection = document.get_selected_text((0, 0), (0, 0)) + selection = document.get_text_range((0, 0), (0, 0)) assert selection == "" def test_get_selected_text_single_line(): document = Document(TEXT_WINDOWS) - selection = document.get_selected_text((0, 2), (0, 6)) + selection = document.get_text_range((0, 2), (0, 6)) assert selection == "must" def test_get_selected_text_multiple_lines_unix(): document = Document(TEXT) - selection = document.get_selected_text((0, 2), (1, 2)) + selection = document.get_text_range((0, 2), (1, 2)) assert selection == "must not fear.\nFe" def test_get_selected_text_multiple_lines_windows(): document = Document(TEXT_WINDOWS) - selection = document.get_selected_text((0, 2), (1, 2)) + selection = document.get_text_range((0, 2), (1, 2)) assert selection == "must not fear.\r\nFe" def test_get_selected_text_including_final_newline_unix(): document = Document(TEXT_NEWLINE) - selection = document.get_selected_text((0, 0), (2, 0)) + selection = document.get_text_range((0, 0), (2, 0)) assert selection == TEXT_NEWLINE def test_get_selected_text_including_final_newline_windows(): document = Document(TEXT_WINDOWS_NEWLINE) - selection = document.get_selected_text((0, 0), (2, 0)) + selection = document.get_text_range((0, 0), (2, 0)) assert selection == TEXT_WINDOWS_NEWLINE def test_get_selected_text_no_newline_at_end_of_file(): document = Document(TEXT) - selection = document.get_selected_text((0, 0), (2, 0)) + selection = document.get_text_range((0, 0), (2, 0)) assert selection == TEXT def test_get_selected_text_no_newline_at_end_of_file_windows(): document = Document(TEXT_WINDOWS) - selection = document.get_selected_text((0, 0), (2, 0)) + selection = document.get_text_range((0, 0), (2, 0)) assert selection == TEXT_WINDOWS From 22952ff48782d82a9544a59d535f9983cbfe8522 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 15:59:53 +0100 Subject: [PATCH 128/366] Using scroll offset y --- src/textual/widgets/_text_area.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 85e75dbf2d..784489a918 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -72,7 +72,7 @@ def undo(self, text_area: TextArea) -> None: text_area: The TextArea to undo the insert operation on. """ - def post_refresh(self, text_area: TextArea) -> None: + def post_resize(self, text_area: TextArea) -> None: """Update the cursor location after the widget has been refreshed. Args: @@ -113,7 +113,7 @@ def do(self, text_area: TextArea) -> str: def undo(self, text_area: TextArea) -> None: """Undo the delete action.""" - def post_refresh(self, text_area: TextArea) -> None: + def post_resize(self, text_area: TextArea) -> None: cursor_destination = self.cursor_destination if cursor_destination is not None: text_area.selection = Selection.cursor(cursor_destination) @@ -251,16 +251,20 @@ def load_text(self, text: str) -> None: self._refresh_size() def _refresh_size(self) -> None: - # Calculate document + """Calculate the size of the document.""" lines = self._document.lines + # TODO - this is a prime candidate for optimisation. text_width = max(cell_len(line.expandtabs(self.indent_width)) for line in lines) height = len(lines) + # +1 width to make space for the cursor resting at the end of the line self.virtual_size = Size(text_width + self.gutter_width + 1, height) def render_line(self, widget_y: int) -> Strip: document = self._document - line_index = round(self.scroll_y + widget_y) + scroll_x, scroll_y = self.scroll_offset + + line_index = widget_y + scroll_y out_of_bounds = line_index >= document.line_count if out_of_bounds: return Strip.blank(self.size.width) @@ -333,11 +337,9 @@ def render_line(self, widget_y: int) -> Strip: # Crop the line to show only the visible part (some may be scrolled out of view) virtual_width, virtual_height = self.virtual_size - text_crop_start = self.scroll_offset.x - text_crop_end = text_crop_start + virtual_width gutter_strip = Strip(gutter_segments) - text_strip = Strip(text_segments).crop(text_crop_start, text_crop_end) + text_strip = Strip(text_segments).crop(scroll_x, scroll_x + virtual_width) # Join and return the gutter and the visible portion of this line strip = Strip.join([gutter_strip, text_strip]).simplify() @@ -510,7 +512,6 @@ def scroll_cursor_visible(self): animate=False, force=True, ) - log.debug("scrolling cursor visible") @property def cursor_at_first_row(self) -> bool: From 6edb6b3f097e2d7c76e1c550ffacb4ccbe416713 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 16:45:30 +0100 Subject: [PATCH 129/366] Add SyntaxAwareDocument support --- .../document/_syntax_aware_document.py | 14 ++++++----- src/textual/widgets/_text_area.py | 24 ++++++++++++++++--- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 8b76233351..38aa6ef0cf 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -76,17 +76,18 @@ def __init__(self, text: str, language: str | None = None): def insert_range( self, start: tuple[int, int], end: tuple[int, int], text: str ) -> tuple[int, int]: - end_location = super().insert_range(start, end, text) - top, bottom = _fix_direction(start, end) - start_byte = self._location_to_byte_offset(top) + old_end_byte = self._location_to_byte_offset(bottom) + + end_location = super().insert_range(start, end, text) + text_byte_length = len(text.encode("utf-8")) if self._syntax_tree is not None: self._syntax_tree.edit( start_byte=start_byte, - old_end_byte=self._location_to_byte_offset(bottom), + old_end_byte=old_end_byte, new_end_byte=start_byte + text_byte_length, start_point=top, old_end_point=bottom, @@ -109,12 +110,13 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: Returns: A string containing the deleted text. """ - deleted_text = super().delete_range(start, end) top, bottom = _fix_direction(start, end) start_byte = self._location_to_byte_offset(top) old_end_byte = self._location_to_byte_offset(bottom) + deleted_text = super().delete_range(start, end) + if self._syntax_tree is not None: self._syntax_tree.edit( start_byte=start_byte, @@ -217,7 +219,7 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No def get_line_text(self, line_index: int) -> Text: null_style = Style.null() - line = Text(self[line_index]) + line = Text(self[line_index], end="") if self._highlights: highlights = self._highlights[line_index] diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 784489a918..2a7b137026 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -12,6 +12,7 @@ from textual._types import Literal, Protocol, runtime_checkable from textual.binding import Binding from textual.document._document import Document, Location, Selection +from textual.document._syntax_aware_document import SyntaxAwareDocument from textual.events import MouseEvent from textual.geometry import Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive @@ -192,7 +193,7 @@ class TextArea(ScrollView, can_focus=True): Binding("ctrl+k", "delete_to_end_of_line", "delete to line end", show=False), ] - language: Reactive[str | None] = reactive(None) + language: Reactive[str | None] = reactive(None, always_update=True) """The language to use for syntax highlighting (via tree-sitter).""" selection: Reactive[Selection] = reactive(Selection()) @@ -231,7 +232,7 @@ def __init__( """A stack (the end of the list is the top of the stack) for tracking edits.""" self._selecting = False - """True if we're currently selecting text, otherwise False.""" + """True if we're currently selecting text using the mouse, otherwise False.""" def _watch_selection(self) -> None: self.scroll_cursor_visible() @@ -241,6 +242,22 @@ def _validate_selection(self, selection: Selection) -> Selection: clamp_visitable = self.clamp_visitable return Selection(clamp_visitable(start), clamp_visitable(end)) + def _watch_language(self, language: str | None) -> None: + """When the language used is updated, update the type of document.""" + if not language: + self._document = Document(self._document.text) + else: + try: + # SyntaxAwareDocument isn't available on Python 3.7. + from textual.document._syntax_aware_document import SyntaxAwareDocument + + self._document = SyntaxAwareDocument(self._document.text, language) + except ImportError: + # Fall back to the standard document. + self._document = Document(self._document.text) + + self._refresh_size() + def load_text(self, text: str) -> None: """Load text from a string into the editor. @@ -254,7 +271,8 @@ def _refresh_size(self) -> None: """Calculate the size of the document.""" lines = self._document.lines # TODO - this is a prime candidate for optimisation. - text_width = max(cell_len(line.expandtabs(self.indent_width)) for line in lines) + cell_lengths = [cell_len(line.expandtabs(self.indent_width)) for line in lines] + text_width = max(cell_lengths or [1]) height = len(lines) # +1 width to make space for the cursor resting at the end of the line self.virtual_size = Size(text_width + self.gutter_width + 1, height) From 4cc6c376b0b0370830590ceaf934cafd8cce201d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 16:53:08 +0100 Subject: [PATCH 130/366] Tree-sitter byte offset calculations take eol markers into account --- src/textual/document/_document.py | 12 ++++++++++-- src/textual/document/_syntax_aware_document.py | 6 +++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 852949cd26..d59ae2f273 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -161,11 +161,19 @@ def get_text_range(self, start: Location, end: Location) -> str: @property def line_count(self) -> int: - """Returns the number of lines in the document""" + """Returns the number of lines in the document.""" return len(self._lines) def get_line_text(self, index: int) -> Text: - """Returns the line with the given index from the document""" + """Returns the line with the given index from the document. + + Args: + index: The index of the line in the document. + + Returns: + The Text instance representing the line. When overriding + this method, ensure the returned Text instance has `end=""`. + """ line_string = self[index] line_string = line_string.replace("\n", "").replace("\r", "") return Text(line_string, end="") diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 38aa6ef0cf..de0e3ab51a 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -138,7 +138,10 @@ def _location_to_byte_offset(self, location: tuple[int, int]) -> int: lines = self._lines row, column = location lines_above = lines[:row] - bytes_lines_above = sum(len(line.encode("utf-8")) + 1 for line in lines_above) + end_of_line_width = len(self.newline) + bytes_lines_above = sum( + len(line.encode("utf-8")) + end_of_line_width for line in lines_above + ) bytes_this_line_left_of_cursor = len(lines[row][:column].encode("utf-8")) return bytes_lines_above + bytes_this_line_left_of_cursor @@ -211,6 +214,7 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No if row_out_of_bounds or column_out_of_bounds: return_value = None elif column == len(lines[row]) and row < len(lines): + # TODO: Need to handle \r\n case here. return_value = "\n".encode("utf8") else: return_value = lines[row][column].encode("utf8") From 7c04369c018f64b5f3ddaa0f673afc3756c0a02b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 1 Aug 2023 17:08:32 +0100 Subject: [PATCH 131/366] Add docstring and switch to tree-sitter-languages wheels - although the wheels arent working --- .../document/_syntax_aware_document.py | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index de0e3ab51a..dd5a08fc53 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -8,6 +8,7 @@ from rich.text import Text from tree_sitter import Language, Parser, Tree from tree_sitter.binding import Query +from tree_sitter_languages import get_language, get_parser from textual._fix_direction import _fix_direction from textual.document._document import Document @@ -54,12 +55,11 @@ def __init__(self, text: str, language: str | None = None): # TODO validate language string - self._language: Language = Language(LANGUAGES_PATH.resolve(), language) + self._language: Language = get_language(language) """The tree-sitter Language.""" - self._parser: Parser = Parser() + self._parser: Parser = get_parser(language) """The tree-sitter Parser""" - self._parser.set_language(self._language) self._syntax_tree = self._build_ast(self._parser) """The tree-sitter Tree (syntax tree) built from the document.""" @@ -76,6 +76,16 @@ def __init__(self, text: str, language: str | None = None): def insert_range( self, start: tuple[int, int], end: tuple[int, int], text: str ) -> tuple[int, int]: + """Insert text at the given range. + + Args: + start: A tuple (row, column) where the edit starts. + end: A tuple (row, column) where the edit ends. + text: The text to insert between start and end. + + Returns: + The new end location after the edit is complete. + """ top, bottom = _fix_direction(start, end) start_byte = self._location_to_byte_offset(top) old_end_byte = self._location_to_byte_offset(bottom) @@ -133,6 +143,26 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: return deleted_text + def get_line_text(self, line_index: int) -> Text: + """Apply syntax highlights and return the Text of the line. + + Args: + line_index: The index of the line. + + Returns: + The syntax highlighted Text of the line. + """ + null_style = Style.null() + line = Text(self[line_index], end="") + + if self._highlights: + highlights = self._highlights[line_index] + for start, end, highlight_name in highlights: + node_style = HIGHLIGHT_STYLES.get(highlight_name, null_style) + line.stylize(node_style, start, end) + + return line + def _location_to_byte_offset(self, location: tuple[int, int]) -> int: """Given a document coordinate, return the byte offset of that coordinate.""" lines = self._lines @@ -220,15 +250,3 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No return_value = lines[row][column].encode("utf8") return return_value - - def get_line_text(self, line_index: int) -> Text: - null_style = Style.null() - line = Text(self[line_index], end="") - - if self._highlights: - highlights = self._highlights[line_index] - for start, end, highlight_name in highlights: - node_style = HIGHLIGHT_STYLES.get(highlight_name, null_style) - line.stylize(node_style, start, end) - - return line From f5c615d8a77ed2816bff5d6406e249b8edb51f66 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 2 Aug 2023 13:05:58 +0100 Subject: [PATCH 132/366] Adding highlights files --- poetry.lock | 6 +- .../document/_syntax_aware_document.py | 3 + tools/collect_highlights.py | 26 + tree-sitter/highlights/ada.scm | 195 ++++++++ tree-sitter/highlights/agda.scm | 82 +++ tree-sitter/highlights/arduino.scm | 111 +++++ tree-sitter/highlights/astro.scm | 5 + tree-sitter/highlights/awk.scm | 195 ++++++++ tree-sitter/highlights/bash.scm | 145 ++++++ tree-sitter/highlights/bass.scm | 109 ++++ tree-sitter/highlights/beancount.scm | 24 + tree-sitter/highlights/bibtex.scm | 49 ++ tree-sitter/highlights/bicep.scm | 230 +++++++++ tree-sitter/highlights/blueprint.scm | 57 +++ tree-sitter/highlights/c.scm | 226 +++++++++ tree-sitter/highlights/c_sharp.scm | 412 +++++++++++++++ tree-sitter/highlights/cairo.scm | 338 +++++++++++++ tree-sitter/highlights/capnp.scm | 154 ++++++ tree-sitter/highlights/chatito.scm | 54 ++ tree-sitter/highlights/clojure.scm | 376 ++++++++++++++ tree-sitter/highlights/cmake.scm | 217 ++++++++ tree-sitter/highlights/comment.scm | 43 ++ tree-sitter/highlights/commonlisp.scm | 145 ++++++ tree-sitter/highlights/cooklang.scm | 22 + tree-sitter/highlights/corn.scm | 22 + tree-sitter/highlights/cpon.scm | 50 ++ tree-sitter/highlights/cpp.scm | 240 +++++++++ tree-sitter/highlights/css.scm | 91 ++++ tree-sitter/highlights/cuda.scm | 13 + tree-sitter/highlights/cue.scm | 164 ++++++ tree-sitter/highlights/d.scm | 288 +++++++++++ tree-sitter/highlights/dart.scm | 272 ++++++++++ tree-sitter/highlights/devicetree.scm | 35 ++ tree-sitter/highlights/dhall.scm | 171 +++++++ tree-sitter/highlights/diff.scm | 6 + tree-sitter/highlights/dockerfile.scm | 57 +++ tree-sitter/highlights/dot.scm | 55 +++ tree-sitter/highlights/ebnf.scm | 43 ++ tree-sitter/highlights/ecma.scm | 357 +++++++++++++ tree-sitter/highlights/eex.scm | 15 + tree-sitter/highlights/elixir.scm | 226 +++++++++ tree-sitter/highlights/elm.scm | 205 ++++++++ tree-sitter/highlights/elsa.scm | 41 ++ tree-sitter/highlights/elvish.scm | 70 +++ tree-sitter/highlights/embedded_template.scm | 12 + tree-sitter/highlights/erlang.scm | 128 +++++ tree-sitter/highlights/fennel.scm | 121 +++++ tree-sitter/highlights/firrtl.scm | 189 +++++++ tree-sitter/highlights/fish.scm | 167 +++++++ tree-sitter/highlights/foam.scm | 61 +++ tree-sitter/highlights/fortran.scm | 332 +++++++++++++ tree-sitter/highlights/fsh.scm | 91 ++++ tree-sitter/highlights/func.scm | 175 +++++++ tree-sitter/highlights/fusion.scm | 120 +++++ tree-sitter/highlights/gdscript.scm | 343 +++++++++++++ tree-sitter/highlights/git_config.scm | 49 ++ tree-sitter/highlights/git_rebase.scm | 7 + tree-sitter/highlights/gitattributes.scm | 53 ++ tree-sitter/highlights/gitcommit.scm | 33 ++ tree-sitter/highlights/gitignore.scm | 31 ++ tree-sitter/highlights/gleam.scm | 172 +++++++ tree-sitter/highlights/glimmer.scm | 88 ++++ tree-sitter/highlights/glsl.scm | 37 ++ tree-sitter/highlights/go.scm | 253 ++++++++++ tree-sitter/highlights/godot_resource.scm | 28 ++ tree-sitter/highlights/gomod.scm | 18 + tree-sitter/highlights/gosum.scm | 31 ++ tree-sitter/highlights/gowork.scm | 14 + tree-sitter/highlights/graphql.scm | 165 +++++++ tree-sitter/highlights/groovy.scm | 95 ++++ tree-sitter/highlights/hack.scm | 317 ++++++++++++ tree-sitter/highlights/hare.scm | 255 ++++++++++ tree-sitter/highlights/haskell.scm | 160 ++++++ tree-sitter/highlights/haskell_persistent.scm | 38 ++ tree-sitter/highlights/hcl.scm | 99 ++++ tree-sitter/highlights/heex.scm | 56 +++ tree-sitter/highlights/hjson.scm | 16 + tree-sitter/highlights/hlsl.scm | 35 ++ tree-sitter/highlights/hocon.scm | 36 ++ tree-sitter/highlights/hoon.scm | 33 ++ tree-sitter/highlights/html.scm | 5 + tree-sitter/highlights/html_tags.scm | 60 +++ tree-sitter/highlights/htmldjango.scm | 35 ++ tree-sitter/highlights/http.scm | 60 +++ tree-sitter/highlights/hurl.scm | 132 +++++ tree-sitter/highlights/ini.scm | 16 + tree-sitter/highlights/ispc.scm | 225 +++++++++ tree-sitter/highlights/janet_simple.scm | 299 +++++++++++ tree-sitter/highlights/java.scm | 296 +++++++++++ tree-sitter/highlights/javascript.scm | 55 +++ tree-sitter/highlights/jq.scm | 330 +++++++++++++ tree-sitter/highlights/jsdoc.scm | 2 + tree-sitter/highlights/json.scm | 32 ++ tree-sitter/highlights/json5.scm | 17 + tree-sitter/highlights/jsonc.scm | 3 + tree-sitter/highlights/jsonnet.scm | 96 ++++ tree-sitter/highlights/jsx.scm | 35 ++ tree-sitter/highlights/julia.scm | 299 +++++++++++ tree-sitter/highlights/kdl.scm | 58 +++ tree-sitter/highlights/kotlin.scm | 422 ++++++++++++++++ tree-sitter/highlights/lalrpop.scm | 60 +++ tree-sitter/highlights/latex.scm | 249 ++++++++++ tree-sitter/highlights/ledger.scm | 54 ++ tree-sitter/highlights/llvm.scm | 166 +++++++ tree-sitter/highlights/lua.scm | 247 +++++++++ tree-sitter/highlights/luadoc.scm | 152 ++++++ tree-sitter/highlights/luap.scm | 35 ++ tree-sitter/highlights/luau.scm | 252 ++++++++++ tree-sitter/highlights/m68k.scm | 71 +++ tree-sitter/highlights/make.scm | 131 +++++ tree-sitter/highlights/markdown.scm | 63 +++ tree-sitter/highlights/markdown_inline.scm | 94 ++++ tree-sitter/highlights/matlab.scm | 154 ++++++ tree-sitter/highlights/menhir.scm | 29 ++ tree-sitter/highlights/mermaid.scm | 177 +++++++ tree-sitter/highlights/meson.scm | 76 +++ tree-sitter/highlights/mlir.scm | 335 +++++++++++++ tree-sitter/highlights/nickel.scm | 61 +++ tree-sitter/highlights/ninja.scm | 98 ++++ tree-sitter/highlights/nix.scm | 133 +++++ tree-sitter/highlights/objc.scm | 216 ++++++++ tree-sitter/highlights/ocaml.scm | 177 +++++++ tree-sitter/highlights/ocaml_interface.scm | 1 + tree-sitter/highlights/ocamllex.scm | 41 ++ tree-sitter/highlights/odin.scm | 293 +++++++++++ tree-sitter/highlights/pascal.scm | 424 ++++++++++++++++ tree-sitter/highlights/passwd.scm | 16 + tree-sitter/highlights/pem.scm | 11 + tree-sitter/highlights/perl.scm | 187 +++++++ tree-sitter/highlights/php.scm | 314 ++++++++++++ tree-sitter/highlights/phpdoc.scm | 45 ++ tree-sitter/highlights/pioasm.scm | 34 ++ tree-sitter/highlights/po.scm | 33 ++ tree-sitter/highlights/poe_filter.scm | 38 ++ tree-sitter/highlights/pony.scm | 292 +++++++++++ tree-sitter/highlights/prisma.scm | 39 ++ tree-sitter/highlights/promql.scm | 41 ++ tree-sitter/highlights/proto.scm | 69 +++ tree-sitter/highlights/prql.scm | 149 ++++++ tree-sitter/highlights/pug.scm | 80 +++ tree-sitter/highlights/puppet.scm | 195 ++++++++ tree-sitter/highlights/python.scm | 74 ++- tree-sitter/highlights/ql.scm | 135 +++++ tree-sitter/highlights/qmldir.scm | 24 + tree-sitter/highlights/qmljs.scm | 153 ++++++ tree-sitter/highlights/query.scm | 34 ++ tree-sitter/highlights/r.scm | 144 ++++++ tree-sitter/highlights/racket.scm | 141 ++++++ tree-sitter/highlights/rasi.scm | 82 +++ tree-sitter/highlights/regex.scm | 34 ++ tree-sitter/highlights/rego.scm | 64 +++ tree-sitter/highlights/rnoweb.scm | 2 + tree-sitter/highlights/robot.scm | 21 + tree-sitter/highlights/ron.scm | 53 ++ tree-sitter/highlights/rst.scm | 171 +++++++ tree-sitter/highlights/ruby.scm | 261 ++++++++++ tree-sitter/highlights/rust.scm | 384 ++++++++++++++ tree-sitter/highlights/scala.scm | 264 ++++++++++ tree-sitter/highlights/scfg.scm | 8 + tree-sitter/highlights/scheme.scm | 181 +++++++ tree-sitter/highlights/scss.scm | 65 +++ tree-sitter/highlights/slint.scm | 154 ++++++ tree-sitter/highlights/smali.scm | 218 ++++++++ tree-sitter/highlights/smithy.scm | 113 +++++ tree-sitter/highlights/solidity.scm | 263 ++++++++++ tree-sitter/highlights/sparql.scm | 211 ++++++++ tree-sitter/highlights/sql.scm | 355 +++++++++++++ tree-sitter/highlights/squirrel.scm | 307 ++++++++++++ tree-sitter/highlights/starlark.scm | 300 +++++++++++ tree-sitter/highlights/supercollider.scm | 97 ++++ tree-sitter/highlights/surface.scm | 44 ++ tree-sitter/highlights/svelte.scm | 30 ++ tree-sitter/highlights/swift.scm | 181 +++++++ tree-sitter/highlights/sxhkdrc.scm | 10 + tree-sitter/highlights/systemtap.scm | 153 ++++++ tree-sitter/highlights/t32.scm | 232 +++++++++ tree-sitter/highlights/tablegen.scm | 156 ++++++ tree-sitter/highlights/teal.scm | 135 +++++ tree-sitter/highlights/terraform.scm | 21 + tree-sitter/highlights/thrift.scm | 230 +++++++++ tree-sitter/highlights/tiger.scm | 121 +++++ tree-sitter/highlights/tlaplus.scm | 270 ++++++++++ tree-sitter/highlights/todotxt.scm | 6 + tree-sitter/highlights/toml.scm | 36 ++ tree-sitter/highlights/tsx.scm | 1 + tree-sitter/highlights/turtle.scm | 58 +++ tree-sitter/highlights/twig.scm | 55 +++ tree-sitter/highlights/typescript.scm | 157 ++++++ tree-sitter/highlights/ungrammar.scm | 30 ++ tree-sitter/highlights/usd.scm | 181 +++++++ tree-sitter/highlights/uxntal.scm | 84 ++++ tree-sitter/highlights/v.scm | 467 ++++++++++++++++++ tree-sitter/highlights/vala.scm | 250 ++++++++++ tree-sitter/highlights/verilog.scm | 315 ++++++++++++ tree-sitter/highlights/vhs.scm | 38 ++ tree-sitter/highlights/vim.scm | 290 +++++++++++ tree-sitter/highlights/vimdoc.scm | 25 + tree-sitter/highlights/vue.scm | 23 + tree-sitter/highlights/wgsl.scm | 106 ++++ tree-sitter/highlights/wgsl_bevy.scm | 36 ++ tree-sitter/highlights/wing.scm | 86 ++++ tree-sitter/highlights/yaml.scm | 53 ++ tree-sitter/highlights/yang.scm | 43 ++ tree-sitter/highlights/yuck.scm | 150 ++++++ tree-sitter/highlights/zig.scm | 232 +++++++++ 205 files changed, 25881 insertions(+), 49 deletions(-) create mode 100644 tools/collect_highlights.py create mode 100644 tree-sitter/highlights/ada.scm create mode 100644 tree-sitter/highlights/agda.scm create mode 100644 tree-sitter/highlights/arduino.scm create mode 100644 tree-sitter/highlights/astro.scm create mode 100644 tree-sitter/highlights/awk.scm create mode 100644 tree-sitter/highlights/bash.scm create mode 100644 tree-sitter/highlights/bass.scm create mode 100644 tree-sitter/highlights/beancount.scm create mode 100644 tree-sitter/highlights/bibtex.scm create mode 100644 tree-sitter/highlights/bicep.scm create mode 100644 tree-sitter/highlights/blueprint.scm create mode 100644 tree-sitter/highlights/c.scm create mode 100644 tree-sitter/highlights/c_sharp.scm create mode 100644 tree-sitter/highlights/cairo.scm create mode 100644 tree-sitter/highlights/capnp.scm create mode 100644 tree-sitter/highlights/chatito.scm create mode 100644 tree-sitter/highlights/clojure.scm create mode 100644 tree-sitter/highlights/cmake.scm create mode 100644 tree-sitter/highlights/comment.scm create mode 100644 tree-sitter/highlights/commonlisp.scm create mode 100644 tree-sitter/highlights/cooklang.scm create mode 100644 tree-sitter/highlights/corn.scm create mode 100644 tree-sitter/highlights/cpon.scm create mode 100644 tree-sitter/highlights/cpp.scm create mode 100644 tree-sitter/highlights/css.scm create mode 100644 tree-sitter/highlights/cuda.scm create mode 100644 tree-sitter/highlights/cue.scm create mode 100644 tree-sitter/highlights/d.scm create mode 100644 tree-sitter/highlights/dart.scm create mode 100644 tree-sitter/highlights/devicetree.scm create mode 100644 tree-sitter/highlights/dhall.scm create mode 100644 tree-sitter/highlights/diff.scm create mode 100644 tree-sitter/highlights/dockerfile.scm create mode 100644 tree-sitter/highlights/dot.scm create mode 100644 tree-sitter/highlights/ebnf.scm create mode 100644 tree-sitter/highlights/ecma.scm create mode 100644 tree-sitter/highlights/eex.scm create mode 100644 tree-sitter/highlights/elixir.scm create mode 100644 tree-sitter/highlights/elm.scm create mode 100644 tree-sitter/highlights/elsa.scm create mode 100644 tree-sitter/highlights/elvish.scm create mode 100644 tree-sitter/highlights/embedded_template.scm create mode 100644 tree-sitter/highlights/erlang.scm create mode 100644 tree-sitter/highlights/fennel.scm create mode 100644 tree-sitter/highlights/firrtl.scm create mode 100644 tree-sitter/highlights/fish.scm create mode 100644 tree-sitter/highlights/foam.scm create mode 100644 tree-sitter/highlights/fortran.scm create mode 100644 tree-sitter/highlights/fsh.scm create mode 100644 tree-sitter/highlights/func.scm create mode 100644 tree-sitter/highlights/fusion.scm create mode 100644 tree-sitter/highlights/gdscript.scm create mode 100644 tree-sitter/highlights/git_config.scm create mode 100644 tree-sitter/highlights/git_rebase.scm create mode 100644 tree-sitter/highlights/gitattributes.scm create mode 100644 tree-sitter/highlights/gitcommit.scm create mode 100644 tree-sitter/highlights/gitignore.scm create mode 100644 tree-sitter/highlights/gleam.scm create mode 100644 tree-sitter/highlights/glimmer.scm create mode 100644 tree-sitter/highlights/glsl.scm create mode 100644 tree-sitter/highlights/go.scm create mode 100644 tree-sitter/highlights/godot_resource.scm create mode 100644 tree-sitter/highlights/gomod.scm create mode 100644 tree-sitter/highlights/gosum.scm create mode 100644 tree-sitter/highlights/gowork.scm create mode 100644 tree-sitter/highlights/graphql.scm create mode 100644 tree-sitter/highlights/groovy.scm create mode 100644 tree-sitter/highlights/hack.scm create mode 100644 tree-sitter/highlights/hare.scm create mode 100644 tree-sitter/highlights/haskell.scm create mode 100644 tree-sitter/highlights/haskell_persistent.scm create mode 100644 tree-sitter/highlights/hcl.scm create mode 100644 tree-sitter/highlights/heex.scm create mode 100644 tree-sitter/highlights/hjson.scm create mode 100644 tree-sitter/highlights/hlsl.scm create mode 100644 tree-sitter/highlights/hocon.scm create mode 100644 tree-sitter/highlights/hoon.scm create mode 100644 tree-sitter/highlights/html.scm create mode 100644 tree-sitter/highlights/html_tags.scm create mode 100644 tree-sitter/highlights/htmldjango.scm create mode 100644 tree-sitter/highlights/http.scm create mode 100644 tree-sitter/highlights/hurl.scm create mode 100644 tree-sitter/highlights/ini.scm create mode 100644 tree-sitter/highlights/ispc.scm create mode 100644 tree-sitter/highlights/janet_simple.scm create mode 100644 tree-sitter/highlights/java.scm create mode 100644 tree-sitter/highlights/javascript.scm create mode 100644 tree-sitter/highlights/jq.scm create mode 100644 tree-sitter/highlights/jsdoc.scm create mode 100644 tree-sitter/highlights/json.scm create mode 100644 tree-sitter/highlights/json5.scm create mode 100644 tree-sitter/highlights/jsonc.scm create mode 100644 tree-sitter/highlights/jsonnet.scm create mode 100644 tree-sitter/highlights/jsx.scm create mode 100644 tree-sitter/highlights/julia.scm create mode 100644 tree-sitter/highlights/kdl.scm create mode 100644 tree-sitter/highlights/kotlin.scm create mode 100644 tree-sitter/highlights/lalrpop.scm create mode 100644 tree-sitter/highlights/latex.scm create mode 100644 tree-sitter/highlights/ledger.scm create mode 100644 tree-sitter/highlights/llvm.scm create mode 100644 tree-sitter/highlights/lua.scm create mode 100644 tree-sitter/highlights/luadoc.scm create mode 100644 tree-sitter/highlights/luap.scm create mode 100644 tree-sitter/highlights/luau.scm create mode 100644 tree-sitter/highlights/m68k.scm create mode 100644 tree-sitter/highlights/make.scm create mode 100644 tree-sitter/highlights/markdown.scm create mode 100644 tree-sitter/highlights/markdown_inline.scm create mode 100644 tree-sitter/highlights/matlab.scm create mode 100644 tree-sitter/highlights/menhir.scm create mode 100644 tree-sitter/highlights/mermaid.scm create mode 100644 tree-sitter/highlights/meson.scm create mode 100644 tree-sitter/highlights/mlir.scm create mode 100644 tree-sitter/highlights/nickel.scm create mode 100644 tree-sitter/highlights/ninja.scm create mode 100644 tree-sitter/highlights/nix.scm create mode 100644 tree-sitter/highlights/objc.scm create mode 100644 tree-sitter/highlights/ocaml.scm create mode 100644 tree-sitter/highlights/ocaml_interface.scm create mode 100644 tree-sitter/highlights/ocamllex.scm create mode 100644 tree-sitter/highlights/odin.scm create mode 100644 tree-sitter/highlights/pascal.scm create mode 100644 tree-sitter/highlights/passwd.scm create mode 100644 tree-sitter/highlights/pem.scm create mode 100644 tree-sitter/highlights/perl.scm create mode 100644 tree-sitter/highlights/php.scm create mode 100644 tree-sitter/highlights/phpdoc.scm create mode 100644 tree-sitter/highlights/pioasm.scm create mode 100644 tree-sitter/highlights/po.scm create mode 100644 tree-sitter/highlights/poe_filter.scm create mode 100644 tree-sitter/highlights/pony.scm create mode 100644 tree-sitter/highlights/prisma.scm create mode 100644 tree-sitter/highlights/promql.scm create mode 100644 tree-sitter/highlights/proto.scm create mode 100644 tree-sitter/highlights/prql.scm create mode 100644 tree-sitter/highlights/pug.scm create mode 100644 tree-sitter/highlights/puppet.scm create mode 100644 tree-sitter/highlights/ql.scm create mode 100644 tree-sitter/highlights/qmldir.scm create mode 100644 tree-sitter/highlights/qmljs.scm create mode 100644 tree-sitter/highlights/query.scm create mode 100644 tree-sitter/highlights/r.scm create mode 100644 tree-sitter/highlights/racket.scm create mode 100644 tree-sitter/highlights/rasi.scm create mode 100644 tree-sitter/highlights/regex.scm create mode 100644 tree-sitter/highlights/rego.scm create mode 100644 tree-sitter/highlights/rnoweb.scm create mode 100644 tree-sitter/highlights/robot.scm create mode 100644 tree-sitter/highlights/ron.scm create mode 100644 tree-sitter/highlights/rst.scm create mode 100644 tree-sitter/highlights/ruby.scm create mode 100644 tree-sitter/highlights/rust.scm create mode 100644 tree-sitter/highlights/scala.scm create mode 100644 tree-sitter/highlights/scfg.scm create mode 100644 tree-sitter/highlights/scheme.scm create mode 100644 tree-sitter/highlights/scss.scm create mode 100644 tree-sitter/highlights/slint.scm create mode 100644 tree-sitter/highlights/smali.scm create mode 100644 tree-sitter/highlights/smithy.scm create mode 100644 tree-sitter/highlights/solidity.scm create mode 100644 tree-sitter/highlights/sparql.scm create mode 100644 tree-sitter/highlights/sql.scm create mode 100644 tree-sitter/highlights/squirrel.scm create mode 100644 tree-sitter/highlights/starlark.scm create mode 100644 tree-sitter/highlights/supercollider.scm create mode 100644 tree-sitter/highlights/surface.scm create mode 100644 tree-sitter/highlights/svelte.scm create mode 100644 tree-sitter/highlights/swift.scm create mode 100644 tree-sitter/highlights/sxhkdrc.scm create mode 100644 tree-sitter/highlights/systemtap.scm create mode 100644 tree-sitter/highlights/t32.scm create mode 100644 tree-sitter/highlights/tablegen.scm create mode 100644 tree-sitter/highlights/teal.scm create mode 100644 tree-sitter/highlights/terraform.scm create mode 100644 tree-sitter/highlights/thrift.scm create mode 100644 tree-sitter/highlights/tiger.scm create mode 100644 tree-sitter/highlights/tlaplus.scm create mode 100644 tree-sitter/highlights/todotxt.scm create mode 100644 tree-sitter/highlights/toml.scm create mode 100644 tree-sitter/highlights/tsx.scm create mode 100644 tree-sitter/highlights/turtle.scm create mode 100644 tree-sitter/highlights/twig.scm create mode 100644 tree-sitter/highlights/typescript.scm create mode 100644 tree-sitter/highlights/ungrammar.scm create mode 100644 tree-sitter/highlights/usd.scm create mode 100644 tree-sitter/highlights/uxntal.scm create mode 100644 tree-sitter/highlights/v.scm create mode 100644 tree-sitter/highlights/vala.scm create mode 100644 tree-sitter/highlights/verilog.scm create mode 100644 tree-sitter/highlights/vhs.scm create mode 100644 tree-sitter/highlights/vim.scm create mode 100644 tree-sitter/highlights/vimdoc.scm create mode 100644 tree-sitter/highlights/vue.scm create mode 100644 tree-sitter/highlights/wgsl.scm create mode 100644 tree-sitter/highlights/wgsl_bevy.scm create mode 100644 tree-sitter/highlights/wing.scm create mode 100644 tree-sitter/highlights/yaml.scm create mode 100644 tree-sitter/highlights/yang.scm create mode 100644 tree-sitter/highlights/yuck.scm create mode 100644 tree-sitter/highlights/zig.scm diff --git a/poetry.lock b/poetry.lock index 7efbfcf548..fe796b0cc8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -907,7 +907,7 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "13.5.1" +version = "13.5.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false @@ -2067,8 +2067,8 @@ rfc3986 = [ {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] rich = [ - {file = "rich-13.5.1-py3-none-any.whl", hash = "sha256:b97381b204a206e1be618f5e1215a57174a1a7732490b3bf6668cf41d30bc72d"}, - {file = "rich-13.5.1.tar.gz", hash = "sha256:881653ee7037803559d8eae98f145e0a4c4b0ec3ff0300d2cc8d479c71fc6819"}, + {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, + {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, ] setuptools = [ {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index dd5a08fc53..99b307a463 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -181,6 +181,9 @@ def _prepare_highlights( end_point: tuple[int, int] = None, ) -> None: highlights = self._highlights + print(self._language.language_id) + print(self._language.name) + print(self._language.lib) query: Query = self._language.query(self._highlights_query) captures_kwargs = {} diff --git a/tools/collect_highlights.py b/tools/collect_highlights.py new file mode 100644 index 0000000000..f93c24a9f0 --- /dev/null +++ b/tools/collect_highlights.py @@ -0,0 +1,26 @@ +import os +import shutil +from pathlib import Path + +# The directory that contains the language folders +source_dir = Path(__file__).parent / "../../nvim-treesitter/queries" +# The directory to store the collected highlights files +target_dir = Path(__file__).parent / "highlights" + +# Ensure the target directory exists +os.makedirs(target_dir, exist_ok=True) + +# Walk through the source directory +for root, dirs, files in os.walk(source_dir): + # If a highlights.scm file exists in the current directory + if "highlights.scm" in files: + # Get the name of the current language directory + language = os.path.basename(root) + # Create the full path to the source and target files + source_file = os.path.join(root, "highlights.scm") + target_file = os.path.join(target_dir, f"{language}.scm") + # Copy the file + shutil.copyfile(source_file, target_file) + +# Print a success message +print("Done!") diff --git a/tree-sitter/highlights/ada.scm b/tree-sitter/highlights/ada.scm new file mode 100644 index 0000000000..0d95f8d572 --- /dev/null +++ b/tree-sitter/highlights/ada.scm @@ -0,0 +1,195 @@ +;; highlight queries. +;; See the syntax at https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries +;; See also https://github.com/nvim-treesitter/nvim-treesitter/blob/master/CONTRIBUTING.md#parser-configurations +;; for a list of recommended @ tags, though not all of them have matching +;; highlights in neovim. + +[ + "abort" + "abs" + "abstract" + "accept" + "access" + "all" + "array" + "at" + "begin" + "declare" + "delay" + "delta" + "digits" + "do" + "end" + "entry" + "exit" + "generic" + "interface" + "is" + "limited" + "null" + "of" + "others" + "out" + "pragma" + "private" + "range" + "synchronized" + "tagged" + "task" + "terminate" + "until" + "when" +] @keyword +[ + "aliased" + "constant" + "renames" +] @storageclass +[ + "mod" + "new" + "protected" + "record" + "subtype" + "type" +] @keyword.type +[ + "with" + "use" +] @include +[ + "body" + "function" + "overriding" + "procedure" + "package" + "separate" +] @keyword.function +[ + "and" + "in" + "not" + "or" + "xor" +] @keyword.operator +[ + "while" + "loop" + "for" + "parallel" + "reverse" + "some" +] @repeat +[ + "return" +] @keyword.return +[ + "case" + "if" + "else" + "then" + "elsif" + "select" +] @conditional +[ + "exception" + "raise" +] @exception +(comment) @comment @spell +(string_literal) @string @spell +(character_literal) @string +(numeric_literal) @number + +;; Highlight the name of subprograms +(procedure_specification name: (_) @function) +(function_specification name: (_) @function) +(package_declaration name: (_) @function) +(package_body name: (_) @function) +(generic_instantiation name: (_) @function) +(entry_declaration . (identifier) @function) + +;; Some keywords should take different categories depending on the context +(use_clause "use" @include "type" @include) +(with_clause "private" @include) +(with_clause "limited" @include) +(use_clause (_) @namespace) +(with_clause (_) @namespace) + +(loop_statement "end" @keyword.repeat) +(if_statement "end" @conditional) +(loop_parameter_specification "in" @keyword.repeat) +(loop_parameter_specification "in" @keyword.repeat) +(iterator_specification ["in" "of"] @keyword.repeat) +(range_attribute_designator "range" @keyword.repeat) + +(raise_statement "with" @exception) + +(gnatprep_declarative_if_statement) @preproc +(gnatprep_if_statement) @preproc +(gnatprep_identifier) @preproc + +(subprogram_declaration "is" @keyword.function "abstract" @keyword.function) +(aspect_specification "with" @keyword.function) + +(full_type_declaration "is" @keyword.type) +(subtype_declaration "is" @keyword.type) +(record_definition "end" @keyword.type) +(full_type_declaration (_ "access" @keyword.type)) +(array_type_definition "array" @keyword.type "of" @keyword.type) +(access_to_object_definition "access" @keyword.type) +(access_to_object_definition "access" @keyword.type + [ + (general_access_modifier "constant" @keyword.type) + (general_access_modifier "all" @keyword.type) + ] +) +(range_constraint "range" @keyword.type) +(signed_integer_type_definition "range" @keyword.type) +(index_subtype_definition "range" @keyword.type) +(record_type_definition "abstract" @keyword.type) +(record_type_definition "tagged" @keyword.type) +(record_type_definition "limited" @keyword.type) +(record_type_definition (record_definition "null" @keyword.type)) +(private_type_declaration "is" @keyword.type "private" @keyword.type) +(private_type_declaration "tagged" @keyword.type) +(private_type_declaration "limited" @keyword.type) +(task_type_declaration "task" @keyword.type "is" @keyword.type) + +;; Gray the body of expression functions +(expression_function_declaration + (function_specification) + "is" + (_) @attribute +) +(subprogram_declaration (aspect_specification) @attribute) + +;; Highlight full subprogram specifications +;(subprogram_body +; [ +; (procedure_specification) +; (function_specification) +; ] @function.spec +;) + +((comment) @comment.documentation + . [ + (entry_declaration) + (subprogram_declaration) + (parameter_specification) + ]) + +(compilation_unit + . (comment) @comment.documentation) + +(component_list + (component_declaration) + . (comment) @comment.documentation) + +(enumeration_type_definition + (identifier) + . (comment) @comment.documentation) + +;; Highlight errors in red. This is not very useful in practice, as text will +;; be highlighted as user types, and the error could be elsewhere in the code. +;; This also requires defining :hi @error guifg=Red for instance. +(ERROR) @error diff --git a/tree-sitter/highlights/agda.scm b/tree-sitter/highlights/agda.scm new file mode 100644 index 0000000000..a4eb3b9435 --- /dev/null +++ b/tree-sitter/highlights/agda.scm @@ -0,0 +1,82 @@ + +;; Constants +(integer) @number + +;; Variables and Symbols + +(typed_binding (atom (qid) @variable)) +(untyped_binding) @variable +(typed_binding (expr) @type) + +(id) @function +(bid) @function + +(function_name (atom (qid) @function)) +(field_name) @function + + +[(data_name) (record_name)] @constructor + +; Set +(SetN) @type.builtin + +(expr . (atom) @function) + +((atom) @boolean + (#any-of? @boolean "true" "false" "True" "False")) + +;; Imports and Module Declarations + +"import" @include + +(module_name) @namespace + +;; Pragmas and comments + +(pragma) @preproc + +(comment) @comment + +;; Keywords +[ + "where" + "data" + "rewrite" + "postulate" + "public" + "private" + "tactic" + "Prop" + "quote" + "renaming" + "open" + "in" + "hiding" + "constructor" + "abstract" + "let" + "field" + "mutual" + "module" + "infix" + "infixl" + "infixr" + "record" + (ARROW) +] +@keyword + +;;;(expr +;;; f_name: (atom) @function) +;; Brackets + +[ + "(" + ")" + "{" + "}"] +@punctuation.bracket + +[ + "=" +] @operator diff --git a/tree-sitter/highlights/arduino.scm b/tree-sitter/highlights/arduino.scm new file mode 100644 index 0000000000..a4e74ae893 --- /dev/null +++ b/tree-sitter/highlights/arduino.scm @@ -0,0 +1,111 @@ +; inherits: cpp + +((identifier) @function.builtin + (#any-of? @function.builtin + ; Digital I/O + "digitalRead" + "digitalWrite" + "pinMode" + ; Analog I/O + "analogRead" + "analogReference" + "analogWrite" + ; Zero, Due & MKR Family + "analogReadResolution" + "analogWriteResolution" + ; Advanced I/O + "noTone" + "pulseIn" + "pulseInLong" + "shiftIn" + "shiftOut" + "tone" + ; Time + "delay" + "delayMicroseconds" + "micros" + "millis" + ; Math + "abs" + "constrain" + "map" + "max" + "min" + "pow" + "sq" + "sqrt" + ; Trigonometry + "cos" + "sin" + "tan" + ; Characters + "isAlpha" + "isAlphaNumeric" + "isAscii" + "isControl" + "isDigit" + "isGraph" + "isHexadecimalDigit" + "isLowerCase" + "isPrintable" + "isPunct" + "isSpace" + "isUpperCase" + "isWhitespace" + ; Random Numbers + "random" + "randomSeed" + ; Bits and Bytes + "bit" + "bitClear" + "bitRead" + "bitSet" + "bitWrite" + "highByte" + "lowByte" + ; External Interrupts + "attachInterrupt" + "detachInterrupt" + ; Interrupts + "interrupts" + "noInterrupts" + )) + +((identifier) @type.builtin + (#any-of? @type.builtin + "Serial" + "SPI" + "Stream" + "Wire" + "Keyboard" + "Mouse" + "String" + )) + +((identifier) @constant.builtin + (#any-of? @constant.builtin + "HIGH" + "LOW" + "INPUT" + "OUTPUT" + "INPUT_PULLUP" + "LED_BUILTIN" + )) + +(function_definition + (function_declarator + declarator: (identifier) @function.builtin) + (#any-of? @function.builtin "loop" "setup")) + +(call_expression + function: (primitive_type) @function.builtin) + +(call_expression + function: (identifier) @constructor + (#any-of? @constructor "SPISettings" "String")) + +(declaration + (type_identifier) @type.builtin + (function_declarator + declarator: (identifier) @constructor) + (#eq? @type.builtin "SPISettings")) diff --git a/tree-sitter/highlights/astro.scm b/tree-sitter/highlights/astro.scm new file mode 100644 index 0000000000..62e8ed247b --- /dev/null +++ b/tree-sitter/highlights/astro.scm @@ -0,0 +1,5 @@ +; inherits: html + +[ "---" ] @punctuation.delimiter + +[ "{" "}" ] @punctuation.special diff --git a/tree-sitter/highlights/awk.scm b/tree-sitter/highlights/awk.scm new file mode 100644 index 0000000000..4faf496e62 --- /dev/null +++ b/tree-sitter/highlights/awk.scm @@ -0,0 +1,195 @@ +; adapted from https://github.com/Beaglefoot/tree-sitter-awk + +[ + (identifier) + (field_ref) +] @variable +(field_ref (_) @variable) + +; https://www.gnu.org/software/gawk/manual/html_node/Auto_002dset.html +((identifier) @constant.builtin + (#any-of? @constant.builtin + "ARGC" + "ARGV" + "ARGIND" + "ENVIRON" + "ERRNO" + "FILENAME" + "FNR" + "NF" + "FUNCTAB" + "NR" + "PROCINFO" + "RLENGTH" + "RSTART" + "RT" + "SYMTAB")) + +; https://www.gnu.org/software/gawk/manual/html_node/User_002dmodified.html +((identifier) @variable.builtin + (#any-of? @variable.builtin + "BINMODE" + "CONVFMT" + "FIELDWIDTHS" + "FPAT" + "FS" + "IGNORECASE" + "LINT" + "OFMT" + "OFS" + "ORS" + "PREC" + "ROUNDMODE" + "RS" + "SUBSEP" + "TEXTDOMAIN")) + +(number) @number + +(string) @string +(regex) @string.regex +(escape_sequence) @string.escape + +(comment) @comment @spell + +((program . (comment) @preproc) + (#lua-match? @preproc "^#!/")) + +(ns_qualified_name (namespace) @namespace) +(ns_qualified_name "::" @punctuation.delimiter) + +(func_def name: (_ (identifier) @function) @function) +(func_call name: (_ (identifier) @function) @function) + +(func_def (param_list (identifier) @parameter)) + +[ + "print" + "printf" + "getline" +] @function.builtin + +[ + (delete_statement) + (break_statement) + (continue_statement) + (next_statement) + (nextfile_statement) +] @keyword + +[ + "func" + "function" +] @keyword.function + +[ + "return" + "exit" +] @keyword.return + +[ + "do" + "while" + "for" + "in" +] @repeat + +[ + "if" + "else" + "switch" + "case" + "default" +] @conditional + +[ + "@include" + "@load" +] @include + +"@namespace" @preproc + +[ + "BEGIN" + "END" + "BEGINFILE" + "ENDFILE" +] @label + +(binary_exp [ + "^" + "**" + "*" + "/" + "%" + "+" + "-" + "<" + ">" + "<=" + ">=" + "==" + "!=" + "~" + "!~" + "in" + "&&" + "||" +] @operator) + +(unary_exp [ + "!" + "+" + "-" +] @operator) + +(assignment_exp [ + "=" + "+=" + "-=" + "*=" + "/=" + "%=" + "^=" +] @operator) + +(ternary_exp [ + "?" + ":" +] @conditional.ternary) + +(update_exp [ + "++" + "--" +] @operator) + +(redirected_io_statement [ + ">" + ">>" +] @operator) + +(piped_io_statement [ + "|" + "|&" +] @operator) + +(piped_io_exp [ + "|" + "|&" +] @operator) + +(field_ref "$" @punctuation.delimiter) + +(regex "/" @punctuation.delimiter) +(regex_constant "@" @punctuation.delimiter) + +[ ";" "," ] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket diff --git a/tree-sitter/highlights/bash.scm b/tree-sitter/highlights/bash.scm new file mode 100644 index 0000000000..23bf03e697 --- /dev/null +++ b/tree-sitter/highlights/bash.scm @@ -0,0 +1,145 @@ +(simple_expansion) @none +(expansion + "${" @punctuation.special + "}" @punctuation.special) @none +[ + "(" + ")" + "((" + "))" + "{" + "}" + "[" + "]" + "[[" + "]]" + ] @punctuation.bracket + +[ + ";" + ";;" + (heredoc_start) + ] @punctuation.delimiter + +[ + "$" +] @punctuation.special + +[ + ">" + ">>" + "<" + "<<" + "&" + "&&" + "|" + "||" + "=" + "=~" + "==" + "!=" + ] @operator + +[ + (string) + (raw_string) + (ansi_c_string) + (heredoc_body) +] @string @spell + +(variable_assignment (word) @string) + +[ + "if" + "then" + "else" + "elif" + "fi" + "case" + "in" + "esac" + ] @conditional + +[ + "for" + "do" + "done" + "select" + "until" + "while" + ] @repeat + +[ + "declare" + "export" + "local" + "readonly" + "unset" + ] @keyword + +"function" @keyword.function + +(special_variable_name) @constant + +; trap -l +((word) @constant.builtin + (#match? @constant.builtin "^SIG(HUP|INT|QUIT|ILL|TRAP|ABRT|BUS|FPE|KILL|USR[12]|SEGV|PIPE|ALRM|TERM|STKFLT|CHLD|CONT|STOP|TSTP|TT(IN|OU)|URG|XCPU|XFSZ|VTALRM|PROF|WINCH|IO|PWR|SYS|RTMIN([+]([1-9]|1[0-5]))?|RTMAX(-([1-9]|1[0-4]))?)$")) + +((word) @boolean + (#any-of? @boolean "true" "false")) + +(comment) @comment @spell +(test_operator) @string + +(command_substitution + [ "$(" ")" ] @punctuation.bracket) + +(process_substitution + [ "<(" ")" ] @punctuation.bracket) + + +(function_definition + name: (word) @function) + +(command_name (word) @function.call) + +((command_name (word) @function.builtin) + (#any-of? @function.builtin + "alias" "bg" "bind" "break" "builtin" "caller" "cd" + "command" "compgen" "complete" "compopt" "continue" + "coproc" "dirs" "disown" "echo" "enable" "eval" + "exec" "exit" "fc" "fg" "getopts" "hash" "help" + "history" "jobs" "kill" "let" "logout" "mapfile" + "popd" "printf" "pushd" "pwd" "read" "readarray" + "return" "set" "shift" "shopt" "source" "suspend" + "test" "time" "times" "trap" "type" "typeset" + "ulimit" "umask" "unalias" "wait")) + +(command + argument: [ + (word) @parameter + (concatenation (word) @parameter) + ]) + +((word) @number + (#lua-match? @number "^[0-9]+$")) + +(file_redirect + descriptor: (file_descriptor) @operator + destination: (word) @parameter) + +(expansion + [ "${" "}" ] @punctuation.bracket) + +(variable_name) @variable + +((variable_name) @constant + (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) + +(case_item + value: (word) @parameter) + +(regex) @string.regex + +((program . (comment) @preproc) + (#lua-match? @preproc "^#!/")) diff --git a/tree-sitter/highlights/bass.scm b/tree-sitter/highlights/bass.scm new file mode 100644 index 0000000000..ca667b5524 --- /dev/null +++ b/tree-sitter/highlights/bass.scm @@ -0,0 +1,109 @@ +;; Variables + +(list (symbol) @variable) + +(cons (symbol) @variable) + +(scope (symbol) @variable) + +(symbind (symbol) @variable) + +;; Constants + +((symbol) @constant + (#lua-match? @constant "^_*[A-Z][A-Z0-9_]*$")) + +;; Functions + +(list + . (symbol) @function) + +;; Namespaces + +(symbind + (symbol) @namespace + . (keyword)) + +;; Includes + +((symbol) @include + (#any-of? @include "use" "import" "load")) + +;; Keywords + +((symbol) @keyword + (#any-of? @keyword "do" "doc")) + +;; Special Functions + +; Keywords construct a symbol + +(keyword) @constructor + +((list + . (symbol) @keyword.function + . (symbol) @function + (symbol)? @parameter) + (#any-of? @keyword.function "def" "defop" "defn" "fn")) + +((cons + . (symbol) @keyword.function + . (symbol) @function + (symbol)? @parameter) + (#any-of? @keyword.function "def" "defop" "defn" "fn")) + +((symbol) @function.builtin + (#any-of? @function.builtin "dump" "mkfs" "json" "log" "error" "now" "cons" "wrap" "unwrap" "eval" "make-scope" "bind" "meta" "with-meta" "null?" "ignore?" "boolean?" "number?" "string?" "symbol?" "scope?" "sink?" "source?" "list?" "pair?" "applicative?" "operative?" "combiner?" "path?" "empty?" "thunk?" "+" "*" "quot" "-" "max" "min" "=" ">" ">=" "<" "<=" "list->source" "across" "emit" "next" "reduce-kv" "assoc" "symbol->string" "string->symbol" "str" "substring" "trim" "scope->list" "string->fs-path" "string->cmd-path" "string->dir" "subpath" "path-name" "path-stem" "with-image" "with-dir" "with-args" "with-cmd" "with-stdin" "with-env" "with-insecure" "with-label" "with-port" "with-tls" "with-mount" "thunk-cmd" "thunk-args" "resolve" "start" "addr" "wait" "read" "cache-dir" "binds?" "recall-memo" "store-memo" "mask" "list" "list*" "first" "rest" "length" "second" "third" "map" "map-pairs" "foldr" "foldl" "append" "filter" "conj" "list->scope" "merge" "apply" "id" "always" "vals" "keys" "memo" "succeeds?" "run" "last" "take" "take-all" "insecure!" "from" "cd" "wrap-cmd" "mkfile" "path-base" "not")) + +((symbol) @function.macro + (#any-of? @function.macro "op" "current-scope" "quote" "let" "provide" "module" "or" "and" "curryfn" "for" "$" "linux")) + +;; Conditionals + +((symbol) @conditional + (#any-of? @conditional "if" "case" "cond" "when")) + +;; Repeats + +((symbol) @repeat + (#any-of? @repeat "each")) + +;; Operators + +((symbol) @operator (#any-of? @operator "&" "*" "+" "-" "<" "<=" "=" ">" ">=")) + +;; Punctuation + +[ "(" ")" ] @punctuation.bracket + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +((symbol) @punctuation.delimiter + (#eq? @punctuation.delimiter "->")) + +;; Literals + +(string) @string + +(escape_sequence) @string.escape + +(path) @text.uri @string.special + +(number) @number + +(boolean) @boolean + +[ + (ignore) + (null) +] @constant.builtin + +[ + "^" +] @character.special + +;; Comments + +(comment) @comment @spell diff --git a/tree-sitter/highlights/beancount.scm b/tree-sitter/highlights/beancount.scm new file mode 100644 index 0000000000..1f0cf8a484 --- /dev/null +++ b/tree-sitter/highlights/beancount.scm @@ -0,0 +1,24 @@ +(date) @field +(txn) @attribute +(account) @type +(amount) @number +(incomplete_amount) @number +(compound_amount) @number +(amount_tolerance) @number +(currency) @property +(key) @label +(string) @string @spell +(narration) @string @spell +(payee) @string @spell +(tag) @constant +(link) @constant +[ + (minus) (plus) (slash) (asterisk) +] @operator +(comment) @comment @spell +[ + (balance) (open) (close) (commodity) (pad) + (event) (price) (note) (document) (query) + (custom) (pushtag) (poptag) (pushmeta) + (popmeta) (option) (include) (plugin) +] @keyword diff --git a/tree-sitter/highlights/bibtex.scm b/tree-sitter/highlights/bibtex.scm new file mode 100644 index 0000000000..00cae314d6 --- /dev/null +++ b/tree-sitter/highlights/bibtex.scm @@ -0,0 +1,49 @@ +; CREDITS @pfoerster (adapted from https://github.com/latex-lsp/tree-sitter-bibtex) + +[ + (string_type) + (preamble_type) + (entry_type) +] @keyword + +[ + (junk) + (comment) +] @comment + +[ + "=" + "#" +] @operator + +(command) @function.builtin + +(number) @number + +(field + name: (identifier) @field) + +(token + (identifier) @parameter) + +[ + (brace_word) + (quote_word) +] @string + +[ + (key_brace) + (key_paren) +] @symbol + +(string + name: (identifier) @constant) + +[ + "{" + "}" + "(" + ")" +] @punctuation.bracket + +"," @punctuation.delimiter diff --git a/tree-sitter/highlights/bicep.scm b/tree-sitter/highlights/bicep.scm new file mode 100644 index 0000000000..4dade9272b --- /dev/null +++ b/tree-sitter/highlights/bicep.scm @@ -0,0 +1,230 @@ +; Includes + +(import_statement + "import" @include) + +(import_with_statement + "import" @include + "with" @include) + +; Namespaces + +(module_declaration + (identifier) @namespace) + +; Builtins + +(primitive_type) @type.builtin + +((member_expression + object: (identifier) @type.builtin) + (#eq? @type.builtin "sys")) + +; Functions + +(call_expression + function: (identifier) @function.call) + +; Properties + +(object_property + (identifier) @property + ":" @punctuation.delimiter + (_)) + +(object_property + (compatible_identifier) @property + ":" @punctuation.delimiter + (_)) + +(property_identifier) @property + +; Attributes + +(decorator + "@" @attribute) + +(decorator + (call_expression (identifier) @attribute)) + +(decorator + (call_expression + (member_expression + object: (identifier) @attribute + property: (property_identifier) @attribute))) + +; Types + +(type_declaration + (identifier) @type) + +(type_declaration + (identifier) + "=" + (identifier) @type) + +(type_declaration + (identifier) + "=" + (array_type (identifier) @type)) + +(type + (identifier) @type) + +(resource_declaration + (identifier) @type) + +(resource_expression + (identifier) @type) + +; Parameters + +(parameter_declaration + (identifier) @parameter + (_)) + +(call_expression + function: (_) + (arguments (identifier) @parameter)) + +(call_expression + function: (_) + (arguments (member_expression object: (identifier) @parameter))) + +; Variables + +(variable_declaration + (identifier) @variable + (_)) + +(metadata_declaration + (identifier) @variable + (_)) + +(output_declaration + (identifier) @variable + (_)) + +(object_property + (_) + ":" + (identifier) @variable) + +(for_statement + "for" + (for_loop_parameters + (loop_variable) @variable + (loop_enumerator) @variable)) + +; Conditionals + +"if" @conditional + +(ternary_expression + "?" @conditional.ternary + ":" @conditional.ternary) + +; Loops + +(for_statement + "for" @repeat + "in" + ":" @punctuation.delimiter) + +; Keywords + +[ + "module" + "metadata" + "output" + "param" + "resource" + "existing" + "targetScope" + "type" + "var" +] @keyword + +; Operators + +[ + "+" + "-" + "*" + "/" + "%" + "||" + "&&" + "|" + "==" + "!=" + "=~" + "!~" + ">" + ">=" + "<=" + "<" + "??" + "=" + "!" +] @operator + +[ + "in" +] @keyword.operator + + +; Literals + +(string) @string +(import_string + "'" @string + (import_name) @namespace + "@" @symbol + (import_version) @string.special) + +(escape_sequence) @string.escape + +(number) @number + +(boolean) @boolean + +(null) @constant.builtin + +; Misc + +(compatible_identifier + "?" @punctuation.special) + +(nullable_return_type) @punctuation.special + +["{" "}"] @punctuation.bracket + +["[" "]"] @punctuation.bracket + +["(" ")"] @punctuation.bracket + +[ + "." + "::" + "=>" +] @punctuation.delimiter + + +; Interpolation + +(interpolation) @none + +(interpolation + "${" @punctuation.special + "}" @punctuation.special) + +(interpolation + (identifier) @variable) + +; Comments + +[ + (comment) + (diagnostic_comment) +] @comment @spell diff --git a/tree-sitter/highlights/blueprint.scm b/tree-sitter/highlights/blueprint.scm new file mode 100644 index 0000000000..3d4b482660 --- /dev/null +++ b/tree-sitter/highlights/blueprint.scm @@ -0,0 +1,57 @@ +(object_id) @variable + +(string) @string +(escape_sequence) @string.escape + +(comment) @comment + +(constant) @constant.builtin + +(boolean) @boolean + +(using) @include + +(template) @keyword + +(decorator) @attribute + +(property_definition (property_name) @property) + +(object) @type + +(signal_binding (signal_name) @function.builtin) +(signal_binding (function (identifier)) @function) +(signal_binding "swapped" @keyword) + +(styles_list "styles" @function.macro) +(layout_definition "layout" @function.macro) + +(gettext_string "_" @function.builtin) + +(menu_definition "menu" @keyword) +(menu_section "section" @keyword) +(menu_item "item" @function.macro) + +(template_definition (template_name_qualifier) @type.qualifier) + +(import_statement (gobject_library) @namespace) + +(import_statement (version_number) @float) + +(float) @float +(number) @number + +[ + ";" + "." + "," +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket diff --git a/tree-sitter/highlights/c.scm b/tree-sitter/highlights/c.scm new file mode 100644 index 0000000000..4a23e1ce9a --- /dev/null +++ b/tree-sitter/highlights/c.scm @@ -0,0 +1,226 @@ +; Lower priority to prefer @parameter when identifier appears in parameter_declaration. +((identifier) @variable (#set! "priority" 95)) +(preproc_def (preproc_arg) @variable) + +[ + "default" + "enum" + "struct" + "typedef" + "union" + "goto" + "asm" + "__asm__" +] @keyword + +[ + "sizeof" + "offsetof" +] @keyword.operator + +"return" @keyword.return + +[ + "while" + "for" + "do" + "continue" + "break" +] @repeat + +[ + "if" + "else" + "case" + "switch" +] @conditional + +[ + "#if" + "#ifdef" + "#ifndef" + "#else" + "#elif" + "#endif" + "#elifdef" + "#elifndef" + (preproc_directive) +] @preproc + +"#define" @define + +"#include" @include + +[ ";" ":" "," "::" ] @punctuation.delimiter + +"..." @punctuation.special + +[ "(" ")" "[" "]" "{" "}"] @punctuation.bracket + +[ + "=" + + "-" + "*" + "/" + "+" + "%" + + "~" + "|" + "&" + "^" + "<<" + ">>" + + "->" + "." + + "<" + "<=" + ">=" + ">" + "==" + "!=" + + "!" + "&&" + "||" + + "-=" + "+=" + "*=" + "/=" + "%=" + "|=" + "&=" + "^=" + ">>=" + "<<=" + "--" + "++" +] @operator + +;; Make sure the comma operator is given a highlight group after the comma +;; punctuator so the operator is highlighted properly. +(comma_expression [ "," ] @operator) + +[ + (true) + (false) +] @boolean + +(conditional_expression [ "?" ":" ] @conditional.ternary) + +(string_literal) @string +(system_lib_string) @string +(escape_sequence) @string.escape + +(null) @constant.builtin +(number_literal) @number +(char_literal) @character + +((preproc_arg) @function.macro (#set! "priority" 90)) +(preproc_defined) @function.macro + +(((field_expression + (field_identifier) @property)) @_parent + (#not-has-parent? @_parent template_method function_declarator call_expression)) + +(field_designator) @property +(((field_identifier) @property) + (#has-ancestor? @property field_declaration) + (#not-has-ancestor? @property function_declarator)) + +(statement_identifier) @label + +[ + (type_identifier) + (type_descriptor) +] @type + +(storage_class_specifier) @storageclass + +[ + (type_qualifier) + (gnu_asm_qualifier) +] @type.qualifier + +(linkage_specification + "extern" @storageclass) + +(type_definition + declarator: (type_identifier) @type.definition) + +(primitive_type) @type.builtin + +(sized_type_specifier _ @type.builtin type: _?) + +((identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z0-9_]+$")) +(preproc_def (preproc_arg) @constant + (#lua-match? @constant "^[A-Z][A-Z0-9_]+$")) +(enumerator + name: (identifier) @constant) +(case_statement + value: (identifier) @constant) + +((identifier) @constant.builtin + (#any-of? @constant.builtin "stderr" "stdin" "stdout")) +(preproc_def (preproc_arg) @constant.builtin + (#any-of? @constant.builtin "stderr" "stdin" "stdout")) + +;; Preproc def / undef +(preproc_def + name: (_) @constant) +(preproc_call + directive: (preproc_directive) @_u + argument: (_) @constant + (#eq? @_u "#undef")) + +(call_expression + function: (identifier) @function.call) +(call_expression + function: (field_expression + field: (field_identifier) @function.call)) +(function_declarator + declarator: (identifier) @function) +(function_declarator + declarator: (parenthesized_declarator + (pointer_declarator + declarator: (field_identifier) @function))) +(preproc_function_def + name: (identifier) @function.macro) + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +;; Parameters +(parameter_declaration + declarator: (identifier) @parameter) + +(parameter_declaration + declarator: (array_declarator) @parameter) + +(parameter_declaration + declarator: (pointer_declarator) @parameter) + +(preproc_params (identifier) @parameter) + +[ + "__attribute__" + "__declspec" + "__based" + "__cdecl" + "__clrcall" + "__stdcall" + "__fastcall" + "__thiscall" + "__vectorcall" + (ms_pointer_modifier) + (attribute_declaration) +] @attribute + +(ERROR) @error diff --git a/tree-sitter/highlights/c_sharp.scm b/tree-sitter/highlights/c_sharp.scm new file mode 100644 index 0000000000..9895d83413 --- /dev/null +++ b/tree-sitter/highlights/c_sharp.scm @@ -0,0 +1,412 @@ +(identifier) @variable + +((identifier) @keyword + (#eq? @keyword "value") + (#has-ancestor? @keyword accessor_declaration)) + +(method_declaration + name: (identifier) @method) + +(local_function_statement + name: (identifier) @method) + +(method_declaration + type: (identifier) @type) + +(local_function_statement + type: (identifier) @type) + +(interpolation) @none + +(invocation_expression + (member_access_expression + name: (identifier) @method.call)) + +(invocation_expression + function: (conditional_access_expression + (member_binding_expression + name: (identifier) @method.call))) + +(namespace_declaration + name: [(qualified_name) (identifier)] @namespace) + +(qualified_name + (identifier) @type) + +(invocation_expression + (identifier) @method.call) + +(field_declaration + (variable_declaration + (variable_declarator + (identifier) @field))) + +(initializer_expression + (assignment_expression + left: (identifier) @field)) + +(parameter_list + (parameter + name: (identifier) @parameter)) + +(parameter_list + (parameter + type: (identifier) @type)) + +(integer_literal) @number +(real_literal) @float + +(null_literal) @constant.builtin +(character_literal) @character + +[ + (string_literal) + (verbatim_string_literal) + (interpolated_string_expression) +] @string + +(boolean_literal) @boolean + +[ + (predefined_type) +] @type.builtin + +(implicit_type) @keyword + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^///[^/]")) +((comment) @comment.documentation + (#lua-match? @comment.documentation "^///$")) + +(using_directive + (identifier) @type) + +(using_directive + (name_equals (identifier) @type.definition)) + +(property_declaration + name: (identifier) @property) + +(property_declaration + type: (identifier) @type) + +(nullable_type + (identifier) @type) + +(catch_declaration + type: (identifier) @type) + +(interface_declaration + name: (identifier) @type) +(class_declaration + name: (identifier) @type) +(record_declaration + name: (identifier) @type) +(enum_declaration + name: (identifier) @type) +(constructor_declaration + name: (identifier) @constructor) +(constructor_initializer [ + "base" @constructor +]) + +(variable_declaration + (identifier) @type) +(object_creation_expression + (identifier) @type) + +; Generic Types. +(type_of_expression + (generic_name + (identifier) @type)) + +(type_argument_list + (generic_name + (identifier) @type)) + +(base_list + (generic_name + (identifier) @type)) + +(type_constraint + (generic_name + (identifier) @type)) + +(object_creation_expression + (generic_name + (identifier) @type)) + +(property_declaration + (generic_name + (identifier) @type)) + +(_ + type: (generic_name + (identifier) @type)) +; Generic Method invocation with generic type +(invocation_expression + function: (generic_name + . (identifier) @method.call)) + +(invocation_expression + (member_access_expression + (generic_name + (identifier) @method))) + +(base_list + (identifier) @type) + +(type_argument_list + (identifier) @type) + +(type_parameter_list + (type_parameter) @type) + +(type_parameter_constraints_clause + target: (identifier) @type) + +(attribute + name: (identifier) @attribute) + +(for_each_statement + type: (identifier) @type) + +(tuple_element + type: (identifier) @type) + +(tuple_expression + (argument + (declaration_expression + type: (identifier) @type))) + +(as_expression + right: (identifier) @type) + +(type_of_expression + (identifier) @type) + +(name_colon + (identifier) @parameter) + +(warning_directive) @text.warning +(error_directive) @exception + +(define_directive + (identifier) @constant) @constant.macro +(undef_directive + (identifier) @constant) @constant.macro + +(line_directive) @constant.macro +(line_directive + (preproc_integer_literal) @constant + (preproc_string_literal)? @string) + +(pragma_directive + (identifier) @constant) @constant.macro +(pragma_directive + (preproc_string_literal) @string) @constant.macro + +[ + (nullable_directive) + (region_directive) + (endregion_directive) +] @constant.macro + +[ + "if" + "else" + "switch" + "break" + "case" + (if_directive) + (elif_directive) + (else_directive) + (endif_directive) +] @conditional + +(if_directive + (identifier) @constant) +(elif_directive + (identifier) @constant) + +[ + "while" + "for" + "do" + "continue" + "goto" + "foreach" +] @repeat + +[ + "try" + "catch" + "throw" + "finally" +] @exception + +[ + "+" + "?" + ":" + "++" + "-" + "--" + "&" + "&&" + "|" + "||" + "!" + "!=" + "==" + "*" + "/" + "%" + "<" + "<=" + ">" + ">=" + "=" + "-=" + "+=" + "*=" + "/=" + "%=" + "^" + "^=" + "&=" + "|=" + "~" + ">>" + ">>>" + "<<" + "<<=" + ">>=" + ">>>=" + "=>" +] @operator + +[ + ";" + "." + "," + ":" +] @punctuation.delimiter + +(conditional_expression ["?" ":"] @conditional.ternary) + +[ + "[" + "]" + "{" + "}" + "(" + ")" +] @punctuation.bracket + +(type_argument_list ["<" ">"] @punctuation.bracket) + +[ + (this_expression) + (base_expression) +] @variable.builtin + +[ + "using" + "as" +] @include + +(alias_qualified_name + (identifier "global") @include) + +[ + "with" + "new" + "typeof" + "sizeof" + "is" + "and" + "or" + "not" + "stackalloc" + "in" + "out" + "ref" +] @keyword.operator + +[ + "lock" + "params" + "operator" + "default" + "implicit" + "explicit" + "override" + "class" + "delegate" + "enum" + "interface" + "namespace" + "struct" + "get" + "set" + "init" + "where" + "record" + "event" + "add" + "remove" + "checked" + "unchecked" + "fixed" +] @keyword + +[ + "async" + "await" +] @keyword.coroutine + +[ + "const" + "extern" + "readonly" + "static" + "volatile" + "required" +] @storageclass + +[ + "abstract" + "private" + "protected" + "internal" + "public" + "partial" + "sealed" + "virtual" +] @type.qualifier + +(parameter_modifier) @operator + +(query_expression + (_ [ + "from" + "orderby" + "select" + "group" + "by" + "ascending" + "descending" + "equals" + "let" + ] @keyword)) + +[ + "return" + "yield" +] @keyword.return diff --git a/tree-sitter/highlights/cairo.scm b/tree-sitter/highlights/cairo.scm new file mode 100644 index 0000000000..0f68121ae9 --- /dev/null +++ b/tree-sitter/highlights/cairo.scm @@ -0,0 +1,338 @@ +; Preproc + +[ + "%builtins" + "%lang" +] @preproc + +; Includes + +(import_statement [ "from" "import" ] @include module_name: (dotted_name (identifier) @namespace . )) + +[ + "as" + "use" + "mod" +] @include + +; Variables + +(identifier) @variable + +; Namespaces + +(namespace_definition (identifier) @namespace) + +(mod_item + name: (identifier) @namespace) + +(use_list (self) @namespace) + +(scoped_use_list (self) @namespace) + +(scoped_identifier + path: (identifier) @namespace) + +(scoped_identifier + (scoped_identifier + name: (identifier) @namespace)) + +(scoped_type_identifier + path: (identifier) @namespace) + +((scoped_identifier + path: (identifier) @type) + (#lua-match? @type "^[A-Z]")) + +((scoped_identifier + name: (identifier) @type) + (#lua-match? @type "^[A-Z]")) + +((scoped_identifier + name: (identifier) @constant) + (#lua-match? @constant "^[A-Z][A-Z%d_]*$")) + +((scoped_identifier + path: (identifier) @type + name: (identifier) @constant) + (#lua-match? @type "^[A-Z]") + (#lua-match? @constant "^[A-Z]")) + +((scoped_type_identifier + path: (identifier) @type + name: (type_identifier) @constant) + (#lua-match? @type "^[A-Z]") + (#lua-match? @constant "^[A-Z]")) + +(scoped_use_list + path: (identifier) @namespace) + +(scoped_use_list + path: (scoped_identifier + (identifier) @namespace)) + +(use_list (scoped_identifier (identifier) @namespace . (_))) + +(use_list (identifier) @type (#lua-match? @type "^[A-Z]")) + +(use_as_clause alias: (identifier) @type (#lua-match? @type "^[A-Z]")) + +; Keywords + +[ + ; 0.x + "using" + "namespace" + "struct" + "let" + "const" + "local" + "rel" + "abs" + "dw" + "alloc_locals" + (inst_ret) + "with_attr" + "with" + "call" + "nondet" + + ; 1.0 + "type" + "impl" + "implicits" + "of" + "ref" + "mut" + "trait" + "enum" +] @keyword + +[ + "func" + "fn" + "end" +] @keyword.function + +"return" @keyword.return + +[ + "cast" + "new" + "and" +] @keyword.operator + +[ + "tempvar" + "extern" +] @storageclass + +[ + "if" + "else" + "match" +] @conditional + +[ + "loop" +] @repeat + +[ + "assert" + "static_assert" + "nopanic" +] @exception + +; Fields + +(implicit_arguments (typed_identifier (identifier) @field)) + +(member_expression "." (identifier) @field) + +(call_expression (assignment_expression left: (identifier) @field)) + +(tuple_expression (assignment_expression left: (identifier) @field)) + +(field_identifier) @field + +(shorthand_field_initializer (identifier) @field) + +; Parameters + +(arguments (typed_identifier (identifier) @parameter)) + +(call_expression (tuple_expression (assignment_expression left: (identifier) @parameter))) + +(return_type (tuple_type (named_type . (identifier) @parameter))) + +(parameter (identifier) @parameter) + +; Builtins + +(builtin_directive (identifier) @variable.builtin) +(lang_directive (identifier) @variable.builtin) + +[ + "ap" + "fp" + (self) +] @variable.builtin + +; Functions + +(function_definition "func" (identifier) @function) +(function_definition "fn" (identifier) @function) +(function_signature "fn" (identifier) @function) +(extern_function_statement (identifier) @function) + +(call_expression + function: (identifier) @function.call) + +(call_expression + function: (scoped_identifier + (identifier) @function.call .)) + +(call_expression + function: (field_expression + field: (field_identifier) @function.call)) + +[ + "jmp" +] @function.builtin + +; Types + +(struct_definition . (identifier) @type (typed_identifier (identifier) @field)?) + +(named_type (identifier) @type .) + +[ + (builtin_type) + (primitive_type) +] @type.builtin + +((identifier) @type + (#lua-match? @type "^[A-Z][a-zA-Z0-9_]*$")) + +(type_identifier) @type + +; Constants + +((identifier) @constant + (#lua-match? @constant "^[A-Z_][A-Z0-9_]*$")) + +(enum_variant + name: (identifier) @constant) + +(call_expression + function: (scoped_identifier + "::" + name: (identifier) @constant) + (#lua-match? @constant "^[A-Z]")) + +((match_arm + pattern: (match_pattern (identifier) @constant)) + (#lua-match? @constant "^[A-Z]")) + +((match_arm + pattern: (match_pattern + (scoped_identifier + name: (identifier) @constant))) + (#lua-match? @constant "^[A-Z]")) + +((identifier) @constant.builtin + (#any-of? @constant.builtin "Some" "None" "Ok" "Err")) + +; Constructors + +(unary_expression "new" (call_expression . (identifier) @constructor)) + +((call_expression . (identifier) @constructor) + (#lua-match? @constructor "^%u")) + +; Attributes + +(decorator "@" @attribute (identifier) @attribute) + +(attribute_item (identifier) @function.macro) + +(attribute_item (scoped_identifier (identifier) @function.macro .)) + +; Labels + +(label . (identifier) @label) + +(inst_jmp_to_label "jmp" . (identifier) @label) + +(inst_jnz_to_label "jmp" . (identifier) @label) + +; Operators + +[ + "+" + "-" + "*" + "/" + "**" + "==" + "!=" + "&" + "=" + "++" + "+=" + "@" + "!" + "~" + ".." + "&&" + "||" + "^" + "<" + "<=" + ">" + ">=" + "<<" + ">>" + "%" + "-=" + "*=" + "/=" + "%=" + "&=" + "|=" + "^=" + "<<=" + ">>=" + "?" +] @operator + +; Literals + +(number) @number + +(boolean) @boolean + +[ + (string) + (short_string) +] @string + +; Punctuation + +(attribute_item "#" @punctuation.special) + +[ "." "," ":" ";" "->" "=>" "::" ] @punctuation.delimiter + +[ "{" "}" "(" ")" "[" "]" "%{" "%}" ] @punctuation.bracket + +(type_parameters [ "<" ">" ] @punctuation.bracket) + +(type_arguments [ "<" ">" ] @punctuation.bracket) + +; Comment + +(comment) @comment @spell + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/capnp.scm b/tree-sitter/highlights/capnp.scm new file mode 100644 index 0000000000..780f03675c --- /dev/null +++ b/tree-sitter/highlights/capnp.scm @@ -0,0 +1,154 @@ +; Preproc + +[ + (unique_id) + (top_level_annotation_body) +] @preproc + +; Includes + +[ + "import" + "$import" + "embed" + "using" +] @include + +(import_path) @string @text.uri + +; Keywords + +[ + "annotation" + "enum" + "group" + "interface" + "struct" + "union" + "extends" + "namespace" +] @keyword + +; Builtins + +[ + "const" +] @type.qualifier + +[ + (primitive_type) + "List" +] @type.builtin + +; Typedefs + +(type_definition) @type.definition + +; Labels (@number, @number!) + +(field_version) @label + +; Methods + +[ + (annotation_definition_identifier) + (method_identifier) +] @method + +; Fields + +(field_identifier) @field + +; Properties + +(property) @property + +; Parameters + +[ + (param_identifier) + (return_identifier) +] @parameter + +(annotation_target) @parameter.builtin + +; Constants + +[ + (const_identifier) + (local_const) + (enum_member) +] @constant + +(void) @constant.builtin + +; Types + +[ + (enum_identifier) + (extend_type) + (type_identifier) +] @type + +; Attributes + +[ + (annotation_identifier) + (attribute) +] @attribute + +; Operators + +"=" @operator + +; Literals + +[ + (string) + (concatenated_string) + (block_text) + (namespace) +] @string + +(namespace) @text.underline + +(escape_sequence) @string.escape + +(data_string) @string.special + +(number) @number + +(float) @float + +(boolean) @boolean + +(data_hex) @symbol + +; Punctuation + +[ + "*" + "$" + ":" +] @punctuation.special + +["{" "}"] @punctuation.bracket + +["(" ")"] @punctuation.bracket + +["[" "]"] @punctuation.bracket + +[ + "." + "," + ";" + "->" +] @punctuation.delimiter + +; Comments + +(comment) @comment @spell + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/chatito.scm b/tree-sitter/highlights/chatito.scm new file mode 100644 index 0000000000..f933f43c35 --- /dev/null +++ b/tree-sitter/highlights/chatito.scm @@ -0,0 +1,54 @@ +;; Punctuation + +[ + "%[" + "@[" + "~[" + "*[" + "]" + "(" + ")" +] @punctuation.bracket + +[":" ","] @punctuation.delimiter + +(["\"" "'"] @punctuation.special @conceal + (#set! conceal "")) + +["%" "?" "#"] @character.special + +;; Entities + +(intent) @namespace + +(slot) @type + +(variation) @type.qualifier + +(alias) @property + +(number) @number + +(argument + key: (string) @label + value: (string) @string) + +(escape) @string.escape + +;; Import + +"import" @include + +(file) @string.special + +;; Text + +(word) @text @spell + +;; Comment + +(comment) @comment @spell + +;; Error + +(ERROR) @error diff --git a/tree-sitter/highlights/clojure.scm b/tree-sitter/highlights/clojure.scm new file mode 100644 index 0000000000..1556b85ddf --- /dev/null +++ b/tree-sitter/highlights/clojure.scm @@ -0,0 +1,376 @@ +;; >> Explanation +;; Parsers for lisps are a bit weird in that they just return the raw forms. +;; This means we have to do a bit of extra work in the queries to get things +;; highlighted as they should be. +;; +;; For the most part this means that some things have to be assigned multiple +;; groups. +;; By doing this we can add a basic capture and then later refine it with more +;; specialized captures. +;; This can mean that sometimes things are highlighted weirdly because they +;; have multiple highlight groups applied to them. + + +;; >> Literals + +( + (dis_expr) @comment + (#set! "priority" 105) ; Higher priority to mark the whole sexpr as a comment +) +(kwd_lit) @symbol +(str_lit) @string @spell +(num_lit) @number +(char_lit) @character +(bool_lit) @boolean +(nil_lit) @constant.builtin +(comment) @comment @spell +(regex_lit) @string.regex + +["'" "`"] @string.escape + +["~" "~@" "#"] @punctuation.special + +["{" "}" "[" "]" "(" ")"] @punctuation.bracket + + + +;; >> Symbols + +; General symbol highlighting +(sym_lit) @variable + +; General function calls +(list_lit + . + (sym_lit) @function.call) +(anon_fn_lit + . + (sym_lit) @function.call) + +; Quoted symbols +(quoting_lit + (sym_lit) @symbol) +(syn_quoting_lit + (sym_lit) @symbol) + +; Used in destructure pattern +((sym_lit) @parameter + (#lua-match? @parameter "^[&]")) + +; Inline function variables +((sym_lit) @variable.builtin + (#lua-match? @variable.builtin "^%%")) + +; Constructor +((sym_lit) @constructor + (#lua-match? @constructor "^-\\>[^\\>].*")) + +; Builtin dynamic variables +((sym_lit) @variable.builtin + (#any-of? @variable.builtin + "*agent*" "*allow-unresolved-vars*" "*assert*" + "*clojure-version*" "*command-line-args*" + "*compile-files*" "*compile-path*" "*compiler-options*" + "*data-readers*" "*default-data-reader-fn*" + "*err*" "*file*" "*flush-on-newline*" "*fn-loader*" + "*in*" "*math-context*" "*ns*" "*out*" + "*print-dup*" "*print-length*" "*print-level*" + "*print-meta*" "*print-namespace-maps*" "*print-readably*" + "*read-eval*" "*reader-resolver*" + "*source-path*" "*suppress-read*" + "*unchecked-math*" "*use-context-classloader*" + "*verbose-defrecords*" "*warn-on-reflection*")) + +; Builtin repl variables +((sym_lit) @variable.builtin + (#any-of? @variable.builtin + "*1" "*2" "*3" "*e")) + +; Gensym +;; Might not be needed +((sym_lit) @variable + (#lua-match? @variable "^.*#$")) + +; Types +;; TODO: improve? +((sym_lit) @type + (#lua-match? @type "^[%u][^/]*$")) +;; Symbols with `.` but not `/` +((sym_lit) @type + (#lua-match? @type "^[^/]+[.][^/]*$")) + +; Interop +((sym_lit) @method + (#match? @method "^\\.[^-]")) +((sym_lit) @field + (#match? @field "^\\.-")) +((sym_lit) @field + (#lua-match? @field "^[%u].*/.+")) +(list_lit + . + (sym_lit) @method + (#lua-match? @method "^[%u].*/.+")) +;; TODO: Special casing for the `.` macro + +; Operators +((sym_lit) @operator + (#any-of? @operator + "*" "*'" "+" "+'" "-" "-'" "/" + "<" "<=" ">" ">=" "=" "==")) +((sym_lit) @keyword.operator + (#any-of? @keyword.operator + "not" "not=" "and" "or")) + +; Definition functions +((sym_lit) @keyword + (#any-of? @keyword + "def" "defonce" "defrecord" "defmacro" "definline" "definterface" + "defmulti" "defmethod" "defstruct" "defprotocol" + "deftype")) +((sym_lit) @keyword + (#eq? @keyword "declare")) +((sym_name) @keyword.coroutine + (#any-of? @keyword.coroutine + "alts!" "alts!!" "await" "await-for" "await1" "chan" "close!" "future" "go" "sync" "thread" "timeout" "!" ">!!")) +((sym_lit) @keyword.function + (#match? @keyword.function "^(defn|defn-|fn|fn[*])$")) + +; Comment +((sym_lit) @comment + (#any-of? @comment "comment")) + +; Conditionals +((sym_lit) @conditional + (#any-of? @conditional + "case" "cond" "cond->" "cond->>" "condp")) +((sym_lit) @conditional + (#any-of? @conditional + "if" "if-let" "if-not" "if-some")) +((sym_lit) @conditional + (#any-of? @conditional + "when" "when-first" "when-let" "when-not" "when-some")) + +; Repeats +((sym_lit) @repeat + (#any-of? @repeat + "doseq" "dotimes" "for" "loop" "recur" "while")) + +; Exception +((sym_lit) @exception + (#any-of? @exception + "throw" "try" "catch" "finally")) + +; Includes +((sym_lit) @include + (#any-of? @include "ns" "import" "require" "use")) + +; Builtin macros +;; TODO: Do all these items belong here? +((sym_lit) @function.macro + (#any-of? @function.macro + "." ".." "->" "->>" "amap" "areduce" "as->" "assert" + "binding" "bound-fn" "delay" "do" "dosync" + "doto" "extend-protocol" "extend-type" + "gen-class" "gen-interface" "io!" "lazy-cat" + "lazy-seq" "let" "letfn" "locking" "memfn" "monitor-enter" + "monitor-exit" "proxy" "proxy-super" "pvalues" + "refer-clojure" "reify" "set!" "some->" "some->>" + "time" "unquote" "unquote-splicing" "var" "vswap!" + "with-bindings" "with-in-str" "with-loading-context" "with-local-vars" + "with-open" "with-out-str" "with-precision" "with-redefs")) + +; All builtin functions +; (->> (ns-publics *ns*) +; (keep (fn [[s v]] (when-not (:macro (meta v)) s))) +; sort +; clojure.pprint/pprint)) +;; ...and then lots of manual filtering... +((sym_lit) @function.builtin + (#any-of? @function.builtin + "->ArrayChunk" "->Eduction" "->Vec" "->VecNode" "->VecSeq" + "-cache-protocol-fn" "-reset-methods" "PrintWriter-on" + "StackTraceElement->vec" "Throwable->map" "accessor" + "aclone" "add-classpath" "add-tap" "add-watch" "agent" + "agent-error" "agent-errors" "aget" "alength" "alias" + "all-ns" "alter" "alter-meta!" "alter-var-root" "ancestors" + "any?" "apply" "array-map" "aset" "aset-boolean" "aset-byte" + "aset-char" "aset-double" "aset-float" "aset-int" + "aset-long" "aset-short" "assoc" "assoc!" "assoc-in" + "associative?" "atom" "bases" "bean" "bigdec" "bigint" "biginteger" + "bit-and" "bit-and-not" "bit-clear" "bit-flip" "bit-not" "bit-or" + "bit-set" "bit-shift-left" "bit-shift-right" "bit-test" + "bit-xor" "boolean" "boolean-array" "boolean?" + "booleans" "bound-fn*" "bound?" "bounded-count" + "butlast" "byte" "byte-array" "bytes" "bytes?" + "cast" "cat" "char" "char-array" "char-escape-string" + "char-name-string" "char?" "chars" "chunk" "chunk-append" + "chunk-buffer" "chunk-cons" "chunk-first" "chunk-next" + "chunk-rest" "chunked-seq?" "class" "class?" + "clear-agent-errors" "clojure-version" "coll?" + "commute" "comp" "comparator" "compare" "compare-and-set!" + "compile" "complement" "completing" "concat" "conj" + "conj!" "cons" "constantly" "construct-proxy" "contains?" + "count" "counted?" "create-ns" "create-struct" "cycle" + "dec" "dec'" "decimal?" "dedupe" "default-data-readers" + "delay?" "deliver" "denominator" "deref" "derive" + "descendants" "destructure" "disj" "disj!" "dissoc" + "dissoc!" "distinct" "distinct?" "doall" "dorun" "double" + "double-array" "eduction" "empty" "empty?" "ensure" "ensure-reduced" + "enumeration-seq" "error-handler" "error-mode" "eval" + "even?" "every-pred" "every?" "extend" "extenders" "extends?" + "false?" "ffirst" "file-seq" "filter" "filterv" "find" + "find-keyword" "find-ns" "find-protocol-impl" + "find-protocol-method" "find-var" "first" "flatten" + "float" "float-array" "float?" "floats" "flush" "fn?" + "fnext" "fnil" "force" "format" "frequencies" + "future-call" "future-cancel" "future-cancelled?" + "future-done?" "future?" "gensym" "get" "get-in" + "get-method" "get-proxy-class" "get-thread-bindings" + "get-validator" "group-by" "halt-when" "hash" + "hash-combine" "hash-map" "hash-ordered-coll" "hash-set" + "hash-unordered-coll" "ident?" "identical?" "identity" + "ifn?" "in-ns" "inc" "inc'" "indexed?" "init-proxy" + "inst-ms" "inst-ms*" "inst?" "instance?" "int" "int-array" + "int?" "integer?" "interleave" "intern" "interpose" "into" + "into-array" "ints" "isa?" "iterate" "iterator-seq" "juxt" + "keep" "keep-indexed" "key" "keys" "keyword" "keyword?" + "last" "line-seq" "list" "list*" "list?" "load" "load-file" + "load-reader" "load-string" "loaded-libs" "long" "long-array" + "longs" "macroexpand" "macroexpand-1" "make-array" "make-hierarchy" + "map" "map-entry?" "map-indexed" "map?" "mapcat" "mapv" + "max" "max-key" "memoize" "merge" "merge-with" "meta" + "method-sig" "methods" "min" "min-key" "mix-collection-hash" + "mod" "munge" "name" "namespace" "namespace-munge" "nat-int?" + "neg-int?" "neg?" "newline" "next" "nfirst" "nil?" "nnext" + "not-any?" "not-empty" "not-every?" "ns-aliases" + "ns-imports" "ns-interns" "ns-map" "ns-name" "ns-publics" + "ns-refers" "ns-resolve" "ns-unalias" "ns-unmap" "nth" + "nthnext" "nthrest" "num" "number?" "numerator" "object-array" + "odd?" "parents" "partial" "partition" "partition-all" + "partition-by" "pcalls" "peek" "persistent!" "pmap" "pop" + "pop!" "pop-thread-bindings" "pos-int?" "pos?" "pr" + "pr-str" "prefer-method" "prefers" "primitives-classnames" + "print" "print-ctor" "print-dup" "print-method" "print-simple" + "print-str" "printf" "println" "println-str" "prn" "prn-str" + "promise" "proxy-call-with-super" "proxy-mappings" "proxy-name" + "push-thread-bindings" "qualified-ident?" "qualified-keyword?" + "qualified-symbol?" "quot" "rand" "rand-int" "rand-nth" "random-sample" + "range" "ratio?" "rational?" "rationalize" "re-find" "re-groups" + "re-matcher" "re-matches" "re-pattern" "re-seq" "read" + "read+string" "read-line" "read-string" "reader-conditional" + "reader-conditional?" "realized?" "record?" "reduce" + "reduce-kv" "reduced" "reduced?" "reductions" "ref" "ref-history-count" + "ref-max-history" "ref-min-history" "ref-set" "refer" + "release-pending-sends" "rem" "remove" "remove-all-methods" + "remove-method" "remove-ns" "remove-tap" "remove-watch" + "repeat" "repeatedly" "replace" "replicate" + "requiring-resolve" "reset!" "reset-meta!" "reset-vals!" + "resolve" "rest" "restart-agent" "resultset-seq" "reverse" + "reversible?" "rseq" "rsubseq" "run!" "satisfies?" + "second" "select-keys" "send" "send-off" "send-via" + "seq" "seq?" "seqable?" "seque" "sequence" "sequential?" + "set" "set-agent-send-executor!" "set-agent-send-off-executor!" + "set-error-handler!" "set-error-mode!" "set-validator!" + "set?" "short" "short-array" "shorts" "shuffle" + "shutdown-agents" "simple-ident?" "simple-keyword?" + "simple-symbol?" "slurp" "some" "some-fn" "some?" + "sort" "sort-by" "sorted-map" "sorted-map-by" + "sorted-set" "sorted-set-by" "sorted?" "special-symbol?" + "spit" "split-at" "split-with" "str" "string?" + "struct" "struct-map" "subs" "subseq" "subvec" "supers" + "swap!" "swap-vals!" "symbol" "symbol?" "tagged-literal" + "tagged-literal?" "take" "take-last" "take-nth" "take-while" + "tap>" "test" "the-ns" "thread-bound?" "to-array" + "to-array-2d" "trampoline" "transduce" "transient" + "tree-seq" "true?" "type" "unchecked-add" "unchecked-add-int" + "unchecked-byte" "unchecked-char" "unchecked-dec" + "unchecked-dec-int" "unchecked-divide-int" "unchecked-double" + "unchecked-float" "unchecked-inc" "unchecked-inc-int" + "unchecked-int" "unchecked-long" "unchecked-multiply" + "unchecked-multiply-int" "unchecked-negate" "unchecked-negate-int" + "unchecked-remainder-int" "unchecked-short" "unchecked-subtract" + "unchecked-subtract-int" "underive" "unquote" + "unquote-splicing" "unreduced" "unsigned-bit-shift-right" + "update" "update-in" "update-proxy" "uri?" "uuid?" + "val" "vals" "var-get" "var-set" "var?" "vary-meta" "vec" + "vector" "vector-of" "vector?" "volatile!" "volatile?" + "vreset!" "with-bindings*" "with-meta" "with-redefs-fn" "xml-seq" + "zero?" "zipmap" + ;; earlier + "drop" "drop-last" "drop-while" + "double?" "doubles" + "ex-data" "ex-info" + ;; 1.10 + "ex-cause" "ex-message" + ;; 1.11 + "NaN?" "abs" "infinite?" "iteration" "random-uuid" + "parse-boolean" "parse-double" "parse-long" "parse-uuid" + "seq-to-map-for-destructuring" "update-keys" "update-vals" + ;; 1.12 + "partitionv" "partitionv-all" "splitv-at")) + + + +;; >> Context based highlighting + +;; def-likes +;; Correctly highlight docstrings +;(list_lit + ;. + ;(sym_lit) @_keyword ; Don't really want to highlight twice + ;(#any-of? @_keyword + ;"def" "defonce" "defrecord" "defmacro" "definline" + ;"defmulti" "defmethod" "defstruct" "defprotocol" + ;"deftype") + ;. + ;(sym_lit) + ;. + ;;; TODO: Add @comment highlight + ;(str_lit)? + ;. + ;(_)) + +; Function definitions +(list_lit + . + (sym_lit) @_keyword.function + (#any-of? @_keyword.function "fn" "fn*" "defn" "defn-") + . + (sym_lit)? @function + . + ;; TODO: Add @comment highlight + (str_lit)?) +;; TODO: Fix parameter highlighting +;; I think there's a bug here in nvim-treesitter +;; TODO: Reproduce bug and file ticket + ;. + ;[(vec_lit + ; (sym_lit)* @parameter) + ; (list_lit + ; (vec_lit + ; (sym_lit)* @parameter))]) + +;[((list_lit +; (vec_lit +; (sym_lit) @parameter) +; (_) +; + +; ((vec_lit +; (sym_lit) @parameter) +; (_))) + + +; Meta punctuation +;; NOTE: When the above `Function definitions` query captures the +;; the @function it also captures the child meta_lit +;; We capture the meta_lit symbol (^) after so that the later +;; highlighting overrides the former +"^" @punctuation.special + +;; namespaces +(list_lit + . + (sym_lit) @_include + (#eq? @_include "ns") + . + (sym_lit) @namespace) diff --git a/tree-sitter/highlights/cmake.scm b/tree-sitter/highlights/cmake.scm new file mode 100644 index 0000000000..4ea9c80160 --- /dev/null +++ b/tree-sitter/highlights/cmake.scm @@ -0,0 +1,217 @@ +(normal_command + (identifier) + (argument_list + (argument (unquoted_argument)) @constant + ) + (#lua-match? @constant "^[%u@][%u%d_]+$") +) + +[ + (quoted_argument) + (bracket_argument) +] @string + +(variable_ref) @none +(variable) @variable + +[ + (bracket_comment) + (line_comment) +] @comment @spell + +(normal_command (identifier) @function) + +["ENV" "CACHE"] @storageclass +["$" "{" "}" "<" ">"] @punctuation.special +["(" ")"] @punctuation.bracket + +[ + (function) + (endfunction) + (macro) + (endmacro) +] @keyword.function + +[ + (if) + (elseif) + (else) + (endif) +] @conditional + +[ + (foreach) + (endforeach) + (while) + (endwhile) +] @repeat + +(normal_command + (identifier) @repeat + (#match? @repeat "\\c^(continue|break)$") +) +(normal_command + (identifier) @keyword.return + (#match? @keyword.return "\\c^return$") +) + +(function_command + (function) + (argument_list + . (argument) @function + (argument)* @parameter + ) +) + +(macro_command + (macro) + (argument_list + . (argument) @function.macro + (argument)* @parameter + ) +) + +(block_def + (block_command + (block) @function.builtin + (argument_list + (argument (unquoted_argument) @constant) + ) + (#any-of? @constant "SCOPE_FOR" "POLICIES" "VARIABLES" "PROPAGATE") + ) + (endblock_command (endblock) @function.builtin) +) +; +((argument) @boolean + (#match? @boolean "\\c^(1|on|yes|true|y|0|off|no|false|n|ignore|notfound|.*-notfound)$") +) +; +(if_command + (if) + (argument_list + (argument) @keyword.operator + ) + (#any-of? @keyword.operator "NOT" "AND" "OR" + "COMMAND" "POLICY" "TARGET" "TEST" "DEFINED" "IN_LIST" + "EXISTS" "IS_NEWER_THAN" "IS_DIRECTORY" "IS_SYMLINK" "IS_ABSOLUTE" + "MATCHES" + "LESS" "GREATER" "EQUAL" "LESS_EQUAL" "GREATER_EQUAL" + "STRLESS" "STRGREATER" "STREQUAL" "STRLESS_EQUAL" "STRGREATER_EQUAL" + "VERSION_LESS" "VERSION_GREATER" "VERSION_EQUAL" "VERSION_LESS_EQUAL" "VERSION_GREATER_EQUAL" + ) +) +(elseif_command + (elseif) + (argument_list + (argument) @keyword.operator + ) + (#any-of? @keyword.operator "NOT" "AND" "OR" + "COMMAND" "POLICY" "TARGET" "TEST" "DEFINED" "IN_LIST" + "EXISTS" "IS_NEWER_THAN" "IS_DIRECTORY" "IS_SYMLINK" "IS_ABSOLUTE" + "MATCHES" + "LESS" "GREATER" "EQUAL" "LESS_EQUAL" "GREATER_EQUAL" + "STRLESS" "STRGREATER" "STREQUAL" "STRLESS_EQUAL" "STRGREATER_EQUAL" + "VERSION_LESS" "VERSION_GREATER" "VERSION_EQUAL" "VERSION_LESS_EQUAL" "VERSION_GREATER_EQUAL" + ) +) + +(normal_command + (identifier) @function.builtin + (#match? @function.builtin "\\c^(cmake_host_system_information|cmake_language|cmake_minimum_required|cmake_parse_arguments|cmake_path|cmake_policy|configure_file|execute_process|file|find_file|find_library|find_package|find_path|find_program|foreach|get_cmake_property|get_directory_property|get_filename_component|get_property|include|include_guard|list|macro|mark_as_advanced|math|message|option|separate_arguments|set|set_directory_properties|set_property|site_name|string|unset|variable_watch|add_compile_definitions|add_compile_options|add_custom_command|add_custom_target|add_definitions|add_dependencies|add_executable|add_library|add_link_options|add_subdirectory|add_test|aux_source_directory|build_command|create_test_sourcelist|define_property|enable_language|enable_testing|export|fltk_wrap_ui|get_source_file_property|get_target_property|get_test_property|include_directories|include_external_msproject|include_regular_expression|install|link_directories|link_libraries|load_cache|project|remove_definitions|set_source_files_properties|set_target_properties|set_tests_properties|source_group|target_compile_definitions|target_compile_features|target_compile_options|target_include_directories|target_link_directories|target_link_libraries|target_link_options|target_precompile_headers|target_sources|try_compile|try_run|ctest_build|ctest_configure|ctest_coverage|ctest_empty_binary_directory|ctest_memcheck|ctest_read_custom_files|ctest_run_script|ctest_sleep|ctest_start|ctest_submit|ctest_test|ctest_update|ctest_upload)$") +) + +(normal_command + (identifier) @_function + (argument_list + . (argument) @variable + ) + (#match? @_function "\\c^set$") +) + +(normal_command + (identifier) @_function + (#match? @_function "\\c^set$") + (argument_list + . (argument) + ( + (argument) @_cache @storageclass + . + (argument) @_type @type + (#any-of? @_cache "CACHE") + (#any-of? @_type "BOOL" "FILEPATH" "PATH" "STRING" "INTERNAL") + ) + ) +) + +(normal_command + (identifier) @_function + (#match? @_function "\\c^unset$") + (argument_list + . (argument) + (argument) @storageclass + (#any-of? @storageclass "CACHE" "PARENT_SCOPE") + ) +) + +(normal_command + (identifier) @_function + (#match? @_function "\\c^list$") + (argument_list + . (argument) @constant + (#any-of? @constant "LENGTH" "GET" "JOIN" "SUBLIST" "FIND") + . (argument) @variable + (argument) @variable . + ) +) +(normal_command + (identifier) @_function + (#match? @_function "\\c^list$") + (argument_list + . (argument) @constant + . (argument) @variable + (#any-of? @constant "APPEND" "FILTER" "INSERT" + "POP_BACK" "POP_FRONT" "PREPEND" + "REMOVE_ITEM" "REMOVE_AT" "REMOVE_DUPLICATES" + "REVERSE" "SORT") + ) +) +(normal_command + (identifier) @_function + (#match? @_function "\\c^list$") + (argument_list + . (argument) @_transform @constant + . (argument) @variable + . (argument) @_action @constant + (#eq? @_transform "TRANSFORM") + (#any-of? @_action "APPEND" "PREPEND" "TOUPPER" "TOLOWER" "STRIP" "GENEX_STRIP" "REPLACE") + ) +) +(normal_command + (identifier) @_function + (#match? @_function "\\c^list$") + (argument_list + . (argument) @_transform @constant + . (argument) @variable + . (argument) @_action @constant + . (argument)? @_selector @constant + (#eq? @_transform "TRANSFORM") + (#any-of? @_action "APPEND" "PREPEND" "TOUPPER" "TOLOWER" "STRIP" "GENEX_STRIP" "REPLACE") + (#any-of? @_selector "AT" "FOR" "REGEX") + ) +) +(normal_command + (identifier) @_function + (#match? @_function "\\c^list$") + (argument_list + . (argument) @_transform @constant + (argument) @constant . + (argument) @variable + (#eq? @_transform "TRANSFORM") + (#eq? @constant "OUTPUT_VARIABLE") + ) +) + +(escape_sequence) @string.escape + +((source_file . (line_comment) @preproc) + (#lua-match? @preproc "^#!/")) diff --git a/tree-sitter/highlights/comment.scm b/tree-sitter/highlights/comment.scm new file mode 100644 index 0000000000..70aa92de2a --- /dev/null +++ b/tree-sitter/highlights/comment.scm @@ -0,0 +1,43 @@ +(_) @spell + +((tag + (name) @text.todo @nospell + ("(" @punctuation.bracket (user) @constant ")" @punctuation.bracket)? + ":" @punctuation.delimiter) + (#any-of? @text.todo "TODO" "WIP")) + +("text" @text.todo @nospell + (#any-of? @text.todo "TODO" "WIP")) + +((tag + (name) @text.note @nospell + ("(" @punctuation.bracket (user) @constant ")" @punctuation.bracket)? + ":" @punctuation.delimiter) + (#any-of? @text.note "NOTE" "XXX" "INFO" "DOCS" "PERF" "TEST")) + +("text" @text.note @nospell + (#any-of? @text.note "NOTE" "XXX" "INFO" "DOCS" "PERF" "TEST")) + +((tag + (name) @text.warning @nospell + ("(" @punctuation.bracket (user) @constant ")" @punctuation.bracket)? + ":" @punctuation.delimiter) + (#any-of? @text.warning "HACK" "WARNING" "WARN" "FIX")) + +("text" @text.warning @nospell + (#any-of? @text.warning "HACK" "WARNING" "WARN" "FIX")) + +((tag + (name) @text.danger @nospell + ("(" @punctuation.bracket (user) @constant ")" @punctuation.bracket)? + ":" @punctuation.delimiter) + (#any-of? @text.danger "FIXME" "BUG" "ERROR")) + +("text" @text.danger @nospell + (#any-of? @text.danger "FIXME" "BUG" "ERROR")) + +; Issue number (#123) +("text" @number + (#lua-match? @number "^#[0-9]+$")) + +((uri) @text.uri @nospell) diff --git a/tree-sitter/highlights/commonlisp.scm b/tree-sitter/highlights/commonlisp.scm new file mode 100644 index 0000000000..8775b4588f --- /dev/null +++ b/tree-sitter/highlights/commonlisp.scm @@ -0,0 +1,145 @@ +(sym_lit) @variable + +;; A highlighting for functions/macros in th cl namespace is available in theHamsta/nvim-treesitter-commonlisp +;(list_lit . (sym_lit) @function.builtin (#cl-standard-function? @function.builtin)) +;(list_lit . (sym_lit) @function.builtin (#cl-standard-macro? @function.macro)) + +(dis_expr) @comment + +(defun_keyword) @function.macro +(defun_header + function_name: (_) @function) +(defun_header + lambda_list: (list_lit (sym_lit) @parameter)) +(defun_header + keyword: (defun_keyword "defmethod") + lambda_list: (list_lit (list_lit . (sym_lit) . (sym_lit) @symbol))) +(defun_header + lambda_list: (list_lit (list_lit . (sym_lit) @parameter . (_)))) +(defun_header + specifier: (sym_lit) @symbol) + +[":" "::" "."] @punctuation.special + +[ + (accumulation_verb) + (for_clause_word) + "for" + "and" + "finally" + "thereis" + "always" + "when" + "if" + "unless" + "else" + "do" + "loop" + "below" + "in" + "from" + "across" + "repeat" + "being" + "into" + "with" + "as" + "while" + "until" + "return" + "initially" +] @function.macro +"=" @operator + +(include_reader_macro) @symbol +["#C" "#c"] @number + +[(kwd_lit) (self_referential_reader_macro)] @symbol + +(package_lit + package: (_) @namespace) +"cl" @namespace + +(str_lit) @string @spell + +(num_lit) @number + +((sym_lit) @boolean (#any-of? @boolean "t" "T")) + +(nil_lit) @constant.builtin + +(comment) @comment @spell + +;; dynamic variables +((sym_lit) @variable.builtin + (#lua-match? @variable.builtin "^[*].+[*]$")) + +;; quote +"'" @string.escape +(format_specifier) @string.escape +(quoting_lit) @string.escape + +;; syntax quote +"`" @string.escape +"," @string.escape +",@" @string.escape +(syn_quoting_lit) @string.escape +(unquoting_lit) @none +(unquote_splicing_lit) @none + + +["(" ")"] @punctuation.bracket + +(block_comment) @comment @spell + + +(with_clause + type: (_) @type) +(for_clause + type: (_) @type) + +;; defun-like things +(list_lit + . + (sym_lit) @function.macro + . + (sym_lit) @function + (#eq? @function.macro "deftest")) + +;;; Macros and Special Operators +(list_lit + . + (sym_lit) @function.macro + ;; Generated via https://github.com/theHamsta/nvim-treesitter-commonlisp/blob/22fdc9fd6ed594176cc7299cc6f68dd21c94c63b/scripts/generate-symbols.lisp#L1-L21 + (#any-of? @function.macro + "do*" "step" "handler-bind" "decf" "prog1" "destructuring-bind" "defconstant" "do" "lambda" "with-standard-io-syntax" "case" "restart-bind" "ignore-errors" "with-slots" "prog2" "defclass" "define-condition" "print-unreadable-object" "defvar" "when" "with-open-file" "prog" "incf" "declaim" "and" "loop-finish" "multiple-value-bind" "pop" "psetf" "defmacro" "with-open-stream" "define-modify-macro" "defsetf" "formatter" "call-method" "handler-case" "pushnew" "or" "with-hash-table-iterator" "ecase" "cond" "defun" "remf" "ccase" "define-compiler-macro" "dotimes" "multiple-value-list" "assert" "deftype" "with-accessors" "trace" "with-simple-restart" "do-symbols" "nth-value" "define-symbol-macro" "psetq" "rotatef" "dolist" "check-type" "multiple-value-setq" "push" "pprint-pop" "loop" "define-setf-expander" "pprint-exit-if-list-exhausted" "with-condition-restarts" "defstruct" "with-input-from-string" "with-compilation-unit" "defgeneric" "with-output-to-string" "untrace" "defparameter" "ctypecase" "do-external-symbols" "etypecase" "do-all-symbols" "with-package-iterator" "unless" "defmethod" "in-package" "defpackage" "return" "typecase" "shiftf" "setf" "pprint-logical-block" "time" "restart-case" "prog*" "define-method-combination" "optimize")) + +;; constant +((sym_lit) @constant + (#lua-match? @constant "^[+].+[+]$")) + +(var_quoting_lit + marker: "#'" @symbol + value: (_) @symbol) + +["#" "#p" "#P"] @symbol + +(list_lit + . + (sym_lit) @function.builtin + ;; Generated via https://github.com/theHamsta/nvim-treesitter-commonlisp/blob/22fdc9fd6ed594176cc7299cc6f68dd21c94c63b/scripts/generate-symbols.lisp#L1-L21 + (#any-of? @function.builtin + "apropos-list" "subst" "substitute" "pprint-linear" "file-namestring" "write-char" "do*" "slot-exists-p" "file-author" "macro-function" "rassoc" "make-echo-stream" "arithmetic-error-operation" "position-if-not" "list" "cdadr" "lisp-implementation-type" "vector-push" "let" "length" "string-upcase" "adjoin" "digit-char" "step" "member-if" "handler-bind" "lognot" "apply" "gcd" "slot-unbound" "stringp" "values-list" "stable-sort" "decode-float" "make-list" "rplaca" "isqrt" "export" "synonym-stream-symbol" "function-keywords" "replace" "tanh" "maphash" "code-char" "decf" "array-displacement" "string-not-lessp" "slot-value" "remove-if" "cell-error-name" "vectorp" "cdddar" "two-way-stream-output-stream" "parse-integer" "get-internal-real-time" "fourth" "make-string" "slot-missing" "byte-size" "string-trim" "nstring-downcase" "cdaddr" "<" "labels" "interactive-stream-p" "fifth" "max" "logxor" "pathname-name" "function" "realp" "eql" "logand" "short-site-name" "prog1" "user-homedir-pathname" "list-all-packages" "exp" "cadar" "read-char-no-hang" "package-error-package" "stream-external-format" "bit-andc2" "nsubstitute-if" "mapcar" "complement" "load-logical-pathname-translations" "pprint-newline" "oddp" "caaar" "destructuring-bind" "copy-alist" "acos" "go" "bit-nor" "defconstant" "fceiling" "tenth" "nreverse" "=" "nunion" "slot-boundp" "string>" "count-if" "atom" "char=" "random-state-p" "row-major-aref" "bit-andc1" "translate-pathname" "simple-vector-p" "coerce" "substitute-if-not" "zerop" "invalid-method-error" "compile" "realpart" "remove-if-not" "pprint-tab" "hash-table-rehash-threshold" "invoke-restart" "if" "count" "/=" "do" "initialize-instance" "abs" "schar" "simple-condition-format-control" "delete-package" "subst-if" "lambda" "hash-table-count" "array-has-fill-pointer-p" "bit" "with-standard-io-syntax" "parse-namestring" "proclaim" "array-in-bounds-p" "multiple-value-call" "rplacd" "some" "graphic-char-p" "read-from-string" "consp" "cadaar" "acons" "every" "make-pathname" "mask-field" "case" "set-macro-character" "bit-and" "restart-bind" "echo-stream-input-stream" "compile-file" "fill-pointer" "numberp" "acosh" "array-dimensions" "documentation" "minusp" "inspect" "copy-structure" "integer-length" "ensure-generic-function" "char>=" "quote" "lognor" "make-two-way-stream" "ignore-errors" "tailp" "with-slots" "fboundp" "logical-pathname-translations" "equal" "float-sign" "shadow" "sleep" "numerator" "prog2" "getf" "ldb-test" "round" "locally" "echo-stream-output-stream" "log" "get-macro-character" "alphanumericp" "find-method" "nintersection" "defclass" "define-condition" "print-unreadable-object" "defvar" "broadcast-stream-streams" "floatp" "subst-if-not" "integerp" "translate-logical-pathname" "subsetp" "when" "write-string" "with-open-file" "clrhash" "apropos" "intern" "min" "string-greaterp" "import" "nset-difference" "prog" "incf" "both-case-p" "multiple-value-prog1" "characterp" "streamp" "digit-char-p" "random" "string-lessp" "make-string-input-stream" "copy-symbol" "read-sequence" "logcount" "bit-not" "boundp" "encode-universal-time" "third" "declaim" "map" "cons" "set-syntax-from-char" "and" "cis" "symbol-plist" "loop-finish" "standard-char-p" "multiple-value-bind" "asin" "string" "pop" "complex" "fdefinition" "psetf" "type-error-datum" "output-stream-p" "floor" "write-line" "<=" "defmacro" "rational" "hash-table-test" "with-open-stream" "read-char" "string-capitalize" "get-properties" "y-or-n-p" "use-package" "remove" "compiler-macro-function" "read" "package-nicknames" "remove-duplicates" "make-load-form-saving-slots" "dribble" "define-modify-macro" "make-dispatch-macro-character" "close" "cosh" "open" "finish-output" "string-downcase" "car" "nstring-capitalize" "software-type" "read-preserving-whitespace" "cadr" "fround" "nsublis" "defsetf" "find-all-symbols" "char>" "no-applicable-method" "compute-restarts" "pathname" "bit-orc2" "write-sequence" "pprint-tabular" "symbol-value" "char-name" "get-decoded-time" "formatter" "bit-vector-p" "intersection" "pathname-type" "clear-input" "call-method" "princ-to-string" "symbolp" "make-load-form" "nsubst" "pprint-dispatch" "handler-case" "method-combination-error" "probe-file" "atan" "string<" "type-error-expected-type" "pushnew" "unread-char" "print" "or" "with-hash-table-iterator" "make-sequence" "ecase" "unwind-protect" "require" "sixth" "get-dispatch-macro-character" "char-not-lessp" "read-byte" "tagbody" "file-error-pathname" "catch" "rationalp" "char-downcase" "char-int" "array-rank" "cond" "last" "make-string-output-stream" "array-dimension" "host-namestring" "input-stream-p" "decode-universal-time" "defun" "eval-when" "char-code" "pathname-directory" "evenp" "subseq" "pprint" "ftruncate" "make-instance" "pathname-host" "logbitp" "remf" "1+" "copy-pprint-dispatch" "char-upcase" "error" "read-line" "second" "make-package" "directory" "special-operator-p" "open-stream-p" "rassoc-if-not" "ccase" "equalp" "substitute-if" "*" "char/=" "cdr" "sqrt" "lcm" "logical-pathname" "eval" "define-compiler-macro" "nsubstitute-if-not" "mapcon" "imagpart" "set-exclusive-or" "simple-condition-format-arguments" "expt" "concatenate" "file-position" "macrolet" "keywordp" "hash-table-rehash-size" "+" "eighth" "use-value" "char-equal" "bit-xor" "format" "byte" "dotimes" "namestring" "char-not-equal" "multiple-value-list" "assert" "append" "notany" "typep" "delete-file" "makunbound" "cdaar" "file-write-date" ">" "cdddr" "write-to-string" "funcall" "member-if-not" "deftype" "readtable-case" "with-accessors" "truename" "constantp" "rassoc-if" "caaadr" "tree-equal" "nset-exclusive-or" "nsubstitute" "make-instances-obsolete" "package-use-list" "invoke-debugger" "provide" "count-if-not" "trace" "logandc1" "nthcdr" "char<=" "functionp" "with-simple-restart" "set-dispatch-macro-character" "logorc2" "unexport" "rest" "unbound-slot-instance" "make-hash-table" "hash-table-p" "reinitialize-instance" "nth" "do-symbols" "nreconc" "macroexpand" "store-value" "float-precision" "remprop" "nth-value" "define-symbol-macro" "update-instance-for-redefined-class" "identity" "progv" "progn" "return-from" "readtablep" "rem" "symbol-name" "psetq" "wild-pathname-p" "char" "list*" "char<" "plusp" "pairlis" "cddar" "pprint-indent" "union" "compiled-function-p" "rotatef" "abort" "machine-type" "concatenated-stream-streams" "string-right-trim" "enough-namestring" "arithmetic-error-operands" "ceiling" "dolist" "delete" "make-condition" "string-left-trim" "integer-decode-float" "check-type" "notevery" "function-lambda-expression" "-" "multiple-value-setq" "name-char" "push" "pprint-pop" "compile-file-pathname" "list-length" "nstring-upcase" "eq" "find-if" "method-qualifiers" "caadr" "cddr" "string=" "let*" "remove-method" "pathname-match-p" "find-package" "truncate" "caaddr" "get-setf-expansion" "loop" "define-setf-expander" "caddr" "package-shadowing-symbols" "force-output" "slot-makunbound" "string-not-greaterp" "cdadar" "cdaadr" "logandc2" "make-array" "merge-pathnames" "sin" "1-" "machine-version" "ffloor" "packagep" "set-pprint-dispatch" "flet" "gensym" "pprint-exit-if-list-exhausted" "cos" "get" "mapl" "delete-if" "with-condition-restarts" "atanh" "copy-list" "fill" "char-not-greaterp" "bit-orc1" "mod" "package-used-by-list" "warn" "add-method" "simple-string-p" "find-restart" "describe" "pathname-version" "peek-char" "yes-or-no-p" "complexp" "aref" "not" "position-if" "string>=" "defstruct" "float-radix" "ninth" "caadar" "subtypep" "set" "butlast" "allocate-instance" "with-input-from-string" "assoc" "write" "make-random-state" "bit-eqv" "float-digits" "long-site-name" "with-compilation-unit" "delete-duplicates" "make-symbol" "room" "cdar" "pprint-fill" "defgeneric" "macroexpand-1" "scale-float" "cdaaar" "update-instance-for-different-class" "array-row-major-index" "ed" "file-string-length" "ensure-directories-exist" "copy-readtable" "string<=" "seventh" "with-output-to-string" "signum" "elt" "untrace" "null" "defparameter" "block" "prin1" "revappend" "gentemp" "ctypecase" "ash" "sxhash" "listp" "do-external-symbols" "bit-ior" "etypecase" "sort" "change-class" "find-class" "alpha-char-p" "map-into" "terpri" "do-all-symbols" "ldb" "logorc1" "search" "fmakunbound" "load" "character" "string-not-equal" "pathnamep" "make-broadcast-stream" "arrayp" "mapcan" "cerror" "invoke-restart-interactively" "assoc-if-not" "with-package-iterator" "get-internal-run-time" "read-delimited-list" "unless" "lower-case-p" "restart-name" "/" "boole" "defmethod" "float" "software-version" "vector-pop" "vector-push-extend" "caar" "ldiff" "member" "find-symbol" "reduce" "svref" "describe-object" "logior" "string-equal" "type-of" "position" "cddadr" "pathname-device" "get-output-stream-string" "symbol-package" "tan" "compute-applicable-methods" "cddddr" "nsubst-if-not" "sublis" "set-difference" "two-way-stream-input-stream" "adjustable-array-p" "machine-instance" "signal" "conjugate" "caaaar" "endp" "lisp-implementation-version" "cddaar" "package-name" "adjust-array" "bit-nand" "gethash" "in-package" "symbol-function" "make-concatenated-stream" "defpackage" "class-of" "no-next-method" "logeqv" "deposit-field" "disassemble" "unuse-package" "copy-tree" "find" "asinh" "class-name" "rename-file" "values" "print-not-readable-object" "mismatch" "cadadr" "shadowing-import" "delete-if-not" "maplist" "listen" "return" "stream-element-type" "unintern" "merge" "make-synonym-stream" "prin1-to-string" "nsubst-if" "byte-position" "phase" "muffle-warning" "remhash" "continue" "load-time-value" "hash-table-size" "upgraded-complex-part-type" "char-lessp" "sbit" "upgraded-array-element-type" "file-length" "typecase" "cadddr" "first" "rationalize" "logtest" "find-if-not" "dpb" "mapc" "sinh" "char-greaterp" "shiftf" "denominator" "get-universal-time" "nconc" "setf" "lognand" "rename-package" "pprint-logical-block" "break" "symbol-macrolet" "the" "fresh-line" "clear-output" "assoc-if" "string/=" "princ" "directory-namestring" "stream-error-stream" "array-element-type" "setq" "copy-seq" "time" "restart-case" "prog*" "shared-initialize" "array-total-size" "simple-bit-vector-p" "define-method-combination" "write-byte" "constantly" "caddar" "print-object" "vector" "throw" "reverse" ">=" "upper-case-p" "nbutlast")) + +(list_lit + . + (sym_lit) @operator + (#match? @operator "^([+*-+=<>]|<=|>=|/=)$")) + + +((sym_lit) @symbol +(#lua-match? @symbol "^[&]")) + +[(array_dimension) "#0A" "#0a"] @number + +(char_lit) @character diff --git a/tree-sitter/highlights/cooklang.scm b/tree-sitter/highlights/cooklang.scm new file mode 100644 index 0000000000..4ced465bd4 --- /dev/null +++ b/tree-sitter/highlights/cooklang.scm @@ -0,0 +1,22 @@ +(metadata) @comment + +(ingredient + "@" @tag + (name)? @text.title + (amount + (quantity)? @number + (units)? @tag.attribute)?) + +(timer + "~" @tag + (name)? @text.title + (amount + (quantity)? @number + (units)? @tag.attribute)?) + +(cookware + "#" @tag + (name)? @text.title + (amount + (quantity)? @number + (units)? @tag.attribute)?) diff --git a/tree-sitter/highlights/corn.scm b/tree-sitter/highlights/corn.scm new file mode 100644 index 0000000000..9bb0274e5e --- /dev/null +++ b/tree-sitter/highlights/corn.scm @@ -0,0 +1,22 @@ +"let" @keyword +"in" @keyword + +[ + "{" + "}" + "[" + "]" +] @punctuation.bracket + +"." @punctuation.delimiter + +(input) @constant +(comment) @comment + +(string) @string +(integer) @number +(float) @float +(boolean) @boolean +(null) @keyword + +(ERROR) @error diff --git a/tree-sitter/highlights/cpon.scm b/tree-sitter/highlights/cpon.scm new file mode 100644 index 0000000000..807714a0f2 --- /dev/null +++ b/tree-sitter/highlights/cpon.scm @@ -0,0 +1,50 @@ +; Literals + +(string) @string +(escape_sequence) @string.escape + +(hex_blob + "x" @character.special + (_) @string) + +(esc_blob + "b" @character.special + (_) @string) + +(datetime + "d" @character.special + (_) @string.special) + +(_ key: (_) @label) + +(number) @number + +(float) @float + +(boolean) @boolean + +(null) @constant.builtin + +; Punctuation + +[ + "," + ":" +] @punctuation.delimiter + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ "<" ">" ] @punctuation.bracket + +(("\"" @conceal) + (#set! conceal "")) + +; Comments + +(comment) @comment @spell + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/cpp.scm b/tree-sitter/highlights/cpp.scm new file mode 100644 index 0000000000..bce3f90618 --- /dev/null +++ b/tree-sitter/highlights/cpp.scm @@ -0,0 +1,240 @@ +; inherits: c + +((identifier) @field + (#lua-match? @field "^m?_.*$")) + +(parameter_declaration + declarator: (reference_declarator) @parameter) + +; function(Foo ...foo) +(variadic_parameter_declaration + declarator: (variadic_declarator + (_) @parameter)) +; int foo = 0 +(optional_parameter_declaration + declarator: (_) @parameter) + +;(field_expression) @parameter ;; How to highlight this? + +(((field_expression + (field_identifier) @method)) @_parent + (#has-parent? @_parent template_method function_declarator)) + +(field_declaration + (field_identifier) @field) + +(field_initializer + (field_identifier) @property) + +(function_declarator + declarator: (field_identifier) @method) + +(concept_definition + name: (identifier) @type.definition) + +(alias_declaration + name: (type_identifier) @type.definition) + +(auto) @type.builtin + +(namespace_identifier) @namespace +((namespace_identifier) @type + (#lua-match? @type "^[%u]")) + +(case_statement + value: (qualified_identifier (identifier) @constant)) + +(using_declaration . "using" . "namespace" . [(qualified_identifier) (identifier)] @namespace) + +(destructor_name + (identifier) @method) + +; functions +(function_declarator + (qualified_identifier + (identifier) @function)) +(function_declarator + (qualified_identifier + (qualified_identifier + (identifier) @function))) +(function_declarator + (qualified_identifier + (qualified_identifier + (qualified_identifier + (identifier) @function)))) +((qualified_identifier + (qualified_identifier + (qualified_identifier + (qualified_identifier + (identifier) @function)))) @_parent + (#has-ancestor? @_parent function_declarator)) + +(function_declarator + (template_function + (identifier) @function)) + +(operator_name) @function +"operator" @function +"static_assert" @function.builtin + +(call_expression + (qualified_identifier + (identifier) @function.call)) +(call_expression + (qualified_identifier + (qualified_identifier + (identifier) @function.call))) +(call_expression + (qualified_identifier + (qualified_identifier + (qualified_identifier + (identifier) @function.call)))) +((qualified_identifier + (qualified_identifier + (qualified_identifier + (qualified_identifier + (identifier) @function.call)))) @_parent + (#has-ancestor? @_parent call_expression)) + +(call_expression + (template_function + (identifier) @function.call)) +(call_expression + (qualified_identifier + (template_function + (identifier) @function.call))) +(call_expression + (qualified_identifier + (qualified_identifier + (template_function + (identifier) @function.call)))) +(call_expression + (qualified_identifier + (qualified_identifier + (qualified_identifier + (template_function + (identifier) @function.call))))) +((qualified_identifier + (qualified_identifier + (qualified_identifier + (qualified_identifier + (template_function + (identifier) @function.call))))) @_parent + (#has-ancestor? @_parent call_expression)) + +; methods +(function_declarator + (template_method + (field_identifier) @method)) +(call_expression + (field_expression + (field_identifier) @method.call)) + +; constructors + +((function_declarator + (qualified_identifier + (identifier) @constructor)) + (#lua-match? @constructor "^%u")) + +((call_expression + function: (identifier) @constructor) +(#lua-match? @constructor "^%u")) +((call_expression + function: (qualified_identifier + name: (identifier) @constructor)) +(#lua-match? @constructor "^%u")) + +((call_expression + function: (field_expression + field: (field_identifier) @constructor)) +(#lua-match? @constructor "^%u")) + +;; constructing a type in an initializer list: Constructor (): **SuperType (1)** +((field_initializer + (field_identifier) @constructor + (argument_list)) + (#lua-match? @constructor "^%u")) + + +; Constants + +(this) @variable.builtin +(null "nullptr" @constant.builtin) + +(true) @boolean +(false) @boolean + +; Literals + +(raw_string_literal) @string + +; Keywords + +[ + "try" + "catch" + "noexcept" + "throw" +] @exception + + +[ + "class" + "decltype" + "explicit" + "friend" + "namespace" + "override" + "template" + "typename" + "using" + "concept" + "requires" +] @keyword + +[ + "co_await" +] @keyword.coroutine + +[ + "co_yield" + "co_return" +] @keyword.coroutine.return + +[ + "public" + "private" + "protected" + "virtual" + "final" +] @type.qualifier + +[ + "new" + "delete" + + "xor" + "bitand" + "bitor" + "compl" + "not" + "xor_eq" + "and_eq" + "or_eq" + "not_eq" + "and" + "or" +] @keyword.operator + +"<=>" @operator + +"::" @punctuation.delimiter + +(template_argument_list + ["<" ">"] @punctuation.bracket) + +(template_parameter_list + ["<" ">"] @punctuation.bracket) + +(literal_suffix) @operator diff --git a/tree-sitter/highlights/css.scm b/tree-sitter/highlights/css.scm new file mode 100644 index 0000000000..b26f0ec96c --- /dev/null +++ b/tree-sitter/highlights/css.scm @@ -0,0 +1,91 @@ +[ + "@media" + "@charset" + "@namespace" + "@supports" + "@keyframes" + (at_keyword) + (to) + (from) + ] @keyword + +"@import" @include + +(comment) @comment @spell + +[ + (tag_name) + (nesting_selector) + (universal_selector) + ] @type + +(function_name) @function + +[ + "~" + ">" + "+" + "-" + "*" + "/" + "=" + "^=" + "|=" + "~=" + "$=" + "*=" + "and" + "or" + "not" + "only" + ] @operator + +(important) @type.qualifier + +(attribute_selector (plain_value) @string) +(pseudo_element_selector "::" (tag_name) @property) +(pseudo_class_selector (class_name) @property) + +[ + (class_name) + (id_name) + (property_name) + (feature_name) + (attribute_name) + ] @property + +(namespace_name) @namespace + +((property_name) @type.definition + (#lua-match? @type.definition "^[-][-]")) +((plain_value) @type + (#lua-match? @type "^[-][-]")) + +[ + (string_value) + (color_value) + (unit) + ] @string + +[ + (integer_value) + (float_value) + ] @number + +[ + "#" + "," + "." + ":" + "::" + ";" + ] @punctuation.delimiter + +[ + "{" + ")" + "(" + "}" + ] @punctuation.bracket + +(ERROR) @error diff --git a/tree-sitter/highlights/cuda.scm b/tree-sitter/highlights/cuda.scm new file mode 100644 index 0000000000..275871debf --- /dev/null +++ b/tree-sitter/highlights/cuda.scm @@ -0,0 +1,13 @@ +; inherits: cpp + +[ "<<<" ">>>" ] @punctuation.bracket + +[ + "__host__" + "__device__" + "__global__" + "__forceinline__" + "__noinline__" +] @storageclass + +"__launch_bounds__" @type.qualifier diff --git a/tree-sitter/highlights/cue.scm b/tree-sitter/highlights/cue.scm new file mode 100644 index 0000000000..b83ffb940b --- /dev/null +++ b/tree-sitter/highlights/cue.scm @@ -0,0 +1,164 @@ +; Includes + +[ + "package" + "import" +] @include + +; Namespaces + +(package_identifier) @namespace + +(import_spec ["." "_"] @punctuation.special) + +[ + (attr_path) + (package_path) +] @text.uri ;; In attributes + +; Attributes + +(attribute) @attribute + +; Conditionals + +"if" @conditional + +; Repeats + +[ + "for" +] @repeat + +(for_clause "_" @punctuation.special) + +; Keywords + +[ + "let" +] @keyword + +[ + "in" +] @keyword.operator + +; Operators + +[ + "+" + "-" + "*" + "/" + "|" + "&" + "||" + "&&" + "==" + "!=" + "<" + "<=" + ">" + ">=" + "=~" + "!~" + "!" + "=" +] @operator + +; Fields & Properties + +(field + (label + (identifier) @field)) + +(selector_expression + (_) + (identifier) @property) + +; Functions + +(call_expression + function: (identifier) @function.call) +(call_expression + function: (selector_expression + (_) + (identifier) @function.call)) +(call_expression + function: (builtin_function) @function.call) + +(builtin_function) @function.builtin + +; Variables + +(identifier) @variable + +; Types + +(primitive_type) @type.builtin + +((identifier) @type + (#lua-match? @type "^_?#")) + +[ + (slice_type) + (pointer_type) +] @type ;; In attributes + +; Punctuation + +[ + "," + ":" +] @punctuation.delimiter + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +[ "<" ">" ] @punctuation.bracket + +[ + (ellipsis) + "?" +] @punctuation.special + +; Literals + +(string) @string + +[ + (escape_char) + (escape_unicode) +] @string.escape + +(number) @number + +(float) @float + +(si_unit + (float) + (_) @symbol) + +(boolean) @boolean + +[ + (null) + (top) + (bottom) +] @constant.builtin + +; Interpolations + +(interpolation "\\(" @punctuation.special (_) ")" @punctuation.special) @none + +(interpolation "\\(" (identifier) @variable ")") + +; Comments + +(comment) @comment @spell + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/d.scm b/tree-sitter/highlights/d.scm new file mode 100644 index 0000000000..229a9bf55b --- /dev/null +++ b/tree-sitter/highlights/d.scm @@ -0,0 +1,288 @@ +;; Misc + +[ + (line_comment) + (block_comment) + (nesting_block_comment) +] @comment @spell + +((line_comment) @comment.documentation + (#lua-match? @comment.documentation "^///[^/]")) +((line_comment) @comment.documentation + (#lua-match? @comment.documentation "^///$")) + +((block_comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +((nesting_block_comment) @comment.documentation + (#lua-match? @comment.documentation "^/[+][+][^+].*[+]/$")) + +[ + "(" ")" + "[" "]" + "{" "}" +] @punctuation.bracket + +[ + "," + ";" + "." + ":" +] @punctuation.delimiter + +[ + ".." + "$" +] @punctuation.special + +;; Constants + +[ + "__FILE_FULL_PATH__" + "__FILE__" + "__FUNCTION__" + "__LINE__" + "__MODULE__" + "__PRETTY_FUNCTION__" +] @constant.macro + +[ + (wysiwyg_string) + (alternate_wysiwyg_string) + (double_quoted_string) + (hex_string) + (delimited_string) + (token_string) +] @string + +(character_literal) @character + +(integer_literal) @number + +(float_literal) @float + +[ + "true" + "false" +] @boolean + +;; Functions + +(func_declarator + (identifier) @function +) + +[ + "__traits" + "__vector" + "assert" + "is" + "mixin" + "pragma" + "typeid" +] @function.builtin + +(import_expression + "import" @function.builtin +) + +(parameter + (var_declarator + (identifier) @parameter + ) +) + +(function_literal + (identifier) @parameter +) + +(constructor + "this" @constructor +) + +(destructor + "this" @constructor +) + +;; Keywords + +[ + "case" + "default" + "else" + "if" + "switch" +] @conditional + +[ + "break" + "continue" + "do" + "for" + "foreach" + "foreach_reverse" + "while" +] @repeat + +[ + "__parameters" + "alias" + "align" + "asm" + "auto" + "body" + "class" + "debug" + "enum" + "export" + "goto" + "interface" + "invariant" + "macro" + "out" + "override" + "package" + "static" + "struct" + "template" + "union" + "unittest" + "version" + "with" +] @keyword + +[ + "delegate" + "function" +] @keyword.function + +"return" @keyword.return + +[ + "cast" + "new" +] @keyword.operator + +[ + "+" + "++" + "+=" + "-" + "--" + "-=" + "*" + "*=" + "%" + "%=" + "^" + "^=" + "^^" + "^^=" + "/" + "/=" + "|" + "|=" + "||" + "~" + "~=" + "=" + "==" + "=>" + "<" + "<=" + "<<" + "<<=" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "!" + "!=" + "&" + "&&" +] @operator + +[ + "catch" + "finally" + "throw" + "try" +] @exception + +"null" @constant.builtin + +[ + "__gshared" + "const" + "immutable" + "shared" +] @storageclass + +[ + "abstract" + "deprecated" + "extern" + "final" + "inout" + "lazy" + "nothrow" + "private" + "protected" + "public" + "pure" + "ref" + "scope" + "synchronized" +] @type.qualifier + +(alias_assignment + . (identifier) @type.definition) + +(module_declaration + "module" @include +) + +(import_declaration + "import" @include +) + +(type) @type + +(catch_parameter + (qualified_identifier) @type +) + +(var_declarations + (qualified_identifier) @type +) + +(func_declaration + (qualified_identifier) @type +) + +(parameter + (qualified_identifier) @type +) + +(class_declaration + (identifier) @type +) + +(fundamental_type) @type.builtin + +(module_fully_qualified_name (packages (package_name) @namespace)) +(module_name) @namespace + +(at_attribute) @attribute + +(user_defined_attribute + "@" @attribute +) + +;; Variables + +(primary_expression + "this" @variable.builtin +) diff --git a/tree-sitter/highlights/dart.scm b/tree-sitter/highlights/dart.scm new file mode 100644 index 0000000000..372a6389c8 --- /dev/null +++ b/tree-sitter/highlights/dart.scm @@ -0,0 +1,272 @@ +(dotted_identifier_list) @string + +; Methods +; -------------------- +(super) @function + +; TODO: add method/call_expression to grammar and +; distinguish method call from variable access +(function_expression_body (identifier) @function) +; ((identifier)(selector (argument_part)) @function) + +; NOTE: This query is a bit of a work around for the fact that the dart grammar doesn't +; specifically identify a node as a function call +(((identifier) @function (#lua-match? @function "^_?[%l]")) + . (selector . (argument_part))) @function + +; Annotations +; -------------------- +(annotation + name: (identifier) @attribute) +(marker_annotation + name: (identifier) @attribute) + +; Operators and Tokens +; -------------------- +(template_substitution + "$" @punctuation.special + "{" @punctuation.special + "}" @punctuation.special +) @none + +(template_substitution + "$" @punctuation.special + (identifier_dollar_escaped) @variable +) @none + +(escape_sequence) @string.escape + +[ + "@" + "=>" + ".." + "??" + "==" + "?" + ":" + "&&" + "%" + "<" + ">" + "=" + ">=" + "<=" + "||" + (multiplicative_operator) + (increment_operator) + (is_operator) + (prefix_operator) + (equality_operator) + (additive_operator) +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +; Delimiters +; -------------------- +[ + ";" + "." + "," +] @punctuation.delimiter + +; Types +; -------------------- +(class_definition + name: (identifier) @type) +(constructor_signature + name: (identifier) @type) +(scoped_identifier + scope: (identifier) @type) +(function_signature + name: (identifier) @method) +(getter_signature + (identifier) @method) +(setter_signature + name: (identifier) @method) +(enum_declaration + name: (identifier) @type) +(enum_constant + name: (identifier) @type) +(void_type) @type + +((scoped_identifier + scope: (identifier) @type + name: (identifier) @type) + (#lua-match? @type "^[%u%l]")) + +(type_identifier) @type + +(type_alias + (type_identifier) @type.definition) + +; Variables +; -------------------- +; var keyword +(inferred_type) @keyword + +((identifier) @type + (#lua-match? @type "^_?[%u].*[%l]")) ; catch Classes or IClasses not CLASSES + +("Function" @type) + +; properties +(unconditional_assignable_selector + (identifier) @property) + +(conditional_assignable_selector + (identifier) @property) + +; assignments +(assignment_expression + left: (assignable_expression) @variable) + +(this) @variable.builtin + +; Parameters +; -------------------- +(formal_parameter + name: (identifier) @parameter) + +(named_argument + (label (identifier) @parameter)) + +; Literals +; -------------------- +[ + (hex_integer_literal) + (decimal_integer_literal) + (decimal_floating_point_literal) + ; TODO: inaccessible nodes + ; (octal_integer_literal) + ; (hex_floating_point_literal) +] @number + +(symbol_literal) @symbol +(string_literal) @string +(true) @boolean +(false) @boolean +(null_literal) @constant.builtin + +(comment) @comment @spell +(documentation_comment) @comment.documentation @spell + +; Keywords +; -------------------- +[ + "import" + "library" + "export" + "as" + "show" + "hide" +] @include + +; Reserved words (cannot be used as identifiers) +[ + ; TODO: + ; "rethrow" cannot be targeted at all and seems to be an invisible node + ; TODO: + ; the assert keyword cannot be specifically targeted + ; because the grammar selects the whole node or the content + ; of the assertion not just the keyword + ; assert + (case_builtin) + "late" + "required" + "extension" + "on" + "class" + "enum" + "extends" + "in" + "is" + "new" + "super" + "with" +] @keyword + +[ + "return" +] @keyword.return + + +; Built in identifiers: +; alone these are marked as keywords +[ + "deferred" + "factory" + "get" + "implements" + "interface" + "library" + "operator" + "mixin" + "part" + "set" + "typedef" +] @keyword + +[ + "async" + "async*" + "sync*" + "await" + "yield" +] @keyword.coroutine + +[ + (const_builtin) + (final_builtin) + "abstract" + "covariant" + "dynamic" + "external" + "static" +] @type.qualifier + +; when used as an identifier: +((identifier) @variable.builtin + (#any-of? @variable.builtin + "abstract" + "as" + "covariant" + "deferred" + "dynamic" + "export" + "external" + "factory" + "Function" + "get" + "implements" + "import" + "interface" + "library" + "operator" + "mixin" + "part" + "set" + "static" + "typedef")) + +["if" "else" "switch" "default"] @conditional + +[ + "try" + "throw" + "catch" + "finally" + (break_statement) +] @exception + +["do" "while" "continue" "for"] @repeat + +; Error +(ERROR) @error diff --git a/tree-sitter/highlights/devicetree.scm b/tree-sitter/highlights/devicetree.scm new file mode 100644 index 0000000000..e3140a5da9 --- /dev/null +++ b/tree-sitter/highlights/devicetree.scm @@ -0,0 +1,35 @@ +(comment) @comment + +[ + (preproc_include) + (dtsi_include) +] @include + +(preproc_def) @constant.macro +(preproc_function_def) @function.macro + +[ + (memory_reservation) + (file_version) +] @attribute + +[ + (string_literal) + (byte_string_literal) + (system_lib_string) +] @string + +(integer_literal) @number + +(identifier) @variable +(node (identifier) @namespace) +(property (identifier) @property) +(labeled_item (identifier) @label) +(call_expression (identifier) @function.macro) + +(reference) @label ; referencing labeled_item.identifier +(unit_address) @constant + +[ "=" ] @operator +[ "(" ")" "[" "]" "{" "}" "<" ">" ] @punctuation.bracket +[ ";" ":" "," "@" ] @punctuation.delimiter diff --git a/tree-sitter/highlights/dhall.scm b/tree-sitter/highlights/dhall.scm new file mode 100644 index 0000000000..f0c454cf1c --- /dev/null +++ b/tree-sitter/highlights/dhall.scm @@ -0,0 +1,171 @@ +;; Text + +;; Imports + +(missing_import) @include + +(local_import) @string.special.path + +(http_import) @string @text.uri + +[ + (env_variable) + (import_hash) +] @string.special + +[ (import_as_location) (import_as_text) ] @type + +;; Types + +([ + (let_binding (label) @type) + (union_type_entry (label) @type) +] (#lua-match? @type "^%u")) + +((primitive_expression + (identifier (label) @type) + (selector (label) @type)) @variable + (#vim-match? @variable "^[A-Z][^.]*$")) + +;; Parameters + +(lambda_expression label: (label) @parameter) + +;; Variables + +(label) @variable + +(identifier [ + (label) @variable + (de_bruijn_index) @operator +]) + +(let_binding label: (label) @variable) + +; Fields + +(record_literal_entry (label) @field) + +(record_type_entry (label) @field) + +(selector + (selector_dot) + (_) @field) + +;; Keywords + +(env_import) @keyword + +[ + "let" + "in" + "assert" +] @keyword + +[ + "using" + "as" + "with" +] @keyword.operator + +;; Operators + +[ + (type_operator) + (assign_operator) + (lambda_operator) + (arrow_operator) + (infix_operator) + (completion_operator) + (assert_operator) + (forall_operator) + (empty_record_literal) +] @operator + +;; Builtins + +(builtin_function) @function.builtin +(builtin [ + "Natural" + "Natural/build" + "Natural/fold" + "Natural/isZero" + "Natural/even" + "Natural/odd" + "Natural/subtract" + "Natural/toInteger" + "Natural/show" + "Integer" + "Integer/toDouble" + "Integer/show" + "Integer/negate" + "Integer/clamp" + "Double" + "Double/show" + "List" + "List/build" + "List/fold" + "List/length" + "List/head" + "List/last" + "List/indexed" + "List/reverse" + "Text" + "Text/show" + "Text/replace" + "Optional" + "Date" + "Time" + "TimeZone" + "Type" + "Kind" + "Sort" +] @type.builtin) + +;; Punctuation + +[ "," "|" ] @punctuation.delimiter +(selector_dot) @punctuation.delimiter + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +[ "<" ">" ] @punctuation.bracket + +;; Conditionals + +[ + "if" + "then" + "else" +] @conditional + +;; Literals + +(text_literal) @string +(interpolation "}" @string) +[ + (double_quote_escaped) + (single_quote_escaped) +] @string.escape + +[ + (integer_literal) + (natural_literal) +] @number + +(double_literal) @float + +(boolean_literal) @boolean + +(builtin "None") @constant.builtin + +;; Comments + +[ + (line_comment) + (block_comment) +] @comment @spell diff --git a/tree-sitter/highlights/diff.scm b/tree-sitter/highlights/diff.scm new file mode 100644 index 0000000000..4b9cbad602 --- /dev/null +++ b/tree-sitter/highlights/diff.scm @@ -0,0 +1,6 @@ +[(addition) (new_file)] @text.diff.add +[(deletion) (old_file)] @text.diff.delete + +(commit) @constant +(location) @attribute +(command) @function diff --git a/tree-sitter/highlights/dockerfile.scm b/tree-sitter/highlights/dockerfile.scm new file mode 100644 index 0000000000..592e704230 --- /dev/null +++ b/tree-sitter/highlights/dockerfile.scm @@ -0,0 +1,57 @@ +[ + "FROM" + "AS" + "RUN" + "CMD" + "LABEL" + "EXPOSE" + "ENV" + "ADD" + "COPY" + "ENTRYPOINT" + "VOLUME" + "USER" + "WORKDIR" + "ARG" + "ONBUILD" + "STOPSIGNAL" + "HEALTHCHECK" + "SHELL" + "MAINTAINER" + "CROSS_BUILD" +] @keyword + +[ + ":" + "@" +] @operator + +(comment) @comment @spell + +(image_spec + (image_tag + ":" @punctuation.special) + (image_digest + "@" @punctuation.special)) + +(double_quoted_string) @string + +(expansion + [ + "$" + "{" + "}" + ] @punctuation.special +) + +((variable) @constant + (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) + +(arg_instruction + . (unquoted_string) @property) + +(env_instruction + (env_pair . (unquoted_string) @property)) + +(expose_instruction + (expose_port) @number) diff --git a/tree-sitter/highlights/dot.scm b/tree-sitter/highlights/dot.scm new file mode 100644 index 0000000000..d8b70a9493 --- /dev/null +++ b/tree-sitter/highlights/dot.scm @@ -0,0 +1,55 @@ +(identifier) @type + +[ + "strict" + "graph" + "digraph" + "subgraph" + "node" + "edge" +] @keyword + +(string_literal) @string +(number_literal) @number + +[ + (edgeop) + (operator) +] @operator + +[ + "," + ";" +] @punctuation.delimiter + +[ + "{" + "}" + "[" + "]" + "<" + ">" +] @punctuation.bracket + +(subgraph + id: (id + (identifier) @namespace) +) + +(attribute + name: (id + (identifier) @field) +) + +(attribute + value: (id + (identifier) @constant) +) + +(comment) @comment + +(preproc) @preproc + +(comment) @spell + +(ERROR) @error diff --git a/tree-sitter/highlights/ebnf.scm b/tree-sitter/highlights/ebnf.scm new file mode 100644 index 0000000000..feafa1cef0 --- /dev/null +++ b/tree-sitter/highlights/ebnf.scm @@ -0,0 +1,43 @@ +;;;; Simple tokens ;;;; +(terminal) @string.grammar + +(special_sequence) @string.special.grammar + +(integer) @number + +(comment) @comment + +;;;; Identifiers ;;;; + +; Allow different highlighting for specific casings +((identifier) @type + (#lua-match? @type "^%u")) + +((identifier) @symbol + (#lua-match? @symbol "^%l")) + +((identifier) @constant + (#lua-match? @constant "^%u[%u%d_]+$")) + +;;; Punctuation ;;;; +[ + ";" + "," +] @punctuation.delimiter + +[ + "|" + "*" + "-" +] @operator + +"=" @keyword.operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket diff --git a/tree-sitter/highlights/ecma.scm b/tree-sitter/highlights/ecma.scm new file mode 100644 index 0000000000..f53fb0518a --- /dev/null +++ b/tree-sitter/highlights/ecma.scm @@ -0,0 +1,357 @@ +; Types + +; Javascript + +; Variables +;----------- +(identifier) @variable + +; Properties +;----------- + +(property_identifier) @property +(shorthand_property_identifier) @property +(private_property_identifier) @property + +(variable_declarator + name: (object_pattern + (shorthand_property_identifier_pattern))) @variable + +; Special identifiers +;-------------------- + +((identifier) @type + (#lua-match? @type "^[A-Z]")) + +((identifier) @constant + (#lua-match? @constant "^_*[A-Z][A-Z%d_]*$")) + +((shorthand_property_identifier) @constant + (#lua-match? @constant "^_*[A-Z][A-Z%d_]*$")) + +((identifier) @variable.builtin + (#any-of? @variable.builtin + "arguments" + "module" + "console" + "window" + "document")) + +((identifier) @type.builtin + (#any-of? @type.builtin + "Object" + "Function" + "Boolean" + "Symbol" + "Number" + "Math" + "Date" + "String" + "RegExp" + "Map" + "Set" + "WeakMap" + "WeakSet" + "Promise" + "Array" + "Int8Array" + "Uint8Array" + "Uint8ClampedArray" + "Int16Array" + "Uint16Array" + "Int32Array" + "Uint32Array" + "Float32Array" + "Float64Array" + "ArrayBuffer" + "DataView" + "Error" + "EvalError" + "InternalError" + "RangeError" + "ReferenceError" + "SyntaxError" + "TypeError" + "URIError")) + +((identifier) @namespace.builtin + (#eq? @namespace.builtin "Intl")) + +((identifier) @function.builtin + (#any-of? @function.builtin + "eval" + "isFinite" + "isNaN" + "parseFloat" + "parseInt" + "decodeURI" + "decodeURIComponent" + "encodeURI" + "encodeURIComponent" + "require")) + +; Function and method definitions +;-------------------------------- + +(function + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(generator_function + name: (identifier) @function) +(generator_function_declaration + name: (identifier) @function) +(method_definition + name: [(property_identifier) (private_property_identifier)] @method) +(method_definition + name: (property_identifier) @constructor + (#eq? @constructor "constructor")) + +(pair + key: (property_identifier) @method + value: (function)) +(pair + key: (property_identifier) @method + value: (arrow_function)) + +(assignment_expression + left: (member_expression + property: (property_identifier) @method) + right: (arrow_function)) +(assignment_expression + left: (member_expression + property: (property_identifier) @method) + right: (function)) + +(variable_declarator + name: (identifier) @function + value: (arrow_function)) +(variable_declarator + name: (identifier) @function + value: (function)) + +(assignment_expression + left: (identifier) @function + right: (arrow_function)) +(assignment_expression + left: (identifier) @function + right: (function)) + +; Function and method calls +;-------------------------- + +(call_expression + function: (identifier) @function.call) + +(call_expression + function: (member_expression + property: [(property_identifier) (private_property_identifier)] @method.call)) + +; Constructor +;------------ + +(new_expression + constructor: (identifier) @constructor) + +; Variables +;---------- +(namespace_import + (identifier) @namespace) + +; Decorators +;---------- +(decorator "@" @attribute (identifier) @attribute) +(decorator "@" @attribute (call_expression (identifier) @attribute)) + +; Literals +;--------- + +[ + (this) + (super) +] @variable.builtin + +((identifier) @variable.builtin + (#eq? @variable.builtin "self")) + +[ + (true) + (false) +] @boolean + +[ + (null) + (undefined) +] @constant.builtin + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +(hash_bang_line) @preproc + +((string_fragment) @preproc + (#eq? @preproc "use strict")) + +(string) @string @spell +(template_string) @string +(escape_sequence) @string.escape +(regex_pattern) @string.regex +(regex_flags) @character.special +(regex "/" @punctuation.bracket) ; Regex delimiters + +(number) @number +((identifier) @number + (#any-of? @number "NaN" "Infinity")) + +; Punctuation +;------------ + +";" @punctuation.delimiter +"." @punctuation.delimiter +"," @punctuation.delimiter + +(pair ":" @punctuation.delimiter) +(pair_pattern ":" @punctuation.delimiter) +(switch_case ":" @punctuation.delimiter) +(switch_default ":" @punctuation.delimiter) + +[ + "--" + "-" + "-=" + "&&" + "+" + "++" + "+=" + "&=" + "/=" + "**=" + "<<=" + "<" + "<=" + "<<" + "=" + "==" + "===" + "!=" + "!==" + "=>" + ">" + ">=" + ">>" + "||" + "%" + "%=" + "*" + "**" + ">>>" + "&" + "|" + "^" + "??" + "*=" + ">>=" + ">>>=" + "^=" + "|=" + "&&=" + "||=" + "??=" + "..." +] @operator + +(binary_expression "/" @operator) +(ternary_expression ["?" ":"] @conditional.ternary) +(unary_expression ["!" "~" "-" "+"] @operator) +(unary_expression ["delete" "void"] @keyword.operator) + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +((template_substitution ["${" "}"] @punctuation.special) @none) + +; Keywords +;---------- + +[ + "if" + "else" + "switch" + "case" +] @conditional + +[ + "import" + "from" +] @include + +(export_specifier "as" @include) +(import_specifier "as" @include) +(namespace_export "as" @include) +(namespace_import "as" @include) + +[ + "for" + "of" + "do" + "while" + "continue" +] @repeat + +[ + "break" + "class" + "const" + "debugger" + "export" + "extends" + "get" + "let" + "set" + "static" + "target" + "var" + "with" +] @keyword + +[ + "async" + "await" +] @keyword.coroutine + +[ + "return" + "yield" +] @keyword.return + +[ + "function" +] @keyword.function + +[ + "new" + "delete" + "in" + "instanceof" + "typeof" +] @keyword.operator + +[ + "throw" + "try" + "catch" + "finally" +] @exception + +(export_statement + "default" @keyword) +(switch_default + "default" @conditional) diff --git a/tree-sitter/highlights/eex.scm b/tree-sitter/highlights/eex.scm new file mode 100644 index 0000000000..781b394669 --- /dev/null +++ b/tree-sitter/highlights/eex.scm @@ -0,0 +1,15 @@ +[ + "%>" + "--%>" + "<%!--" + "<%" + "<%#" + "<%%=" + "<%=" +] @tag.delimiter + +; EEx comments are highlighted as such +(comment) @comment + +; Tree-sitter parser errors +(ERROR) @error diff --git a/tree-sitter/highlights/elixir.scm b/tree-sitter/highlights/elixir.scm new file mode 100644 index 0000000000..81bf5f858b --- /dev/null +++ b/tree-sitter/highlights/elixir.scm @@ -0,0 +1,226 @@ +; Punctuation +[ + "," + ";" +] @punctuation.delimiter + +[ + "(" + ")" + "<<" + ">>" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "%" +] @punctuation.special + +; Parser Errors +(ERROR) @error + +; Identifiers +(identifier) @variable + +; Unused Identifiers +((identifier) @comment (#lua-match? @comment "^_")) + +; Comments +(comment) @comment @spell + +; Strings +(string) @string @spell + +; Modules +(alias) @type + +; Atoms & Keywords +[ + (atom) + (quoted_atom) + (keyword) + (quoted_keyword) +] @symbol + +; Interpolation +(interpolation ["#{" "}"] @string.special) + +; Escape sequences +(escape_sequence) @string.escape + +; Integers +(integer) @number + +; Floats +(float) @float + +; Characters +[ + (char) + (charlist) +] @character + +; Booleans +(boolean) @boolean + +; Nil +(nil) @constant.builtin + +; Operators +(operator_identifier) @operator + +(unary_operator operator: _ @operator) + +(binary_operator operator: _ @operator) + +; Pipe Operator +(binary_operator operator: "|>" right: (identifier) @function) + +(dot operator: _ @operator) + +(stab_clause operator: _ @operator) + +; Local Function Calls +(call target: (identifier) @function.call) + +; Remote Function Calls +(call target: (dot left: [ + (atom) @type + (_) +] right: (identifier) @function.call) (arguments)) + +; Definition Function Calls +(call target: ((identifier) @keyword.function (#any-of? @keyword.function + "def" + "defdelegate" + "defexception" + "defguard" + "defguardp" + "defimpl" + "defmacro" + "defmacrop" + "defmodule" + "defn" + "defnp" + "defoverridable" + "defp" + "defprotocol" + "defstruct" + )) + (arguments [ + (call (identifier) @function) + (binary_operator left: (call target: (identifier) @function) operator: "when")])?) + +; Kernel Keywords & Special Forms +(call target: ((identifier) @keyword (#any-of? @keyword + "alias" + "case" + "catch" + "cond" + "else" + "for" + "if" + "import" + "quote" + "raise" + "receive" + "require" + "reraise" + "super" + "throw" + "try" + "unless" + "unquote" + "unquote_splicing" + "use" + "with" +))) + +; Special Constants +((identifier) @constant.builtin (#any-of? @constant.builtin + "__CALLER__" + "__DIR__" + "__ENV__" + "__MODULE__" + "__STACKTRACE__" +)) + +; Reserved Keywords +[ + "after" + "catch" + "do" + "end" + "fn" + "rescue" + "when" + "else" +] @keyword + +; Operator Keywords +[ + "and" + "in" + "not in" + "not" + "or" +] @keyword.operator + +; Capture Operator +(unary_operator + operator: "&" + operand: [ + (integer) @operator + (binary_operator + left: [ + (call target: (dot left: (_) right: (identifier) @function)) + (identifier) @function + ] operator: "/" right: (integer) @operator) + ]) + +; Non-String Sigils +(sigil + "~" @string.special + ((sigil_name) @string.special) @_sigil_name + quoted_start: _ @string.special + quoted_end: _ @string.special + ((sigil_modifiers) @string.special)? + (#not-any-of? @_sigil_name "s" "S")) + +; String Sigils +(sigil + "~" @string + ((sigil_name) @string) @_sigil_name + quoted_start: _ @string + (quoted_content) @string + quoted_end: _ @string + ((sigil_modifiers) @string)? + (#any-of? @_sigil_name "s" "S")) + +; Module attributes +(unary_operator + operator: "@" + operand: [ + (identifier) + (call target: (identifier)) + ] @constant) @constant + +; Documentation +(unary_operator + operator: "@" + operand: (call + target: ((identifier) @_identifier (#any-of? @_identifier "moduledoc" "typedoc" "shortdoc" "doc")) @comment.documentation + (arguments [ + (string) + (boolean) + (charlist) + (sigil + "~" @comment.documentation + ((sigil_name) @comment.documentation) + quoted_start: _ @comment.documentation + (quoted_content) @comment.documentation + quoted_end: _ @comment.documentation) + ] @comment.documentation))) @comment.documentation diff --git a/tree-sitter/highlights/elm.scm b/tree-sitter/highlights/elm.scm new file mode 100644 index 0000000000..fbd3a4c3fb --- /dev/null +++ b/tree-sitter/highlights/elm.scm @@ -0,0 +1,205 @@ +[ + (line_comment) + (block_comment) +] @comment @spell + +((block_comment) @comment.documentation + (#lua-match? @comment.documentation "^{[-]|[^|]")) + + +; Keywords +;--------- + +[ + "if" + "then" + "else" + (case) + (of) +] @conditional + +[ + "let" + "in" + (as) + (port) + (alias) + (infix) + (module) + (type) +] @keyword + +[ + (import) + (exposing) +] @include + + +; Punctuation +;------------ + +[ + (double_dot) +] @punctuation.special + +[ + "," + "|" + (dot) +] @punctuation.delimiter + +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + + +; Variables +;---------- + +(value_qid + (lower_case_identifier) @variable) +(value_declaration + (function_declaration_left (lower_case_identifier) @variable)) +(type_annotation + (lower_case_identifier) @variable) +(port_annotation + (lower_case_identifier) @variable) +(anything_pattern + (underscore) @variable) +(record_base_identifier + (lower_case_identifier) @variable) +(lower_pattern + (lower_case_identifier) @variable) +(exposed_value + (lower_case_identifier) @variable) + +(value_qid + ((dot) (lower_case_identifier) @field)) +(field_access_expr + ((dot) (lower_case_identifier) @field)) + +(function_declaration_left + (anything_pattern (underscore) @parameter)) +(function_declaration_left + (lower_pattern (lower_case_identifier) @parameter)) + + +; Functions +;---------- + +(value_declaration + functionDeclarationLeft: + (function_declaration_left + (lower_case_identifier) @function + (pattern))) +(value_declaration + functionDeclarationLeft: + (function_declaration_left + (lower_case_identifier) @function + pattern: (_))) +(value_declaration + functionDeclarationLeft: + (function_declaration_left + (lower_case_identifier) @function) + body: (anonymous_function_expr)) +(type_annotation + name: (lower_case_identifier) @function + typeExpression: (type_expression (arrow))) +(port_annotation + name: (lower_case_identifier) @function + typeExpression: (type_expression (arrow))) + +(function_call_expr + target: (value_expr + (value_qid (lower_case_identifier) @function.call))) + + +; Operators +;---------- + +[ + (operator_identifier) + (eq) + (colon) + (arrow) + (backslash) + "::" +] @operator + + +; Modules +;-------- + +(module_declaration + (upper_case_qid (upper_case_identifier) @namespace)) +(import_clause + (upper_case_qid (upper_case_identifier) @namespace)) +(as_clause + (upper_case_identifier) @namespace) +(value_expr + (value_qid (upper_case_identifier) @namespace)) + + +; Types +;------ + +(type_declaration + (upper_case_identifier) @type) +(type_ref + (upper_case_qid (upper_case_identifier) @type)) +(type_variable + (lower_case_identifier) @type) +(lower_type_name + (lower_case_identifier) @type) +(exposed_type + (upper_case_identifier) @type) + +(type_alias_declaration + (upper_case_identifier) @type.definition) + +(field_type + name: (lower_case_identifier) @property) +(field + name: (lower_case_identifier) @property) + +(type_declaration + (union_variant (upper_case_identifier) @constructor)) +(nullary_constructor_argument_pattern + (upper_case_qid (upper_case_identifier) @constructor)) +(union_pattern + (upper_case_qid (upper_case_identifier) @constructor)) +(value_expr + (upper_case_qid (upper_case_identifier)) @constructor) + + +; Literals +;--------- + +(number_constant_expr + (number_literal) @number) + +(upper_case_qid + ((upper_case_identifier) @boolean (#any-of? @boolean "True" "False"))) + +[ + (open_quote) + (close_quote) +] @string +(string_constant_expr + (string_escape) @string) +(string_constant_expr + (regular_string_part) @string) + +[ + (open_char) + (close_char) +] @character +(char_constant_expr + (string_escape) @character) +(char_constant_expr + (regular_string_part) @character) diff --git a/tree-sitter/highlights/elsa.scm b/tree-sitter/highlights/elsa.scm new file mode 100644 index 0000000000..7021a09704 --- /dev/null +++ b/tree-sitter/highlights/elsa.scm @@ -0,0 +1,41 @@ +; Keywords + +[ + "eval" + "let" +] @keyword + +; Function + +(function) @function + +; Method + +(method) @method + +; Parameter + +(parameter) @parameter + +; Variables + +(identifier) @variable + +; Operators + +[ + "\\" + "->" + "=" + (step) +] @operator + +; Punctuation + +["(" ")"] @punctuation.bracket + +":" @punctuation.delimiter + +; Comments + +(comment) @comment diff --git a/tree-sitter/highlights/elvish.scm b/tree-sitter/highlights/elvish.scm new file mode 100644 index 0000000000..af7c41dd81 --- /dev/null +++ b/tree-sitter/highlights/elvish.scm @@ -0,0 +1,70 @@ +(comment) @comment + +["if" "elif"] @conditional +(if (else "else" @conditional)) + +["while" "for"] @repeat +(while (else "else" @repeat)) +(for (else "else" @repeat)) + +["try" "catch" "finally"] @exception +(try (else "else" @exception)) + +"use" @include +(import (bareword) @string.special) + +(wildcard ["*" "**" "?"] @character.special) + +(command argument: (bareword) @parameter) +(command head: (identifier) @function.call) +((command head: (identifier) @keyword.return) + (#eq? @keyword.return "return")) +((command (identifier) @keyword.operator) + (#any-of? @keyword.operator "and" "or" "coalesce")) +[ + "+" "-" "*" "/" "%" "<" "<=""==" "!=" ">" + ">=" "s" ">=s" +] @function.builtin + +[">" "<" ">>" "<>" "|"] @operator + +(io_port) @number + +(function_definition + "fn" @keyword.function + (identifier) @function) + +(parameter_list) @parameter +(parameter_list "|" @punctuation.bracket) + +["var" "set" "tmp" "del"] @keyword +(variable_declaration + (lhs (identifier) @variable)) + +(variable_assignment + (lhs (identifier) @variable)) + +(temporary_assignment + (lhs (identifier) @variable)) + +(variable_deletion + (identifier) @variable) + + +(number) @number +(string) @string + +(variable (identifier) @variable) +((variable (identifier) @function) + (#match? @function ".+\\~$")) +((variable (identifier) @boolean) + (#any-of? @boolean "true" "false")) +((variable (identifier) @constant.builtin) + (#any-of? @constant.builtin + "_" "after-chdir" "args" "before-chdir" "buildinfo" "nil" + "notify-bg-job-success" "num-bg-jobs" "ok" "paths" "pid" + "pwd" "value-out-indicator" "version")) + +["$" "@"] @punctuation.special +["(" ")" "[" "]" "{" "}"] @punctuation.bracket +";" @punctuation.delimiter diff --git a/tree-sitter/highlights/embedded_template.scm b/tree-sitter/highlights/embedded_template.scm new file mode 100644 index 0000000000..0bf76a7d49 --- /dev/null +++ b/tree-sitter/highlights/embedded_template.scm @@ -0,0 +1,12 @@ +(comment_directive) @comment + +[ + "<%#" + "<%" + "<%=" + "<%_" + "<%-" + "%>" + "-%>" + "_%>" +] @keyword diff --git a/tree-sitter/highlights/erlang.scm b/tree-sitter/highlights/erlang.scm new file mode 100644 index 0000000000..68688d5051 --- /dev/null +++ b/tree-sitter/highlights/erlang.scm @@ -0,0 +1,128 @@ +((atom) @constant (#set! "priority" "90")) +(var) @variable + +(char) @character +(integer) @number +(float) @float + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^[%%][%%]")) + +;; keyword +[ + "fun" + "div" +] @keyword + +;; bracket +[ + "(" + ")" + "{" + "}" + "[" + "]" + "#" +] @punctuation.bracket + +;;; Comparisons +[ + "==" + "=:=" + "=/=" + "=<" + ">=" + "<" + ">" +] @operator ;; .comparison + +;;; operator +[ + ":" + ":=" + "!" + ;; "-" + "+" + "=" + "->" + "=>" + "|" + "?=" +] @operator + +[ + "," + "." + ";" +] @punctuation.delimiter + +;; conditional +[ + "receive" + "if" + "case" + "of" + "when" + "after" + "end" + "maybe" + "else" +] @conditional + +[ + "catch" + "try" +] @exception + +((atom) @boolean (#any-of? @boolean "true" "false")) + +;; Macros +((macro_call_expr) @constant.macro (#set! "priority" 101)) + +;; Preprocessor +(pp_define + lhs: _ @constant.macro (#set! "priority" 101) +) +(_preprocessor_directive) @preproc (#set! "priority" 99) + +;; Attributes +(pp_include) @include +(pp_include_lib) @include +(export_attribute) @include +(export_type_attribute) @type.definition +(export_type_attribute types: (fa fun: _ @type (#set! "priority" 101))) +(behaviour_attribute) @include +(module_attribute (atom) @namespace) @include +(wild_attribute name: (attr_name name: _ @attribute)) @attribute + +;; Records +(record_expr) @type +(record_field_expr _ @field) @type +(record_field_name _ @field) @type +(record_name "#" @type name: _ @type) @type +(record_decl name: _ @type) @type.definition +(record_field name: _ @field) +(record_field name: _ @field ty: _ @type) + +;; Type alias +(type_alias name: _ @type) @type.definition +(spec) @type.definition + +[(string) (binary)] @string + +;;; expr_function_call +(call expr: [(atom) (remote) (var)] @function) +(call (atom) @exception (#any-of? @exception "error" "throw" "exit")) + +;;; Parenthesized expression: (SomeFunc)(), only highlight the parens +(call + expr: (paren_expr "(" @function.call ")" @function.call) +) + +;;; function +(external_fun) @function.call +(internal_fun fun: (atom) @function.call) +(function_clause name: (atom) @function) +(fa fun: (atom) @function) diff --git a/tree-sitter/highlights/fennel.scm b/tree-sitter/highlights/fennel.scm new file mode 100644 index 0000000000..9b072f55b0 --- /dev/null +++ b/tree-sitter/highlights/fennel.scm @@ -0,0 +1,121 @@ +(comment) @comment @spell + +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + +[ + ":" + ":until" + "&" + "&as" + "?" +] @punctuation.special + +(nil) @constant.builtin +(vararg) @punctuation.special + +(boolean) @boolean +(number) @number + +(string) @string @spell +(escape_sequence) @string.escape + +(symbol) @variable + +(multi_symbol + "." @punctuation.delimiter + (symbol) @field) + +(multi_symbol_method + ":" @punctuation.delimiter + (symbol) @method.call .) + +(list . (symbol) @function.call) +(list . (multi_symbol (symbol) @function.call .)) + +((symbol) @variable.builtin + (#lua-match? @variable.builtin "^[$]")) + +(binding) @symbol + +[ + "fn" + "lambda" + "hashfn" + "#" +] @keyword.function + +(fn name: [ + (symbol) @function + (multi_symbol (symbol) @function .) +]) + +(lambda name: [ + (symbol) @function + (multi_symbol (symbol) @function .) +]) + +[ + "for" + "each" +] @repeat +((symbol) @repeat + (#any-of? @repeat + "while")) + +[ + "match" +] @conditional +((symbol) @conditional + (#any-of? @conditional + "if" "when")) + +[ + "global" + "local" + "let" + "set" + "var" + "where" + "or" +] @keyword +((symbol) @keyword + (#any-of? @keyword + "comment" "do" "doc" "eval-compiler" "lua" "macros" "quote" "tset" "values")) + +((symbol) @include + (#any-of? @include + "require" "require-macros" "import-macros" "include")) + +[ + "collect" + "icollect" + "accumulate" +] @function.macro + +((symbol) @function.macro + (#any-of? @function.macro + "->" "->>" "-?>" "-?>>" "?." "doto" "macro" "macrodebug" "partial" "pick-args" + "pick-values" "with-open")) + +; Lua builtins +((symbol) @constant.builtin + (#any-of? @constant.builtin + "arg" "_ENV" "_G" "_VERSION")) + +((symbol) @function.builtin + (#any-of? @function.builtin + "assert" "collectgarbage" "dofile" "error" "getmetatable" "ipairs" + "load" "loadfile" "next" "pairs" "pcall" "print" "rawequal" "rawget" + "rawlen" "rawset" "require" "select" "setmetatable" "tonumber" "tostring" + "type" "warn" "xpcall")) + +((symbol) @function.builtin + (#any-of? @function.builtin + "loadstring" "module" "setfenv" "unpack")) diff --git a/tree-sitter/highlights/firrtl.scm b/tree-sitter/highlights/firrtl.scm new file mode 100644 index 0000000000..551b2e7516 --- /dev/null +++ b/tree-sitter/highlights/firrtl.scm @@ -0,0 +1,189 @@ +; Namespaces + +(circuit (identifier) @namespace) + +(module (identifier) @namespace) + +; Types + +((identifier) @type + (#lua-match? @type "^[A-Z][A-Za-z0-9_$]*$")) + +; Keywords + +[ + "circuit" + "module" + "extmodule" + + "flip" + "parameter" + "reset" + "wire" + + "cmem" + "smem" + "mem" + + "reg" + "with" + "mport" + "inst" + "of" + "node" + "is" + "invalid" + "skip" + + "infer" + "read" + "write" + "rdwr" + + "defname" +] @keyword + +; Qualifiers + +(qualifier) @type.qualifier + +; Storageclasses + +[ + "input" + "output" +] @storageclass + +; Conditionals + +[ + "when" + "else" +] @conditional + +; Annotations + +(info) @attribute + +; Builtins + +[ + "stop" + "printf" + "assert" + "assume" + "cover" + "attach" + "mux" + "validif" +] @function.builtin + +[ + "UInt" + "SInt" + "Analog" + "Fixed" + "Clock" + "AsyncReset" + "Reset" +] @type.builtin + +; Fields + +[ + "data-type" + "depth" + "read-latency" + "write-latency" + "read-under-write" + "reader" + "writer" + "readwriter" +] @field.builtin + +((field_id) @field + (#set! "priority" 105)) + +(port (identifier) @field) + +(wire (identifier) @field) + +(cmem (identifier) @field) + +(smem (identifier) @field) + +(memory (identifier) @field) + +(register (identifier) @field) + +; Parameters + +(primitive_operation (identifier) @parameter) + +(mux (identifier) @parameter) +(printf (identifier) @parameter) +(reset (identifier) @parameter) +(stop (identifier) @parameter) + +; Variables + +(identifier) @variable + +; Operators + +(primop) @keyword.operator + +[ + "+" + "-" + "=" + "=>" + "<=" + "<-" +] @operator + +; Literals + +[ + (uint) + (number) +] @number + +(number_str) @string.special + +(double) @float + +(string) @string + +(escape_sequence) @string.escape + +[ + "old" + "new" + "undefined" +] @constant.builtin + +; Punctuation + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ "<" ">" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +[ + "," + "." + ":" +] @punctuation.delimiter + +; Comments + +(comment) @comment @spell + +["=>" "<=" "="] @operator + +; Error +(ERROR) @error diff --git a/tree-sitter/highlights/fish.scm b/tree-sitter/highlights/fish.scm new file mode 100644 index 0000000000..73ea7cf14b --- /dev/null +++ b/tree-sitter/highlights/fish.scm @@ -0,0 +1,167 @@ +;; Fish highlighting + +;; Operators + +[ + "&&" + "||" + "|" + "&" + ".." + "!" + (direction) + (stream_redirect) +] @operator + +;; match operators of test command +(command + name: (word) @function.builtin (#eq? @function.builtin "test") + argument: (word) @operator (#match? @operator "^(!?\\=|-[a-zA-Z]+)$")) + +;; match operators of [ command +(command + name: (word) @punctuation.bracket (#eq? @punctuation.bracket "[") + argument: (word) @operator (#match? @operator "^(!?\\=|-[a-zA-Z]+)$")) + +[ + "not" + "and" + "or" +] @keyword.operator + +;; Conditionals + +(if_statement +[ + "if" + "end" +] @conditional) + +(switch_statement +[ + "switch" + "end" +] @conditional) + +(case_clause +[ + "case" +] @conditional) + +(else_clause +[ + "else" +] @conditional) + +(else_if_clause +[ + "else" + "if" +] @conditional) + +;; Loops/Blocks + +(while_statement +[ + "while" + "end" +] @repeat) + +(for_statement +[ + "for" + "end" +] @repeat) + +(begin_statement +[ + "begin" + "end" +] @repeat) + +;; Keywords + +[ + "in" + (break) + (continue) +] @keyword + +"return" @keyword.return + +;; Punctuation + +[ + "[" + "]" + "{" + "}" + "(" + ")" +] @punctuation.bracket + +"," @punctuation.delimiter + +;; Commands + +(command + argument: [ + (word) @parameter (#lua-match? @parameter "^[-]") + ] +) + +(command_substitution "$" @punctuation.bracket) + +; non-builtin command names +(command name: (word) @function.call) + +; derived from builtin -n (fish 3.2.2) +(command + name: [ + (word) @function.builtin + (#any-of? @function.builtin "." ":" "_" "alias" "argparse" "bg" "bind" "block" "breakpoint" "builtin" "cd" "command" "commandline" "complete" "contains" "count" "disown" "echo" "emit" "eval" "exec" "exit" "fg" "functions" "history" "isatty" "jobs" "math" "printf" "pwd" "random" "read" "realpath" "set" "set_color" "source" "status" "string" "test" "time" "type" "ulimit" "wait") + ] +) + +;; Functions + +(function_definition ["function" "end"] @keyword.function) + +(function_definition + name: [ + (word) (concatenation) + ] +@function) + +(function_definition + option: [ + (word) + (concatenation (word)) + ] @parameter (#lua-match? @parameter "^[-]") +) + +;; Strings + +[(double_quote_string) (single_quote_string)] @string +(escape_sequence) @string.escape + +;; Variables + +(variable_name) @variable +(variable_expansion) @constant + +;; Nodes + +[(integer) (float)] @number +(comment) @comment +(comment) @spell + +((word) @boolean +(#any-of? @boolean "true" "false")) + +((program . (comment) @preproc) + (#lua-match? @preproc "^#!/")) + +;; Error + +(ERROR) @error diff --git a/tree-sitter/highlights/foam.scm b/tree-sitter/highlights/foam.scm new file mode 100644 index 0000000000..5d45b0a34e --- /dev/null +++ b/tree-sitter/highlights/foam.scm @@ -0,0 +1,61 @@ +;; Comments +(comment) @comment + +;; Generic Key-value pairs and dictionary keywords +(key_value + keyword: (identifier) @function +) +(dict + key: (identifier) @type +) + +;; Macros +(macro + "$" @conditional + (prev_scope)* @conditional + (identifier)* @namespace +) + + +;; Directives +"#" @conditional +(preproc_call + directive: (identifier)* @conditional + argument: (identifier)* @namespace +) +((preproc_call + argument: (identifier)* @namespace) @conditional + (#eq? @conditional "ifeq")) +((preproc_call) @conditional + (#any-of? @conditional "else" "endif")) + +;; Literals +(number_literal) @float +(string_literal) @string +(escape_sequence) @string.escape +(boolean) @boolean + +;; Treat [m^2 s^-2] the same as if it was put in numbers format +(dimensions dimension: (identifier) @float) + +;; Punctuation +[ + "(" + ")" + "[" + "]" + "{" + "}" + "#{" + "#}" + "|-" + "-|" + "" + "$$" +] @punctuation.bracket + +";" @punctuation.delimiter + +((identifier) @constant.builtin + (#any-of? @constant.builtin "uniform" "non-uniform" "and" "or")) diff --git a/tree-sitter/highlights/fortran.scm b/tree-sitter/highlights/fortran.scm new file mode 100644 index 0000000000..a6b55413bc --- /dev/null +++ b/tree-sitter/highlights/fortran.scm @@ -0,0 +1,332 @@ +; Preprocs + +(preproc_file_line) @preproc + +; Namespaces + +(program_statement + (name) @namespace) + +(end_program_statement + (name) @namespace) + +(module_statement + (name) @namespace) + +(end_module_statement + (name) @namespace) + +(submodule_statement + (name) @namespace) + +(end_submodule_statement + (name) @namespace) + +; Includes + +[ + "import" + "include" + "use" +] @include + +(import_statement + "," + ["all" "none"] @keyword) + +; Attributes + +[ + (none) + "implicit" + "intent" +] @attribute + +(implicit_statement + "type" @attribute) + +; Keywords + +[ + "attributes" + "associate" + "block" + "class" + "classis" + "contains" + "default" + "dimension" + "endassociate" + "endenum" + "endinterface" + "endmodule" + "endselect" + "endsubmodule" + "endtype" + "enum" + "enumerator" + "equivalence" + "extends" + "goto" + "interface" + "intrinsic" + "non_intrinsic" + "module" + "submodule" + "namelist" + "parameter" + "quiet" + "rank" + "save" + "selectcase" + "selectrank" + "selecttype" + "sequence" + "stop" + "target" + "type" + "typeis" +] @keyword + +[ + (default) +] @keyword + +; Types + +[ + (type_name) +] @type + +[ + (intrinsic_type) +] @type.builtin + +; Qualifiers + +[ + "abstract" + "allocatable" + "automatic" + "constant" + "contiguous" + "data" + "deferred" + "device" + "external" + "family" + "final" + "generic" + "global" + "grid_global" + "host" + "initial" + "local" + "local_init" + "managed" + "nopass" + "non_overridable" + "optional" + "pass" + "pinned" + "pointer" + "private" + "property" + "protected" + "public" + "shared" + "static" + "texture" + "value" + "volatile" + (procedure_qualifier) +] @type.qualifier + +[ + "common" + "in" + "inout" + "out" +] @storageclass + +; Labels + +[ + (statement_label) + (statement_label_reference) +] @label + +[ + "call" + "endfunction" + "endprogram" + "endprocedure" + "endsubroutine" + "function" + "procedure" + "program" + "subroutine" +] @keyword.function + +[ + "result" + "return" +] @keyword.return + +; Functions + +(function_statement + (name) @function) + +(end_function_statement + (name) @function) + +(subroutine_statement + (name) @function) + +(end_subroutine_statement + (name) @function) + +(module_procedure_statement + (name) @function) + +(end_module_procedure_statement + (name) @function) + +(subroutine_call + (identifier) @function.call) + +[ + "character" + "close" + "bind" + "format" + "open" + "print" + "read" + "write" +] @function.builtin + +; Exceptions + +[ + "error" +] @exception + +; Conditionals + +[ + "else" + "elseif" + "elsewhere" + "endif" + "endwhere" + "if" + "then" + "where" +] @conditional + +; Repeats + +[ + "do" + "concurrent" + "enddo" + "endforall" + "forall" + "while" + "continue" + "cycle" + "exit" +] @repeat + +; Variables + +(identifier) @variable + +; Parameters + +(keyword_argument + name: (identifier) @parameter) + +(parameters + (identifier) @parameter) + +; Properties + +(derived_type_member_expression + (type_member) @property) + +; Operators + +[ + "+" + "-" + "*" + "**" + "/" + "=" + "<" + ">" + "<=" + ">=" + "==" + "/=" + "//" + (assumed_rank) +] @operator + +[ + "\\.and\\." + "\\.or\\." + "\\.eqv\\." + "\\.neqv\\." + "\\.lt\\." + "\\.gt\\." + "\\.le\\." + "\\.ge\\." + "\\.eq\\." + "\\.ne\\." + "\\.not\\." +] @keyword.operator + +; Punctuation + +[ "[" "]" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +[ "<<<" ">>>" ] @punctuation.bracket + +(array_literal + ["(/" "/)"] @punctuation.bracket) + +[ + ":" + "," + "/" + "%" + "::" + "=>" +] @punctuation.delimiter + +; Literals + +(string_literal) @string + +(number_literal) @number + +(boolean_literal) @boolean + +(null_literal) @constant.builtin + +; Comments + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^!>")) + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/fsh.scm b/tree-sitter/highlights/fsh.scm new file mode 100644 index 0000000000..ccc93fc56b --- /dev/null +++ b/tree-sitter/highlights/fsh.scm @@ -0,0 +1,91 @@ +[ + "(" + ")" +] @punctuation.bracket + +[ + "^" + "=" + ":" +] @operator + +[ + "#" + ".." + "*" + "->" +] @punctuation.special + +; Entities +[ + "Profile" + "Alias" + "Extension" + "Invariant" + "Instance" + "ValueSet" + "CodeSystem" + "Mapping" + "Logical" + "Resource" + "RuleSet" +] @keyword + +; Metadata Keywords +[ + "Parent" + "Title" + "Description" + "Id" + "Severity" + "InstanceOf" + "Usage" + "Source" + "XPath" + "Target" +] @keyword + +; Rule Keywords +[ + "contentReference" + "insert" + "and" + "or" + "contains" + "named" + "only" + "obeys" + "valueset" + "codes" + "from" + "include" + "exclude" + "where" + "system" + "exactly" +] @keyword.operator + +; Types +[ + "Reference" + "Canonical" +] @type.builtin + + +(sd_metadata (parent (name))) @type +(target_type (name)) @type + +; Strings +(string) @string @spell +(multiline_string) @string @spell + +; Constants +(strength_value) @constant +(bool) @constant.boolean +(flag) @constant + +; Special Params +(code_value) @parameter + +; Extras +(fsh_comment) @comment @spell diff --git a/tree-sitter/highlights/func.scm b/tree-sitter/highlights/func.scm new file mode 100644 index 0000000000..c1662a1807 --- /dev/null +++ b/tree-sitter/highlights/func.scm @@ -0,0 +1,175 @@ +; Include + +"#include" @include +(include_path) @string + +; Preproc + +[ + "#pragma" +] @preproc + +(pragma_directive + [ + "version" + "not-version" + "test-version-set" + ] @preproc) + +; Keywords + +[ + "asm" + "impure" + "inline" + "inline_ref" + "method_id" + "type" +] @keyword + +[ + "return" +] @keyword.return + +; Conditionals + +[ + "if" + "ifnot" + "else" + "elseif" + "elseifnot" + "until" +] @conditional + +; Exceptions + +[ + "try" + "catch" +] @exception + +; Repeats + +[ + "do" + "forall" + "repeat" + "while" +] @repeat + +; Qualifiers +[ + "const" + "global" + (var) +] @type.qualifier + +; Variables + +(identifier) @variable + +; Constants + +(const_var_declarations + name: (identifier) @constant) + +; Functions/Methods + +(function_definition + name: (function_name) @function) + +(function_application + function: (identifier) @function) + +(method_call + method_name: (identifier) @method.call) + +; Parameters + +(parameter) @parameter + +; Types + +(type_identifier) @type + +(primitive_type) @type.builtin + +; Operators + +[ + "=" + "+=" + "-=" + "*=" + "/=" + "~/=" + "^/=" + "%=" + "~%=" + "^%=" + "<<=" + ">>=" + "~>>=" + "^>>=" + "&=" + "|=" + "^=" + "==" + "<" + ">" + "<=" + ">=" + "!=" + "<=>" + "<<" + ">>" + "~>>" + "^>>" + "-" + "+" + "|" + "^" + "*" + "/" + "%" + "~/" + "^/" + "~%" + "^%" + "/%" + "&" + "~" +] @operator + +; Literals + +[ + (string) + (asm_instruction) +] @string + +[ + (string_type) + (underscore) +] @character.special + +(number) @number + +; Punctuation + +["{" "}"] @punctuation.bracket + +["(" ")" "()"] @punctuation.bracket + +["[" "]"] @punctuation.bracket + +[ + ";" + "," + "->" +] @punctuation.delimiter + +; Comments + +(comment) @comment @spell diff --git a/tree-sitter/highlights/fusion.scm b/tree-sitter/highlights/fusion.scm new file mode 100644 index 0000000000..bb24a413fe --- /dev/null +++ b/tree-sitter/highlights/fusion.scm @@ -0,0 +1,120 @@ +(comment) @comment +(afx_comment) @comment + +; identifiers afx +(afx_opening_element + (afx_identifier) @tag) +(afx_closing_element + (afx_identifier) @tag) +(afx_element_self_closing + (afx_identifier) @tag) + +(afx_attribute + (afx_property_identifier) @tag.attribute) + +(afx_text) @text + +; identifiers eel + +(eel_object_path + (eel_path_identifier) @variable.builtin + (#any-of? @variable.builtin "this" "props") +) + +(eel_object_path + (eel_path_identifier) @variable) + +(eel_object_pair + key: (eel_property_name) @property) + +(eel_method_name) @function + +(eel_parameter) @variable + +; identifiers fusion +; ----------- + +(path_part) @property +(meta_property) @attribute +(prototype_signature + "prototype" @keyword + +) +(include_statement + [ + "include" + ] @include + (source_file) @text.uri +) + +(namespace_declaration + "namespace" @keyword + (alias_namespace) @namespace) + +(type + name: (type_name) @type) + +; tokens +; ------ +(afx_opening_element + [ + "<" + ">" + ] @punctuation.bracket) + + (afx_closing_element + [ + "<" + ">" + "/" + ] @punctuation.bracket) + +(afx_element_self_closing + [ + "<" + "/>" + ] @punctuation.bracket) + +[ + (package_name) + (alias_namespace) +] @namespace + +(namespace_declaration "=" @operator) +(assignment "=" @operator) +(copy "<" @operator) +(deletion) @operator +(eel_binary_expression + operator: _ @operator) +(eel_not_expression + [ + "!" + "not" + ] @operator) + +(string) @string +(number) @number +(boolean) @boolean +(null) @constant.builtin + +(value_expression + start: _ @punctuation.special + end: _ @punctuation.special +) +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + +[ + ":" + "." + "?" +] @punctuation.delimiter + +(eel_ternary_expression + ["?" ":"] @conditional.ternary) diff --git a/tree-sitter/highlights/gdscript.scm b/tree-sitter/highlights/gdscript.scm new file mode 100644 index 0000000000..4d5221e93d --- /dev/null +++ b/tree-sitter/highlights/gdscript.scm @@ -0,0 +1,343 @@ +;; Basic +(ERROR) @error + +(identifier) @variable +(name) @variable +(type) @type +(comment) @comment @spell +(string_name) @string +(string) @string +(float) @float +(integer) @number +(null) @constant +(setter) @function +(getter) @function +(set_body "set" @keyword.function) +(get_body "get" @keyword.function) +(static_keyword) @type.qualifier +(tool_statement) @keyword +(breakpoint_statement) @debug +(inferred_type) @operator +[(true) (false)] @boolean + +[ + (get_node) + (node_path) +] @text.uri + +(class_name_statement + (name) @type) @keyword + +(const_statement + "const" @type.qualifier + (name) @constant) + +(expression_statement (string) @comment @spell) + +;; Identifier naming conventions +((identifier) @type + (#lua-match? @type "^[A-Z]")) +((identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) + +;; Functions +(constructor_definition) @constructor + +(function_definition + (name) @function (parameters + (identifier) @parameter)*) + +(typed_parameter (identifier) @parameter) +(default_parameter (identifier) @parameter) + +(call (identifier) @function.call) +(call (identifier) @include + (#any-of? @include "preload" "load")) + +;; Properties and Methods + +; We'll use @property since that's the term Godot uses. +; But, should (source (variable_statement (name))) be @property, too? Since a +; script file is a class in gdscript. +(class_definition + (body (variable_statement (name) @property))) + +; Same question but for methods? +(class_definition + (body (function_definition (name) @method))) + +(attribute_call (identifier) @method.call) +(attribute (_) (identifier) @property) + +;; Enums + +(enumerator left: (identifier) @constant) + +;; Special Builtins + +((identifier) @variable.builtin + (#any-of? @variable.builtin "self" "super")) + +(attribute_call (identifier) @keyword.operator + (#eq? @keyword.operator "new")) + +;; Match Pattern + +(underscore) @constant ; The "_" pattern. +(pattern_open_ending) @operator ; The ".." pattern. + +;; Alternations +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + +["," "." ":"] @punctuation.delimiter + +["if" "elif" "else" "match"] @conditional + +["for" "while" "break" "continue"] @repeat + +[ + "~" + "-" + "*" + "/" + "%" + "+" + "-" + "<<" + ">>" + "&" + "^" + "|" + "<" + ">" + "==" + "!=" + ">=" + "<=" + "!" + "&&" + "||" + "=" + "+=" + "-=" + "*=" + "/=" + "%=" + "&=" + "|=" + "->" +] @operator + +[ + "and" + "as" + "in" + "is" + "not" + "or" +] @keyword.operator + +[ + "pass" + "class" + "class_name" + "extends" + "signal" + "enum" + "var" + "onready" + "export" + "setget" + "remote" + "master" + "puppet" + "remotesync" + "mastersync" + "puppetsync" +] @keyword + +"func" @keyword.function + +[ + "return" +] @keyword.return + +[ + "await" +] @keyword.coroutine + +(call (identifier) @keyword.coroutine + (#eq? @keyword.coroutine "yield")) + + +;; Builtins +; generated from +; - https://github.com/godotengine/godot/blob/491ded18983a4ae963ce9c29e8df5d5680873ccb/doc/classes/@GlobalScope.xml +; - https://github.com/godotengine/godot/blob/491ded18983a4ae963ce9c29e8df5d5680873ccb/modules/gdscript/doc_classes/@GDScript.xml +; some from: +; - https://github.com/godotengine/godot-vscode-plugin/blob/0636797c22bf1e23a41fd24d55cdb9be62e0c992/syntaxes/GDScript.tmLanguage.json + +;; Built-in Annotations + +((annotation "@" @attribute (identifier) @attribute) + (#any-of? @attribute + ; @GDScript + "export" "export_category" "export_color_no_alpha" "export_dir" + "export_enum" "export_exp_easing" "export_file" "export_flags" + "export_flags_2d_navigation" "export_flags_2d_physics" + "export_flags_2d_render" "export_flags_3d_navigation" + "export_flags_3d_physics" "export_flags_3d_render" "export_global_dir" + "export_global_file" "export_group" "export_multiline" "export_node_path" + "export_placeholder" "export_range" "export_subgroup" "icon" "onready" + "rpc" "tool" "warning_ignore" + )) + +;; Builtin Types + +([(identifier) (type)] @type.builtin + (#any-of? @type.builtin + ; from godot-vscode-plugin + "Vector2" "Vector2i" "Vector3" "Vector3i" + "Color" "Rect2" "Rect2i" "Array" "Basis" "Dictionary" + "Plane" "Quat" "RID" "Rect3" "Transform" "Transform2D" + "Transform3D" "AABB" "String" "NodePath" "Object" + "PoolByteArray" "PoolIntArray" "PoolRealArray" + "PoolStringArray" "PoolVector2Array" "PoolVector3Array" + "PoolColorArray" "bool" "int" "float" "StringName" "Quaternion" + "PackedByteArray" "PackedInt32Array" "PackedInt64Array" + "PackedFloat32Array" "PackedFloat64Array" "PackedStringArray" + "PackedVector2Array" "PackedVector2iArray" "PackedVector3Array" + "PackedVector3iArray" "PackedColorArray" + + ; @GlobalScope + "AudioServer" "CameraServer" "ClassDB" "DisplayServer" "Engine" + "EngineDebugger" "GDExtensionManager" "Geometry2D" "Geometry3D" "GodotSharp" + "IP" "Input" "InputMap" "JavaClassWrapper" "JavaScriptBridge" "Marshalls" + "NavigationMeshGenerator" "NavigationServer2D" "NavigationServer3D" "OS" + "Performance" "PhysicsServer2D" "PhysicsServer2DManager" "PhysicsServer3D" + "PhysicsServer3DManager" "ProjectSettings" "RenderingServer" "ResourceLoader" + "ResourceSaver" "ResourceUID" "TextServerManager" "ThemeDB" "Time" + "TranslationServer" "WorkerThreadPool" "XRServer" + )) + +;; Builtin Funcs + +(call (identifier) @function.builtin + (#any-of? @function.builtin + ; @GlobalScope + "abs" "absf" "absi" "acos" "asin" "atan" "atan2" "bezier_derivative" + "bezier_interpolate" "bytes_to_var" "bytes_to_var_with_objects" "ceil" "ceilf" + "ceili" "clamp" "clampf" "clampi" "cos" "cosh" "cubic_interpolate" + "cubic_interpolate_angle" "cubic_interpolate_angle_in_time" + "cubic_interpolate_in_time" "db_to_linear" "deg_to_rad" "ease" "error_string" + "exp" "floor" "floorf" "floori" "fmod" "fposmod" "hash" "instance_from_id" + "inverse_lerp" "is_equal_approx" "is_finite" "is_inf" "is_instance_id_valid" + "is_instance_valid" "is_nan" "is_same" "is_zero_approx" "lerp" "lerp_angle" + "lerpf" "linear_to_db" "log" "max" "maxf" "maxi" "min" "minf" "mini" + "move_toward" "nearest_po2" "pingpong" "posmod" "pow" "print" "print_rich" + "print_verbose" "printerr" "printraw" "prints" "printt" "push_error" + "push_warning" "rad_to_deg" "rand_from_seed" "randf" "randf_range" "randfn" + "randi" "randi_range" "randomize" "remap" "rid_allocate_id" "rid_from_int64" + "round" "roundf" "roundi" "seed" "sign" "signf" "signi" "sin" "sinh" + "smoothstep" "snapped" "snappedf" "snappedi" "sqrt" "step_decimals" "str" + "str_to_var" "tan" "tanh" "typeof" "var_to_bytes" "var_to_bytes_with_objects" + "var_to_str" "weakref" "wrap" "wrapf" "wrapi" + + ; @GDScript + "Color8" "assert" "char" "convert" "dict_to_inst" "get_stack" "inst_to_dict" + "is_instance_of" "len" "print_debug" "print_stack" "range" + "type_exists" + )) + +;; Builtin Constants + +((identifier) @constant.builtin + (#any-of? @constant.builtin + ; @GDScript + "PI" "TAU" "INF" "NAN" + + ; @GlobalScope + "SIDE_LEFT" "SIDE_TOP" "SIDE_RIGHT" "SIDE_BOTTOM" "CORNER_TOP_LEFT" "CORNER_TOP_RIGHT" "CORNER_BOTTOM_RIGHT" + "CORNER_BOTTOM_LEFT" "VERTICAL" "HORIZONTAL" "CLOCKWISE" "COUNTERCLOCKWISE" "HORIZONTAL_ALIGNMENT_LEFT" + "HORIZONTAL_ALIGNMENT_CENTER" "HORIZONTAL_ALIGNMENT_RIGHT" "HORIZONTAL_ALIGNMENT_FILL" "VERTICAL_ALIGNMENT_TOP" + "VERTICAL_ALIGNMENT_CENTER" "VERTICAL_ALIGNMENT_BOTTOM" "VERTICAL_ALIGNMENT_FILL" "INLINE_ALIGNMENT_TOP_TO" + "INLINE_ALIGNMENT_CENTER_TO" "INLINE_ALIGNMENT_BASELINE_TO" "INLINE_ALIGNMENT_BOTTOM_TO" "INLINE_ALIGNMENT_TO_TOP" + "INLINE_ALIGNMENT_TO_CENTER" "INLINE_ALIGNMENT_TO_BASELINE" "INLINE_ALIGNMENT_TO_BOTTOM" "INLINE_ALIGNMENT_TOP" + "INLINE_ALIGNMENT_CENTER" "INLINE_ALIGNMENT_BOTTOM" "INLINE_ALIGNMENT_IMAGE_MASK" "INLINE_ALIGNMENT_TEXT_MASK" + "EULER_ORDER_XYZ" "EULER_ORDER_XZY" "EULER_ORDER_YXZ" "EULER_ORDER_YZX" "EULER_ORDER_ZXY" "EULER_ORDER_ZYX" "KEY_NONE" + "KEY_SPECIAL" "KEY_ESCAPE" "KEY_TAB" "KEY_BACKTAB" "KEY_BACKSPACE" "KEY_ENTER" "KEY_KP_ENTER" "KEY_INSERT" "KEY_DELETE" + "KEY_PAUSE" "KEY_PRINT" "KEY_SYSREQ" "KEY_CLEAR" "KEY_HOME" "KEY_END" "KEY_LEFT" "KEY_UP" "KEY_RIGHT" "KEY_DOWN" + "KEY_PAGEUP" "KEY_PAGEDOWN" "KEY_SHIFT" "KEY_CTRL" "KEY_META" "KEY_ALT" "KEY_CAPSLOCK" "KEY_NUMLOCK" "KEY_SCROLLLOCK" + "KEY_F1" "KEY_F2" "KEY_F3" "KEY_F4" "KEY_F5" "KEY_F6" "KEY_F7" "KEY_F8" "KEY_F9" "KEY_F10" "KEY_F11" "KEY_F12" + "KEY_F13" "KEY_F14" "KEY_F15" "KEY_F16" "KEY_F17" "KEY_F18" "KEY_F19" "KEY_F20" "KEY_F21" "KEY_F22" "KEY_F23" "KEY_F24" + "KEY_F25" "KEY_F26" "KEY_F27" "KEY_F28" "KEY_F29" "KEY_F30" "KEY_F31" "KEY_F32" "KEY_F33" "KEY_F34" "KEY_F35" + "KEY_KP_MULTIPLY" "KEY_KP_DIVIDE" "KEY_KP_SUBTRACT" "KEY_KP_PERIOD" "KEY_KP_ADD" "KEY_KP_0" "KEY_KP_1" "KEY_KP_2" + "KEY_KP_3" "KEY_KP_4" "KEY_KP_5" "KEY_KP_6" "KEY_KP_7" "KEY_KP_8" "KEY_KP_9" "KEY_MENU" "KEY_HYPER" "KEY_HELP" + "KEY_BACK" "KEY_FORWARD" "KEY_STOP" "KEY_REFRESH" "KEY_VOLUMEDOWN" "KEY_VOLUMEMUTE" "KEY_VOLUMEUP" "KEY_MEDIAPLAY" + "KEY_MEDIASTOP" "KEY_MEDIAPREVIOUS" "KEY_MEDIANEXT" "KEY_MEDIARECORD" "KEY_HOMEPAGE" "KEY_FAVORITES" "KEY_SEARCH" + "KEY_STANDBY" "KEY_OPENURL" "KEY_LAUNCHMAIL" "KEY_LAUNCHMEDIA" "KEY_LAUNCH0" "KEY_LAUNCH1" "KEY_LAUNCH2" "KEY_LAUNCH3" + "KEY_LAUNCH4" "KEY_LAUNCH5" "KEY_LAUNCH6" "KEY_LAUNCH7" "KEY_LAUNCH8" "KEY_LAUNCH9" "KEY_LAUNCHA" "KEY_LAUNCHB" + "KEY_LAUNCHC" "KEY_LAUNCHD" "KEY_LAUNCHE" "KEY_LAUNCHF" "KEY_UNKNOWN" "KEY_SPACE" "KEY_EXCLAM" "KEY_QUOTEDBL" + "KEY_NUMBERSIGN" "KEY_DOLLAR" "KEY_PERCENT" "KEY_AMPERSAND" "KEY_APOSTROPHE" "KEY_PARENLEFT" "KEY_PARENRIGHT" + "KEY_ASTERISK" "KEY_PLUS" "KEY_COMMA" "KEY_MINUS" "KEY_PERIOD" "KEY_SLASH" "KEY_0" "KEY_1" "KEY_2" "KEY_3" "KEY_4" + "KEY_5" "KEY_6" "KEY_7" "KEY_8" "KEY_9" "KEY_COLON" "KEY_SEMICOLON" "KEY_LESS" "KEY_EQUAL" "KEY_GREATER" "KEY_QUESTION" + "KEY_AT" "KEY_A" "KEY_B" "KEY_C" "KEY_D" "KEY_E" "KEY_F" "KEY_G" "KEY_H" "KEY_I" "KEY_J" "KEY_K" "KEY_L" "KEY_M" + "KEY_N" "KEY_O" "KEY_P" "KEY_Q" "KEY_R" "KEY_S" "KEY_T" "KEY_U" "KEY_V" "KEY_W" "KEY_X" "KEY_Y" "KEY_Z" + "KEY_BRACKETLEFT" "KEY_BACKSLASH" "KEY_BRACKETRIGHT" "KEY_ASCIICIRCUM" "KEY_UNDERSCORE" "KEY_QUOTELEFT" "KEY_BRACELEFT" + "KEY_BAR" "KEY_BRACERIGHT" "KEY_ASCIITILDE" "KEY_YEN" "KEY_SECTION" "KEY_GLOBE" "KEY_KEYBOARD" "KEY_JIS_EISU" + "KEY_JIS_KANA" "KEY_CODE_MASK" "KEY_MODIFIER_MASK" "KEY_MASK_CMD_OR_CTRL" "KEY_MASK_SHIFT" "KEY_MASK_ALT" + "KEY_MASK_META" "KEY_MASK_CTRL" "KEY_MASK_KPAD" "KEY_MASK_GROUP_SWITCH" "MOUSE_BUTTON_NONE" "MOUSE_BUTTON_LEFT" + "MOUSE_BUTTON_RIGHT" "MOUSE_BUTTON_MIDDLE" "MOUSE_BUTTON_WHEEL_UP" "MOUSE_BUTTON_WHEEL_DOWN" "MOUSE_BUTTON_WHEEL_LEFT" + "MOUSE_BUTTON_WHEEL_RIGHT" "MOUSE_BUTTON_XBUTTON1" "MOUSE_BUTTON_XBUTTON2" "MOUSE_BUTTON_MASK_LEFT" + "MOUSE_BUTTON_MASK_RIGHT" "MOUSE_BUTTON_MASK_MIDDLE" "MOUSE_BUTTON_MASK_MB_XBUTTON1" "MOUSE_BUTTON_MASK_MB_XBUTTON2" + "JOY_BUTTON_INVALID" "JOY_BUTTON_A" "JOY_BUTTON_B" "JOY_BUTTON_X" "JOY_BUTTON_Y" "JOY_BUTTON_BACK" "JOY_BUTTON_GUIDE" + "JOY_BUTTON_START" "JOY_BUTTON_LEFT_STICK" "JOY_BUTTON_RIGHT_STICK" "JOY_BUTTON_LEFT_SHOULDER" + "JOY_BUTTON_RIGHT_SHOULDER" "JOY_BUTTON_DPAD_UP" "JOY_BUTTON_DPAD_DOWN" "JOY_BUTTON_DPAD_LEFT" "JOY_BUTTON_DPAD_RIGHT" + "JOY_BUTTON_MISC1" "JOY_BUTTON_PADDLE1" "JOY_BUTTON_PADDLE2" "JOY_BUTTON_PADDLE3" "JOY_BUTTON_PADDLE4" + "JOY_BUTTON_TOUCHPAD" "JOY_BUTTON_SDL_MAX" "JOY_BUTTON_MAX" "JOY_AXIS_INVALID" "JOY_AXIS_LEFT_X" "JOY_AXIS_LEFT_Y" + "JOY_AXIS_RIGHT_X" "JOY_AXIS_RIGHT_Y" "JOY_AXIS_TRIGGER_LEFT" "JOY_AXIS_TRIGGER_RIGHT" "JOY_AXIS_SDL_MAX" + "JOY_AXIS_MAX" "MIDI_MESSAGE_NONE" "MIDI_MESSAGE_NOTE_OFF" "MIDI_MESSAGE_NOTE_ON" "MIDI_MESSAGE_AFTERTOUCH" + "MIDI_MESSAGE_CONTROL_CHANGE" "MIDI_MESSAGE_PROGRAM_CHANGE" "MIDI_MESSAGE_CHANNEL_PRESSURE" "MIDI_MESSAGE_PITCH_BEND" + "MIDI_MESSAGE_SYSTEM_EXCLUSIVE" "MIDI_MESSAGE_QUARTER_FRAME" "MIDI_MESSAGE_SONG_POSITION_POINTER" + "MIDI_MESSAGE_SONG_SELECT" "MIDI_MESSAGE_TUNE_REQUEST" "MIDI_MESSAGE_TIMING_CLOCK" "MIDI_MESSAGE_START" + "MIDI_MESSAGE_CONTINUE" "MIDI_MESSAGE_STOP" "MIDI_MESSAGE_ACTIVE_SENSING" "MIDI_MESSAGE_SYSTEM_RESET" "OK" "FAILED" + "ERR_UNAVAILABLE" "ERR_UNCONFIGURED" "ERR_UNAUTHORIZED" "ERR_PARAMETER_RANGE_ERROR" "ERR_OUT_OF_MEMORY" + "ERR_FILE_NOT_FOUND" "ERR_FILE_BAD_DRIVE" "ERR_FILE_BAD_PATH" "ERR_FILE_NO_PERMISSION" "ERR_FILE_ALREADY_IN_USE" + "ERR_FILE_CANT_OPEN" "ERR_FILE_CANT_WRITE" "ERR_FILE_CANT_READ" "ERR_FILE_UNRECOGNIZED" "ERR_FILE_CORRUPT" + "ERR_FILE_MISSING_DEPENDENCIES" "ERR_FILE_EOF" "ERR_CANT_OPEN" "ERR_CANT_CREATE" "ERR_QUERY_FAILED" + "ERR_ALREADY_IN_USE" "ERR_LOCKED" "ERR_TIMEOUT" "ERR_CANT_CONNECT" "ERR_CANT_RESOLVE" "ERR_CONNECTION_ERROR" + "ERR_CANT_ACQUIRE_RESOURCE" "ERR_CANT_FORK" "ERR_INVALID_DATA" "ERR_INVALID_PARAMETER" "ERR_ALREADY_EXISTS" + "ERR_DOES_NOT_EXIST" "ERR_DATABASE_CANT_READ" "ERR_DATABASE_CANT_WRITE" "ERR_COMPILATION_FAILED" "ERR_METHOD_NOT_FOUND" + "ERR_LINK_FAILED" "ERR_SCRIPT_FAILED" "ERR_CYCLIC_LINK" "ERR_INVALID_DECLARATION" "ERR_DUPLICATE_SYMBOL" + "ERR_PARSE_ERROR" "ERR_BUSY" "ERR_SKIP" "ERR_HELP" "ERR_BUG" "ERR_PRINTER_ON_FIRE" "PROPERTY_HINT_NONE" + "PROPERTY_HINT_RANGE" "PROPERTY_HINT_ENUM" "PROPERTY_HINT_ENUM_SUGGESTION" "PROPERTY_HINT_EXP_EASING" + "PROPERTY_HINT_LINK" "PROPERTY_HINT_FLAGS" "PROPERTY_HINT_LAYERS_2D_RENDER" "PROPERTY_HINT_LAYERS_2D_PHYSICS" + "PROPERTY_HINT_LAYERS_2D_NAVIGATION" "PROPERTY_HINT_LAYERS_3D_RENDER" "PROPERTY_HINT_LAYERS_3D_PHYSICS" + "PROPERTY_HINT_LAYERS_3D_NAVIGATION" "PROPERTY_HINT_FILE" "PROPERTY_HINT_DIR" "PROPERTY_HINT_GLOBAL_FILE" + "PROPERTY_HINT_GLOBAL_DIR" "PROPERTY_HINT_RESOURCE_TYPE" "PROPERTY_HINT_MULTILINE_TEXT" "PROPERTY_HINT_EXPRESSION" + "PROPERTY_HINT_PLACEHOLDER_TEXT" "PROPERTY_HINT_COLOR_NO_ALPHA" "PROPERTY_HINT_OBJECT_ID" "PROPERTY_HINT_TYPE_STRING" + "PROPERTY_HINT_NODE_PATH_TO_EDITED_NODE" "PROPERTY_HINT_OBJECT_TOO_BIG" "PROPERTY_HINT_NODE_PATH_VALID_TYPES" + "PROPERTY_HINT_SAVE_FILE" "PROPERTY_HINT_GLOBAL_SAVE_FILE" "PROPERTY_HINT_INT_IS_OBJECTID" + "PROPERTY_HINT_INT_IS_POINTER" "PROPERTY_HINT_ARRAY_TYPE" "PROPERTY_HINT_LOCALE_ID" "PROPERTY_HINT_LOCALIZABLE_STRING" + "PROPERTY_HINT_NODE_TYPE" "PROPERTY_HINT_HIDE_QUATERNION_EDIT" "PROPERTY_HINT_PASSWORD" "PROPERTY_HINT_MAX" + "PROPERTY_USAGE_NONE" "PROPERTY_USAGE_STORAGE" "PROPERTY_USAGE_EDITOR" "PROPERTY_USAGE_INTERNAL" + "PROPERTY_USAGE_CHECKABLE" "PROPERTY_USAGE_CHECKED" "PROPERTY_USAGE_GROUP" "PROPERTY_USAGE_CATEGORY" + "PROPERTY_USAGE_SUBGROUP" "PROPERTY_USAGE_CLASS_IS_BITFIELD" "PROPERTY_USAGE_NO_INSTANCE_STATE" + "PROPERTY_USAGE_RESTART_IF_CHANGED" "PROPERTY_USAGE_SCRIPT_VARIABLE" "PROPERTY_USAGE_STORE_IF_NULL" + "PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED" "PROPERTY_USAGE_SCRIPT_DEFAULT_VALUE" "PROPERTY_USAGE_CLASS_IS_ENUM" + "PROPERTY_USAGE_NIL_IS_VARIANT" "PROPERTY_USAGE_ARRAY" "PROPERTY_USAGE_ALWAYS_DUPLICATE" + "PROPERTY_USAGE_NEVER_DUPLICATE" "PROPERTY_USAGE_HIGH_END_GFX" "PROPERTY_USAGE_NODE_PATH_FROM_SCENE_ROOT" + "PROPERTY_USAGE_RESOURCE_NOT_PERSISTENT" "PROPERTY_USAGE_KEYING_INCREMENTS" "PROPERTY_USAGE_DEFERRED_SET_RESOURCE" + "PROPERTY_USAGE_EDITOR_INSTANTIATE_OBJECT" "PROPERTY_USAGE_EDITOR_BASIC_SETTING" "PROPERTY_USAGE_READ_ONLY" + "PROPERTY_USAGE_DEFAULT" "PROPERTY_USAGE_NO_EDITOR" "METHOD_FLAG_NORMAL" "METHOD_FLAG_EDITOR" "METHOD_FLAG_CONST" + "METHOD_FLAG_VIRTUAL" "METHOD_FLAG_VARARG" "METHOD_FLAG_STATIC" "METHOD_FLAG_OBJECT_CORE" "METHOD_FLAGS_DEFAULT" + "TYPE_NIL" "TYPE_BOOL" "TYPE_INT" "TYPE_FLOAT" "TYPE_STRING" "TYPE_VECTOR2" "TYPE_VECTOR2I" "TYPE_RECT2" "TYPE_RECT2I" + "TYPE_VECTOR3" "TYPE_VECTOR3I" "TYPE_TRANSFORM2D" "TYPE_VECTOR4" "TYPE_VECTOR4I" "TYPE_PLANE" "TYPE_QUATERNION" + "TYPE_AABB" "TYPE_BASIS" "TYPE_TRANSFORM3D" "TYPE_PROJECTION" "TYPE_COLOR" "TYPE_STRING_NAME" "TYPE_NODE_PATH" + "TYPE_RID" "TYPE_OBJECT" "TYPE_CALLABLE" "TYPE_SIGNAL" "TYPE_DICTIONARY" "TYPE_ARRAY" "TYPE_PACKED_BYTE_ARRAY" + "TYPE_PACKED_INT32_ARRAY" "TYPE_PACKED_INT64_ARRAY" "TYPE_PACKED_FLOAT32_ARRAY" "TYPE_PACKED_FLOAT64_ARRAY" + "TYPE_PACKED_STRING_ARRAY" "TYPE_PACKED_VECTOR2_ARRAY" "TYPE_PACKED_VECTOR3_ARRAY" "TYPE_PACKED_COLOR_ARRAY" "TYPE_MAX" + "OP_EQUAL" "OP_NOT_EQUAL" "OP_LESS" "OP_LESS_EQUAL" "OP_GREATER" "OP_GREATER_EQUAL" "OP_ADD" "OP_SUBTRACT" + "OP_MULTIPLY" "OP_DIVIDE" "OP_NEGATE" "OP_POSITIVE" "OP_MODULE" "OP_POWER" "OP_SHIFT_LEFT" "OP_SHIFT_RIGHT" + "OP_BIT_AND" "OP_BIT_OR" "OP_BIT_XOR" "OP_BIT_NEGATE" "OP_AND" "OP_OR" "OP_XOR" "OP_NOT" "OP_IN" "OP_MAX" + )) diff --git a/tree-sitter/highlights/git_config.scm b/tree-sitter/highlights/git_config.scm new file mode 100644 index 0000000000..18b75bc643 --- /dev/null +++ b/tree-sitter/highlights/git_config.scm @@ -0,0 +1,49 @@ +; Sections + +(section_name) @type + +((section_name) @include + (#eq? @include "include")) + +((section_header + (section_name) @include + (subsection_name)) + (#eq? @include "includeIf")) + +(variable (name) @property) + +; Operators + +[ + "=" +] @operator + +; Literals + +(integer) @number +[ + (true) + (false) +] @boolean + +(string) @string + +((string) @text.uri + (#lua-match? @text.uri "^[.]?[/]")) + +((string) @text.uri + (#lua-match? @text.uri "^[~]")) + +(section_header + [ + "\"" + (subsection_name) + ] @string.special) + +; Punctuation + +[ "[" "]" ] @punctuation.bracket + +; Comments + +(comment) @comment @spell diff --git a/tree-sitter/highlights/git_rebase.scm b/tree-sitter/highlights/git_rebase.scm new file mode 100644 index 0000000000..e20dbec8a2 --- /dev/null +++ b/tree-sitter/highlights/git_rebase.scm @@ -0,0 +1,7 @@ +((command) @keyword + (label)? @constant + (message)? @text @spell) + +(option) @operator + +(comment) @comment diff --git a/tree-sitter/highlights/gitattributes.scm b/tree-sitter/highlights/gitattributes.scm new file mode 100644 index 0000000000..73ec71438a --- /dev/null +++ b/tree-sitter/highlights/gitattributes.scm @@ -0,0 +1,53 @@ +(dir_sep) @punctuation.delimiter + +(quoted_pattern + ("\"" @punctuation.special)) + +(range_notation) @string.special + +(range_notation + [ "[" "]" ] @punctuation.bracket) + +(wildcard) @character.special + +(range_negation) @operator + +(character_class) @constant + +(class_range ("-" @operator)) + +[ + (ansi_c_escape) + (escaped_char) +] @string.escape + +(attribute + (attr_name) @parameter) + +(attribute + (builtin_attr) @variable.builtin) + +[ + (attr_reset) + (attr_unset) + (attr_set) +] @operator + +(boolean_value) @boolean + +(string_value) @string + +(macro_tag) @preproc + +(macro_def + macro_name: (_) @property) + +[ + (pattern_negation) + (redundant_escape) + (trailing_slash) +] @error + +(ERROR) @error + +(comment) @comment @spell diff --git a/tree-sitter/highlights/gitcommit.scm b/tree-sitter/highlights/gitcommit.scm new file mode 100644 index 0000000000..ff0c4ddb35 --- /dev/null +++ b/tree-sitter/highlights/gitcommit.scm @@ -0,0 +1,33 @@ +(comment) @comment +(generated_comment) @comment +(title) @text.title +(text) @text +(branch) @text.reference +(change) @keyword +(filepath) @text.uri +(arrow) @punctuation.delimiter + +(subject) @text.title @spell +(subject (overflow) @text @spell) +(prefix (type) @keyword @nospell) +(prefix (scope) @parameter @nospell) +(prefix [ + "(" + ")" + ":" +] @punctuation.delimiter) +(prefix [ + "!" +] @punctuation.special) + +(message) @text @spell + +(trailer (token) @label) +(trailer (value) @text) + +(breaking_change (token) @text.warning) +(breaking_change (value) @text @spell) + +(scissor) @comment + +(ERROR) @error diff --git a/tree-sitter/highlights/gitignore.scm b/tree-sitter/highlights/gitignore.scm new file mode 100644 index 0000000000..769f96a79c --- /dev/null +++ b/tree-sitter/highlights/gitignore.scm @@ -0,0 +1,31 @@ +(comment) @comment @spell + +[ + (directory_separator) + (directory_separator_escaped) +] @punctuation.delimiter + +[ + (wildcard_char_single) + (wildcard_chars) + (wildcard_chars_allow_slash) + (bracket_negation) +] @operator + +(negation) @punctuation.special + +[ + (pattern_char_escaped) + (bracket_char_escaped) +] @string.escape + +;; bracket expressions +[ + "[" + "]" +] @punctuation.bracket + +(bracket_char) @constant +(bracket_range + "-" @operator) +(bracket_char_class) @constant.builtin diff --git a/tree-sitter/highlights/gleam.scm b/tree-sitter/highlights/gleam.scm new file mode 100644 index 0000000000..051cbffc05 --- /dev/null +++ b/tree-sitter/highlights/gleam.scm @@ -0,0 +1,172 @@ +; Keywords +[ + "as" + "let" + "panic" + "todo" + "type" + "use" +] @keyword + +; Function Keywords +[ + "fn" +] @keyword.function + +; Imports +[ + "import" +] @include + +; Conditionals +[ + "case" + "if" +] @conditional + +; Exceptions +[ + "assert" +] @exception + +; Punctuation +[ + "(" + ")" + "<<" + ">>" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "," + "." + ":" + "->" +] @punctuation.delimiter + +[ + "#" +] @punctuation.special + +; Operators +[ + "%" + "&&" + "*" + "*." + "+" + "+." + "-" + "-." + ".." + "/" + "/." + "<" + "<." + "<=" + "<=." + "=" + "==" + ">" + ">." + ">=" + ">=." + "|>" + "||" +] @operator + +; Identifiers +(identifier) @variable + +; Comments +[ + (comment) +] @comment @spell + +[ + (module_comment) + (statement_comment) +] @comment.documentation @spell + +; Unused Identifiers +[ + (discard) + (hole) +] @comment + +; Modules & Imports +(module) @namespace +(import alias: ((identifier) @namespace)?) +(remote_type_identifier module: (identifier) @namespace) +(unqualified_import name: (identifier) @function) + +; Strings +(string) @string + +; Bit Strings +(bit_string_segment) @string.special + +; Numbers +(integer) @number + +(float) @float + +; Function Parameter Labels +(function_call arguments: (arguments (argument label: (label) @label))) +(function_parameter label: (label)? @label name: (identifier) @parameter) + +; Records +(record arguments: (arguments (argument label: (label) @property)?)) +(record_pattern_argument label: (label) @property) +(record_update_argument label: (label) @property) +(field_access record: (identifier) @variable field: (label) @property) +(data_constructor_argument (label) @property) + +; Types +[ + (type_identifier) + (type_parameter) + (type_var) +] @type + +((type_identifier) @type.builtin + (#any-of? @type.builtin "Int" "Float" "String" "List")) + +; Type Qualifiers +[ + "const" + "external" + (opacity_modifier) + (visibility_modifier) +] @type.qualifier + +; Tuples +(tuple_access index: (integer) @operator) + +; Functions +(function name: (identifier) @function) +(function_call function: (identifier) @function.call) +(function_call function: (field_access field: (label) @function.call)) + +; External Functions +(external_function name: (identifier) @function) +(external_function_body (string) @namespace . (string) @function) + +; Constructors +(constructor_name) @type @constructor + +([(type_identifier) (constructor_name)] @constant.builtin + (#any-of? @constant.builtin "Ok" "Error")) + +; Booleans +((constructor_name) @boolean (#any-of? @boolean "True" "False")) + +; Pipe Operator +(binary_expression operator: "|>" right: (identifier) @function) + +; Parser Errors +(ERROR) @error diff --git a/tree-sitter/highlights/glimmer.scm b/tree-sitter/highlights/glimmer.scm new file mode 100644 index 0000000000..70a48cf830 --- /dev/null +++ b/tree-sitter/highlights/glimmer.scm @@ -0,0 +1,88 @@ +; === Tag Names === + +; Tags that start with a lower case letter are HTML tags +; We'll also use this highlighting for named blocks (which start with `:`) +((tag_name) @tag + (#lua-match? @tag "^:?[%l]")) +; Tags that start with a capital letter are Glimmer components +((tag_name) @constructor + (#lua-match? @constructor "^%u")) + +(attribute_name) @property + +(string_literal) @string +(number_literal) @number +(boolean_literal) @boolean + +(concat_statement) @string + +; === Block Statements === + +; Highlight the brackets +(block_statement_start) @tag.delimiter +(block_statement_end) @tag.delimiter + +; Highlight `if`/`each`/`let` +(block_statement_start path: (identifier) @conditional) +(block_statement_end path: (identifier) @conditional) +((mustache_statement (identifier) @conditional) + (#lua-match? @conditional "else")) + +; == Mustache Statements === + +; Highlight the whole statement, to color brackets and separators +(mustache_statement) @tag.delimiter + +; An identifier in a mustache expression is a variable +((mustache_statement [ + (path_expression (identifier) @variable) + (identifier) @variable + ]) + (#not-any-of? @variable "yield" "outlet" "this" "else")) +; As are arguments in a block statement +(block_statement_start argument: [ + (path_expression (identifier) @variable) + (identifier) @variable + ]) +; As is an identifier in a block param +(block_params (identifier) @variable) +; As are helper arguments +((helper_invocation argument: [ + (path_expression (identifier) @variable) + (identifier) @variable + ]) + (#not-eq? @variable "this")) +; `this` should be highlighted as a built-in variable +((identifier) @variable.builtin + (#eq? @variable.builtin "this")) + +; If the identifier is just "yield" or "outlet", it's a keyword +((mustache_statement (identifier) @keyword) + (#any-of? @keyword "yield" "outlet")) + +; Helpers are functions +((helper_invocation helper: [ + (path_expression (identifier) @function) + (identifier) @function + ]) + (#not-any-of? @function "if" "yield")) +((helper_invocation helper: (identifier) @conditional) + (#eq? @conditional "if")) +((helper_invocation helper: (identifier) @keyword) + (#eq? @keyword "yield")) + +(hash_pair key: (identifier) @property) + +(comment_statement) @comment + +(attribute_node "=" @operator) + +(block_params "as" @keyword) +(block_params "|" @operator) + +[ + "<" + ">" + "" +] @tag.delimiter diff --git a/tree-sitter/highlights/glsl.scm b/tree-sitter/highlights/glsl.scm new file mode 100644 index 0000000000..4e5e57f477 --- /dev/null +++ b/tree-sitter/highlights/glsl.scm @@ -0,0 +1,37 @@ +; inherits: c + +[ + "in" + "out" + "inout" + "uniform" + "shared" + "layout" + "attribute" + "varying" + "buffer" + "coherent" + "readonly" + "writeonly" + "precision" + "highp" + "mediump" + "lowp" + "centroid" + "sample" + "patch" + "smooth" + "flat" + "noperspective" + "invariant" + "precise" +] @type.qualifier + +"subroutine" @keyword.function + +(extension_storage_class) @storageclass + +( + (identifier) @variable.builtin + (#match? @variable.builtin "^gl_") +) diff --git a/tree-sitter/highlights/go.scm b/tree-sitter/highlights/go.scm new file mode 100644 index 0000000000..4569ec0861 --- /dev/null +++ b/tree-sitter/highlights/go.scm @@ -0,0 +1,253 @@ +;; Forked from tree-sitter-go +;; Copyright (c) 2014 Max Brunsfeld (The MIT License) + +;; +; Identifiers + +(type_identifier) @type +(type_spec name: (type_identifier) @type.definition) +(field_identifier) @property +(identifier) @variable +(package_identifier) @namespace + +(parameter_declaration (identifier) @parameter) +(variadic_parameter_declaration (identifier) @parameter) + +(label_name) @label + +(const_spec + name: (identifier) @constant) + +; Function calls + +(call_expression + function: (identifier) @function.call) + +(call_expression + function: (selector_expression + field: (field_identifier) @method.call)) + +; Function definitions + +(function_declaration + name: (identifier) @function) + +(method_declaration + name: (field_identifier) @method) + +(method_spec + name: (field_identifier) @method) + +; Constructors + +((call_expression (identifier) @constructor) + (#lua-match? @constructor "^[nN]ew.+$")) + +((call_expression (identifier) @constructor) + (#lua-match? @constructor "^[mM]ake.+$")) + +; Operators + +[ + "--" + "-" + "-=" + ":=" + "!" + "!=" + "..." + "*" + "*" + "*=" + "/" + "/=" + "&" + "&&" + "&=" + "&^" + "&^=" + "%" + "%=" + "^" + "^=" + "+" + "++" + "+=" + "<-" + "<" + "<<" + "<<=" + "<=" + "=" + "==" + ">" + ">=" + ">>" + ">>=" + "|" + "|=" + "||" + "~" +] @operator + +; Keywords + +[ + "break" + "const" + "continue" + "default" + "defer" + "goto" + "interface" + "range" + "select" + "struct" + "type" + "var" + "fallthrough" +] @keyword + +"func" @keyword.function +"return" @keyword.return +"go" @keyword.coroutine + +"for" @repeat + +[ + "import" + "package" +] @include + +[ + "else" + "case" + "switch" + "if" + ] @conditional + + +;; Builtin types + +[ "chan" "map" ] @type.builtin + +((type_identifier) @type.builtin + (#any-of? @type.builtin + "any" + "bool" + "byte" + "comparable" + "complex128" + "complex64" + "error" + "float32" + "float64" + "int" + "int16" + "int32" + "int64" + "int8" + "rune" + "string" + "uint" + "uint16" + "uint32" + "uint64" + "uint8" + "uintptr")) + + +;; Builtin functions + +((identifier) @function.builtin + (#any-of? @function.builtin + "append" + "cap" + "clear" + "close" + "complex" + "copy" + "delete" + "imag" + "len" + "make" + "new" + "panic" + "print" + "println" + "real" + "recover")) + + +; Delimiters + +"." @punctuation.delimiter +"," @punctuation.delimiter +":" @punctuation.delimiter +";" @punctuation.delimiter + +"(" @punctuation.bracket +")" @punctuation.bracket +"{" @punctuation.bracket +"}" @punctuation.bracket +"[" @punctuation.bracket +"]" @punctuation.bracket + + +; Literals + +(interpreted_string_literal) @string +(raw_string_literal) @string @spell +(rune_literal) @string +(escape_sequence) @string.escape + +(int_literal) @number +(float_literal) @float +(imaginary_literal) @number + +[ + (true) + (false) +] @boolean + +[ + (nil) + (iota) +] @constant.builtin + +(keyed_element + . (literal_element (identifier) @field)) +(field_declaration name: (field_identifier) @field) + +; Comments + +(comment) @comment @spell + +;; Doc Comments + +(source_file . (comment)+ @comment.documentation) + +(source_file + (comment)+ @comment.documentation + . (const_declaration)) + +(source_file + (comment)+ @comment.documentation + . (function_declaration)) + +(source_file + (comment)+ @comment.documentation + . (type_declaration)) + +(source_file + (comment)+ @comment.documentation + . (var_declaration)) + +; Errors + +(ERROR) @error + +; Spell + +((interpreted_string_literal) @spell + (#not-has-parent? @spell import_spec)) diff --git a/tree-sitter/highlights/godot_resource.scm b/tree-sitter/highlights/godot_resource.scm new file mode 100644 index 0000000000..005ddaacb8 --- /dev/null +++ b/tree-sitter/highlights/godot_resource.scm @@ -0,0 +1,28 @@ +(identifier) @type.builtin + +(attribute (identifier) @property) +(property (path) @property) +(constructor (identifier) @constructor) + +(string) @string +(integer) @number +(float) @float + +(true) @constant.builtin +(false) @constant.builtin + +[ + "[" + "]" +] @tag.delimiter + +[ + "(" + ")" + "{" + "}" +] @punctuation.bracket + +"=" @operator + +(ERROR) @error diff --git a/tree-sitter/highlights/gomod.scm b/tree-sitter/highlights/gomod.scm new file mode 100644 index 0000000000..46cd3b65ef --- /dev/null +++ b/tree-sitter/highlights/gomod.scm @@ -0,0 +1,18 @@ +[ + "require" + "replace" + "go" + "exclude" + "retract" + "module" +] @keyword + +"=>" @operator + +(comment) @comment +(module_path) @text.uri + +[ +(version) +(go_version) +] @string diff --git a/tree-sitter/highlights/gosum.scm b/tree-sitter/highlights/gosum.scm new file mode 100644 index 0000000000..bb65bd71ff --- /dev/null +++ b/tree-sitter/highlights/gosum.scm @@ -0,0 +1,31 @@ +[ + "alpha" + "beta" + "dev" + "pre" + "rc" + "+incompatible" +] @keyword + + +(module_path) @string @text.uri +(module_version) @string.special + +(hash_version) @attribute +(hash) @symbol + +[ + (number) + (number_with_decimal) + (hex_number) +] @number + +(checksum + "go.mod" @string) + +[ + ":" + "." + "-" + "/" +] @punctuation.delimiter diff --git a/tree-sitter/highlights/gowork.scm b/tree-sitter/highlights/gowork.scm new file mode 100644 index 0000000000..9c84bcc449 --- /dev/null +++ b/tree-sitter/highlights/gowork.scm @@ -0,0 +1,14 @@ +[ + "replace" + "go" + "use" +] @keyword + +"=>" @operator + +(comment) @comment + +[ +(version) +(go_version) +] @string diff --git a/tree-sitter/highlights/graphql.scm b/tree-sitter/highlights/graphql.scm new file mode 100644 index 0000000000..90122ae59b --- /dev/null +++ b/tree-sitter/highlights/graphql.scm @@ -0,0 +1,165 @@ +; Types +;------ + +(scalar_type_definition + (name) @type) + +(object_type_definition + (name) @type) + +(interface_type_definition + (name) @type) + +(union_type_definition + (name) @type) + +(enum_type_definition + (name) @type) + +(input_object_type_definition + (name) @type) + +(scalar_type_extension + (name) @type) + +(object_type_extension + (name) @type) + +(interface_type_extension + (name) @type) + +(union_type_extension + (name) @type) + +(enum_type_extension + (name) @type) + +(input_object_type_extension + (name) @type) + +(named_type + (name) @type) + +; Directives +;----------- + +(directive_definition + "@" @attribute + (name) @attribute) + +(directive) @attribute + +; Properties +;----------- + +(field + (name) @property) + +(field + (alias + (name) @property)) + +(field_definition + (name) @property) + +(object_value + (object_field + (name) @property)) + +(enum_value + (name) @property) + +; Variable Definitions and Arguments +;----------------------------------- + +(operation_definition + (name) @variable) + +(fragment_name + (name) @variable) + +(input_fields_definition + (input_value_definition + (name) @parameter)) + +(argument + (name) @parameter) + +(arguments_definition + (input_value_definition + (name) @parameter)) + +(variable_definition + (variable) @parameter) + +(argument + (value + (variable) @variable)) + +; Constants +;---------- + +(string_value) @string + +(int_value) @number + +(float_value) @float + +(boolean_value) @boolean + +; Literals +;--------- + +(description + (string_value) @string.documentation @spell) + +(comment) @comment @spell + +(directive_location + (executable_directive_location) @type.builtin) + +(directive_location + (type_system_directive_location) @type.builtin) + +; Keywords +;---------- + +[ + "query" + "mutation" + "subscription" + "fragment" + "scalar" + "type" + "interface" + "union" + "enum" + "input" + "extend" + "directive" + "schema" + "on" + "repeatable" + "implements" +] @keyword + +; Punctuation +;------------ + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +"=" @operator + +"|" @punctuation.delimiter +"&" @punctuation.delimiter +":" @punctuation.delimiter + +"..." @punctuation.special +"!" @punctuation.special diff --git a/tree-sitter/highlights/groovy.scm b/tree-sitter/highlights/groovy.scm new file mode 100644 index 0000000000..a9b38ef5b2 --- /dev/null +++ b/tree-sitter/highlights/groovy.scm @@ -0,0 +1,95 @@ +(unit + (identifier) @variable) +(string + (identifier) @variable) + +(escape_sequence) @string.escape + +(block + (unit + (identifier) @namespace)) + +(func + (identifier) @function) + +(number) @number + +((identifier) @boolean + (#any-of? @boolean "true" "false" "True" "False")) + +((identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z%d_]*$")) + +((identifier) @constant.builtin + (#eq? @constant.builtin "null")) + +((identifier) @type + (#any-of? @type + "String" + "Map" + "Object" + "Boolean" + "Integer" + "List")) + +((identifier) @function.builtin + (#any-of? @function.builtin + "void" + "id" + "version" + "apply" + "implementation" + "testImplementation" + "androidTestImplementation" + "debugImplementation")) + +((identifier) @keyword + (#any-of? @keyword + "static" + "class" + "def" + "import" + "package" + "assert" + "extends" + "implements" + "instanceof" + "interface" + "new")) + +((identifier) @type.qualifier + (#any-of? @type.qualifier + "abstract" + "protected" + "private" + "public")) + +((identifier) @exception + (#any-of? @exception + "throw" + "finally" + "try" + "catch")) + +(string) @string + +[ + (line_comment) + (block_comment) +] @comment @spell + +((block_comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +((line_comment) @comment.documentation + (#lua-match? @comment.documentation "^///[^/]")) + +((line_comment) @comment.documentation + (#lua-match? @comment.documentation "^///$")) + +[ + (operators) + (leading_key) +] @operator + +["(" ")" "[" "]" "{" "}"] @punctuation.bracket diff --git a/tree-sitter/highlights/hack.scm b/tree-sitter/highlights/hack.scm new file mode 100644 index 0000000000..a75b166ca2 --- /dev/null +++ b/tree-sitter/highlights/hack.scm @@ -0,0 +1,317 @@ +(variable) @variable +(identifier) @variable +((variable) @variable.builtin + (#eq? @variable.builtin "$this")) + +(braced_expression) @none + +(scoped_identifier + (qualified_identifier + (identifier) @type)) + +[ + (comment) + (heredoc) +] @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +"function" @keyword.function + +[ + "type" + "interface" + "implements" + "class" + "using" + "namespace" + "attribute" + "const" + "extends" + "insteadof" +] @keyword + +[ + "async" + "await" +] @keyword.coroutine + +[ + "use" + "include" + "include_once" + "require" + "require_once" +] @include + +[ + "new" + "print" + "echo" + "newtype" + "clone" + "as" +] @keyword.operator + +"return" @keyword.return + +[ + (abstract_modifier) + (final_modifier) + (static_modifier) + (visibility_modifier) + (xhp_modifier) +] @type.qualifier + +[ + "shape" + "tuple" + (array_type) + "bool" + "float" + "int" + "string" + "arraykey" + "void" + "nonnull" + "mixed" + "dynamic" + "noreturn" +] @type.builtin + +[ + (null) +] @constant.builtin + +[ + (true) + (false) +] @boolean + +(type_specifier) @type +(new_expression + (_) @type) + +(alias_declaration "newtype" . (_) @type) +(alias_declaration "type" . (_) @type) + +(class_declaration + name: (identifier) @type) +(type_parameter + name: (identifier) @type) + +(collection + (qualified_identifier + (identifier) @type .)) + +[ + "@required" + "@lateinit" + (attribute_modifier) +] @attribute + +[ + "=" + "??=" + ".=" + "|=" + "^=" + "&=" + "<<=" + ">>=" + "+=" + "-=" + "*=" + "/=" + "%=" + "**=" + + "==>" + "|>" + "??" + "||" + "&&" + "|" + "^" + "&" + "==" + "!=" + "===" + "!==" + "<" + ">" + "<=" + ">=" + "<=>" + "<<" + ">>" + "->" + "+" + "-" + "." + "*" + "/" + "%" + "**" + + "++" + "--" + "!" + + "?:" + + "=" + "??=" + ".=" + "|=" + "^=" + "&=" + "<<=" + ">>=" + "+=" + "-=" + "*=" + "/=" + "%=" + "**=" + "=>" + + ;; type modifiers + "@" + "?" + "~" +] @operator + +(integer) @number +(float) @float + +(parameter + (variable) @parameter) + +(call_expression + function: (qualified_identifier (identifier) @function.call .)) + +(call_expression + function: (scoped_identifier (identifier) @function.call .)) + +(call_expression + function: (selection_expression + (qualified_identifier (identifier) @method.call .))) + +(qualified_identifier + (_) @namespace . + (_)) + +(use_statement + (qualified_identifier + (_) @namespace .) + (use_clause)) + +(use_statement + (use_type "namespace") + (use_clause + (qualified_identifier + (identifier) @namespace .) + alias: (identifier)? @namespace)) + +(use_statement + (use_type "const") + (use_clause + (qualified_identifier + (identifier) @constant .) + alias: (identifier)? @constant)) + +(use_statement + (use_type "function") + (use_clause + (qualified_identifier + (identifier) @function .) + alias: (identifier)? @function)) + +(use_statement + (use_type "type") + (use_clause + (qualified_identifier + (identifier) @type .) + alias: (identifier)? @type)) + +(use_clause + (use_type "namespace") + (qualified_identifier + (_) @namespace .) + alias: (identifier)? @namespace) + +(use_clause + (use_type "function") + (qualified_identifier + (_) @function .) + alias: (identifier)? @function) + +(use_clause + (use_type "const") + (qualified_identifier + (_) @constant .) + alias: (identifier)? @constant) + +(use_clause + (use_type "type") + (qualified_identifier + (_) @type .) + alias: (identifier)? @type) + +(function_declaration + name: (identifier) @function) +(method_declaration + name: (identifier) @method) + +(type_arguments + [ "<" ">" ] @punctuation.bracket) +[ "(" ")" "[" "]" "{" "}" "<<" ">>"] @punctuation.bracket + +(xhp_open + [ "<" ">" ] @tag.delimiter) +(xhp_close + [ "" ] @tag.delimiter) + +[ "." ";" "::" ":" "," ] @punctuation.delimiter +(qualified_identifier + "\\" @punctuation.delimiter) + +(ternary_expression + ["?" ":"] @conditional.ternary) + +[ + "if" + "else" + "elseif" + "switch" + "case" +] @conditional + +[ + "try" + "catch" + "finally" +] @exception + +[ + "for" + "while" + "foreach" + "do" + "continue" + "break" +] @repeat + +[ + (string) + (xhp_string) +] @string + +[ + (xhp_open) + (xhp_close) +] @tag + +(ERROR) @error diff --git a/tree-sitter/highlights/hare.scm b/tree-sitter/highlights/hare.scm new file mode 100644 index 0000000000..063af31b39 --- /dev/null +++ b/tree-sitter/highlights/hare.scm @@ -0,0 +1,255 @@ +; Variables + +(identifier) @variable + +; Types + +(type) @type + +(scoped_type_identifier + (identifier) . (identifier) @type) + +(struct_literal + . (identifier) @type) + +(builtin_type) @type.builtin + +; Constants + +((identifier) @constant + (#lua-match? @constant "^[A-Z_]+$")) + +; Includes + +[ + "use" +] @include + +(use_statement + (scoped_type_identifier + (identifier) @namespace)) +(use_statement + (identifier) @namespace "{") +(use_statement + . (identifier) @namespace .) + +((scoped_type_identifier + path: (_) @namespace) + (#set! "priority" 105)) + +; Keywords + +[ + "def" + "enum" + "export" + "let" + "struct" + "type" + "union" +] @keyword + +[ + "fn" +] @keyword.function + +[ + "defer" + "yield" + "return" +] @keyword.return + +[ + "as" + "is" +] @keyword.operator + +; Typedefs + +(type_declaration + "type" (identifier) @type.definition . "=") + +; Qualifiers + +[ + "const" + "static" + "nullable" +] @type.qualifier + +; Attributes + +[ + "@fini" + "@init" + "@test" + "@noreturn" + "@packed" + (declaration_attribute) +] @attribute + +; Labels + +((label) @label + (#set! "priority" 105)) + +; Functions + +(function_declaration + "fn" . (identifier) @function) + +(call_expression + . (identifier) @function.call) + +(call_expression + . (scoped_type_identifier + . (identifier) . "::" . (identifier) @method.call)) + +((call_expression + . (identifier) @function.builtin) + (#any-of? @function.builtin "align" "assert" "free" "len" "offset" "size")) + +(size_expression + "size" @function.builtin) + +((function_declaration + "fn" . (identifier) @constructor) + (#eq? @constructor "init")) + +((call_expression + . (identifier) @constructor) + (#eq? @constructor "init")) + +; Parameters + +(parameter + (_) @parameter . ":") + +; Fields + +((member_expression + "." (_) @field) + (#set! "priority" 105)) + +(field + . (identifier) @field) + +(field_assignment + . (identifier) @field) + +; Repeats + +[ + "for" +] @repeat + +; Conditionals + +[ + "if" + "else" + "break" + "switch" + "match" + "case" +] @conditional + +; Operators + +[ + "+" + "-" + "*" + "/" + "%" + "||" + "&&" + "^^" + "|" + "&" + "^" + "==" + "!=" + ">" + ">=" + "<=" + "<" + "<<" + ">>" + "~" + "!" + "+=" + "-=" + "*=" + "/=" + "%=" + "<<=" + ">>=" + "|=" + "&=" + "^=" + "||=" + "&&=" + "^^=" + "=" + "?" +] @operator + +; Punctuation + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +[ + ".." + "..." + "_" +] @punctuation.special + +(pointer_type "*" @punctuation.special) + +(slice_type "*" @punctuation.special) + +(error_type "!" @punctuation.special) + +[ + "," + "." + ":" + ";" + "::" + "=>" +] @punctuation.delimiter + +; Literals + +[ + (string) + (raw_string) +] @string + +(rune) @character + +(escape_sequence) @string.escape + +(number) @number + +(float) @float + +(boolean) @boolean + +[ + (void) + (null) +] @constant.builtin + +; Comments + +(comment) @comment @spell + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/haskell.scm b/tree-sitter/highlights/haskell.scm new file mode 100644 index 0000000000..ee1a679a95 --- /dev/null +++ b/tree-sitter/highlights/haskell.scm @@ -0,0 +1,160 @@ +;; ---------------------------------------------------------------------------- +;; Literals and comments + +(integer) @number +(exp_negation) @number +(exp_literal (float)) @float +(char) @character +(string) @string + +(con_unit) @symbol ; unit, as in () + +(comment) @comment + + +;; ---------------------------------------------------------------------------- +;; Punctuation + +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + +[ + (comma) + ";" +] @punctuation.delimiter + + +;; ---------------------------------------------------------------------------- +;; Keywords, operators, includes + +[ + "forall" + "∀" +] @repeat + +(pragma) @preproc + +[ + "if" + "then" + "else" + "case" + "of" +] @conditional + +[ + "import" + "qualified" + "module" +] @include + +[ + (operator) + (constructor_operator) + (type_operator) + (tycon_arrow) + (qualified_module) ; grabs the `.` (dot), ex: import System.IO + (qualified_type) + (qualified_variable) + (all_names) + (wildcard) + "." + ".." + "=" + "|" + "::" + "=>" + "->" + "<-" + "\\" + "`" + "@" +] @operator + +(module) @namespace + +[ + (where) + "let" + "in" + "class" + "instance" + "pattern" + "data" + "newtype" + "family" + "type" + "as" + "hiding" + "deriving" + "via" + "stock" + "anyclass" + "do" + "mdo" + "rec" + "infix" + "infixl" + "infixr" +] @keyword + + +;; ---------------------------------------------------------------------------- +;; Functions and variables + +(variable) @variable +(pat_wildcard) @variable +(signature name: (variable) @variable) + +(function + name: (variable) @function + patterns: (patterns)) +(function + name: (variable) @function + rhs: (exp_lambda)) +((signature (variable) @function (fun)) . (function (variable))) +((signature (variable) @_type (fun)) . (function (variable) @function) (#eq? @function @_type)) +((signature (variable) @function (context (fun))) . (function (variable))) +((signature (variable) @_type (context (fun))) . (function (variable) @function) (#eq? @function @_type)) +((signature (variable) @function (forall (context (fun)))) . (function (variable))) +((signature (variable) @_type (forall (context (fun)))) . (function (variable) @function) (#eq? @function @_type)) + +(exp_infix (variable) @operator) ; consider infix functions as operators +(exp_section_right (variable) @operator) ; partially applied infix functions (sections) also get highlighted as operators +(exp_section_left (variable) @operator) + +(exp_infix (exp_name) @function.call (#set! "priority" 101)) +(exp_apply . (exp_name (variable) @function.call)) +(exp_apply . (exp_name (qualified_variable (variable) @function.call))) + + +;; ---------------------------------------------------------------------------- +;; Types + +(type) @type +(type_star) @type +(type_variable) @type + +(constructor) @constructor + +; True or False +((constructor) @boolean (#any-of? @boolean "True" "False")) + + +;; ---------------------------------------------------------------------------- +;; Quasi-quotes + +(quoter) @function.call +; Highlighting of quasiquote_body is handled by injections.scm + +;; ---------------------------------------------------------------------------- +;; Spell checking + +(string) @spell +(comment) @spell diff --git a/tree-sitter/highlights/haskell_persistent.scm b/tree-sitter/highlights/haskell_persistent.scm new file mode 100644 index 0000000000..afb32f11dd --- /dev/null +++ b/tree-sitter/highlights/haskell_persistent.scm @@ -0,0 +1,38 @@ +;; ---------------------------------------------------------------------------- +;; Literals and comments + +(integer) @number +(float) @float +(char) @character +(string) @string +(attribute_name) @attribute +(attribute_exclamation_mark) @attribute + +(con_unit) @symbol ; unit, as in () + +(comment) @comment @spell + +;; ---------------------------------------------------------------------------- +;; Keywords, operators, includes + +[ + "Id" + "Primary" + "Foreign" + "deriving" +] @keyword + +"=" @operator + + +;; ---------------------------------------------------------------------------- +;; Functions and variables + +(variable) @variable + +;; ---------------------------------------------------------------------------- +;; Types + +(type) @type + +(constructor) @constructor diff --git a/tree-sitter/highlights/hcl.scm b/tree-sitter/highlights/hcl.scm new file mode 100644 index 0000000000..ba22f99d75 --- /dev/null +++ b/tree-sitter/highlights/hcl.scm @@ -0,0 +1,99 @@ +; highlights.scm + +[ + "!" + "\*" + "/" + "%" + "\+" + "-" + ">" + ">=" + "<" + "<=" + "==" + "!=" + "&&" + "||" +] @operator + +[ + "{" + "}" + "[" + "]" + "(" + ")" +] @punctuation.bracket + +[ + "." + ".*" + "," + "[*]" +] @punctuation.delimiter + +[ + (ellipsis) + "\?" + "=>" +] @punctuation.special + +[ + ":" + "=" +] @none + +[ + "for" + "endfor" + "in" +] @repeat + +[ + "if" + "else" + "endif" +] @conditional + +[ + (quoted_template_start) ; " + (quoted_template_end); " + (template_literal) ; non-interpolation/directive content +] @string + +[ + (heredoc_identifier) ; END + (heredoc_start) ; << or <<- +] @punctuation.delimiter + +[ + (template_interpolation_start) ; ${ + (template_interpolation_end) ; } + (template_directive_start) ; %{ + (template_directive_end) ; } + (strip_marker) ; ~ +] @punctuation.special + +(numeric_lit) @number +(bool_lit) @boolean +(null_lit) @constant +(comment) @comment @spell +(identifier) @variable + +(body (block (identifier) @keyword)) +(body (block (body (block (identifier) @type)))) +(function_call (identifier) @function) +(attribute (identifier) @field) + +; { key: val } +; +; highlight identifier keys as though they were block attributes +(object_elem key: (expression (variable_expr (identifier) @field))) + +; var.foo, data.bar +; +; first element in get_attr is a variable.builtin or a reference to a variable.builtin +(expression (variable_expr (identifier) @variable.builtin) (get_attr (identifier) @field)) + +(ERROR) @error diff --git a/tree-sitter/highlights/heex.scm b/tree-sitter/highlights/heex.scm new file mode 100644 index 0000000000..bf26090192 --- /dev/null +++ b/tree-sitter/highlights/heex.scm @@ -0,0 +1,56 @@ +; HEEx delimiters +[ + "%>" + "--%>" + "-->" + "/>" + "" + "{" + "}" +] @tag.delimiter + +; HEEx operators are highlighted as such +"=" @operator + +; HEEx inherits the DOCTYPE tag from HTML +(doctype) @constant + +; HEEx comments are highlighted as such +(comment) @comment + +; HEEx text content is treated as markup +(text) @text + +; Tree-sitter parser errors +(ERROR) @error + +; HEEx tags and slots are highlighted as HTML +[ + (tag_name) + (slot_name) +] @tag + +; HEEx attributes are highlighted as HTML attributes +(attribute_name) @tag.attribute +[ + (attribute_value) + (quoted_attribute_value) +] @string + +; HEEx components are highlighted as modules and function calls +(component_name [ + (module) @type + (function) @function + "." @punctuation.delimiter +]) diff --git a/tree-sitter/highlights/hjson.scm b/tree-sitter/highlights/hjson.scm new file mode 100644 index 0000000000..c8eed139f3 --- /dev/null +++ b/tree-sitter/highlights/hjson.scm @@ -0,0 +1,16 @@ +(true) @boolean +(false) @boolean +(null) @constant.builtin +(number) @number +(pair key: (string) @label) +(pair value: (string) @string) +(array (string) @string) +; (string_content (escape_sequence) @string.escape) +(ERROR) @error +; "," @punctuation.delimiter +"[" @punctuation.bracket +"]" @punctuation.bracket +"{" @punctuation.bracket +"}" @punctuation.bracket + +(comment) @comment diff --git a/tree-sitter/highlights/hlsl.scm b/tree-sitter/highlights/hlsl.scm new file mode 100644 index 0000000000..68e9e44bdb --- /dev/null +++ b/tree-sitter/highlights/hlsl.scm @@ -0,0 +1,35 @@ +; inherits: cpp + +[ + "in" + "out" + "inout" + "uniform" + "shared" + "groupshared" + "discard" + "cbuffer" + "row_major" + "column_major" + "globallycoherent" + "centroid" + "noperspective" + "nointerpolation" + "sample" + "linear" + "snorm" + "unorm" + "point" + "line" + "triangleadj" + "lineadj" + "triangle" +] @type.qualifier + +( + (identifier) @variable.builtin + (#lua-match? @variable.builtin "^SV_") +) + +(hlsl_attribute) @attribute +(hlsl_attribute ["[" "]"] @attribute) diff --git a/tree-sitter/highlights/hocon.scm b/tree-sitter/highlights/hocon.scm new file mode 100644 index 0000000000..9b74681f3b --- /dev/null +++ b/tree-sitter/highlights/hocon.scm @@ -0,0 +1,36 @@ +(comment) @comment + +(null) @constant.builtin +[ (true) (false) ] @boolean +(number) @number +(unit) @keyword +(string) @string +(multiline_string) @string +(string (escape_sequence) @string.escape) +(unquoted_string) @string + +[ "url" + "file" + "classpath" + "required" +] @keyword + +(include "include" @include) + +(substitution ["${" "${?" "}"] @punctuation.special) +(substitution (_) @field) + +(path (_) @field) +(value [":" "=" "+=" ] @operator) + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ "," ] @punctuation.delimiter +(unquoted_path "." @punctuation.delimiter) diff --git a/tree-sitter/highlights/hoon.scm b/tree-sitter/highlights/hoon.scm new file mode 100644 index 0000000000..f7136f63e8 --- /dev/null +++ b/tree-sitter/highlights/hoon.scm @@ -0,0 +1,33 @@ +(number) @number + +(string) @string + +[ + "(" + ")" + "[" + "]" +] @punctuation.bracket + +[ + (coreTerminator) + (seriesTerminator) +] @punctuation.delimiter + + +(rune) @operator + +(term) @constant + +(aura) @constant.builtin + +(Gap) @comment + +(boolean) @constant.builtin + +(date) @string.special + +(mold) @symbol +(specialIndex) @number.builtin +(lark) @operator +(fullContext) @symbol diff --git a/tree-sitter/highlights/html.scm b/tree-sitter/highlights/html.scm new file mode 100644 index 0000000000..6da261c0aa --- /dev/null +++ b/tree-sitter/highlights/html.scm @@ -0,0 +1,5 @@ +; inherits: html_tags + +(doctype) @constant + +"" + "" +] @tag.delimiter + +"=" @operator diff --git a/tree-sitter/highlights/htmldjango.scm b/tree-sitter/highlights/htmldjango.scm new file mode 100644 index 0000000000..848b455dd2 --- /dev/null +++ b/tree-sitter/highlights/htmldjango.scm @@ -0,0 +1,35 @@ +; adapted from https://github.com/interdependence/tree-sitter-htmldjango + +[ + (unpaired_comment) + (paired_comment) +] @comment @spell + +[ + "{{" "}}" + "{%" "%}" + (end_paired_statement) +] @punctuation.bracket + +[ + "end" + (tag_name) +] @function + +(variable_name) @variable + +(filter_name) @method +(filter_argument) @parameter + +(keyword) @keyword + +(operator) @operator +(variable "|" @operator) +(paired_statement "=" @operator) +(keyword_operator) @keyword.operator + +(number) @number + +(boolean) @boolean + +(string) @string diff --git a/tree-sitter/highlights/http.scm b/tree-sitter/highlights/http.scm new file mode 100644 index 0000000000..e89b4dc7f5 --- /dev/null +++ b/tree-sitter/highlights/http.scm @@ -0,0 +1,60 @@ +; Keywords + +(scheme) @keyword + +; Methods + +(method) @method + +; Constants + +(const_spec) @constant + +; Variables + +(identifier) @variable + +; Fields + +(pair name: (identifier) @field) + +; Parameters + +(query_param (key) @parameter) + +; Operators + +[ + "=" + "?" + "&" + "@" +] @operator + +; Literals + +(string) @string + +(target_url) @string @text.uri + +(number) @number + +; (boolean) @boolean + +(null) @constant.builtin + +; Punctuation + +[ "{{" "}}" ] @punctuation.bracket + +[ + ":" +] @punctuation.delimiter + +; Comments + +(comment) @comment @spell + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/hurl.scm b/tree-sitter/highlights/hurl.scm new file mode 100644 index 0000000000..269b2afc10 --- /dev/null +++ b/tree-sitter/highlights/hurl.scm @@ -0,0 +1,132 @@ +; highlights.scm + +[ + "[QueryStringParams]" + "[FormParams]" + "[MultipartFormData]" + "[Cookies]" + "[Captures]" + "[Asserts]" + "[Options]" + "[BasicAuth]" + (key_string) + (json_key_string) +] @property + +[ + "\\" + (regex_escaped_char) + (quoted_string_escaped_char) + (key_string_escaped_char) + (value_string_escaped_char) + (oneline_string_escaped_char) + (multiline_string_escaped_char) + (filename_escaped_char) + (json_string_escaped_char) +] @string.escape + +[ + "status" + "url" + "header" + "cookie" + "body" + "xpath" + "jsonpath" + "regex" + "variable" + "duration" + "sha256" + "md5" + "bytes" +] @function.builtin + +[ + "null" + "cacert" + "compressed" + "location" + "insecure" + "path-as-is" + "proxy" + "max-redirs" + "retry" + "retry-interval" + "retry-max-count" + (variable_option "variable") + "verbose" + "very-verbose" +] @constant.builtin + +(boolean) @boolean + +(variable_name) @variable + +[ + "not" + "equals" + "notEquals" + "greaterThan" + "greaterThanOrEquals" + "lessThan" + "lessThanOrEquals" + "startsWith" + "endsWith" + "contains" + "matches" + "exists" + "includes" + "isInteger" + "isFloat" + "isBoolean" + "isString" + "isCollection" +] @keyword.operator + +[ + "==" + "!=" + ">" + ">=" + "<" + "<=" +] @operator + +[ + (integer) + (status) +] @number + +[ + (float) + (json_number) +] @float + +[ ":" "," ] @punctuation.delimiter + +[ "[" "]" "{" "}" "{{" "}}" ] @punctuation.bracket + +[ + (value_string) + (quoted_string) + (json_string) +] @string + + +[ + "base64," + "file," + "hex," + (file_value) + (version) +] @string.special + +(regex) @string.regex + +(multiline_string_type) @type + +(comment) @comment @spell + +(filter) @attribute + +(method) @type.builtin diff --git a/tree-sitter/highlights/ini.scm b/tree-sitter/highlights/ini.scm new file mode 100644 index 0000000000..e14d5a334b --- /dev/null +++ b/tree-sitter/highlights/ini.scm @@ -0,0 +1,16 @@ +(section_name + (text) @type) ; consistency with toml +(comment) @comment + +[ + "[" + "]" +] @punctuation.bracket + +[ + "=" +] @operator + +(setting (setting_name) @property) +(setting_value) @text ; grammar does not support subtypes +(ERROR (setting_name) @text) diff --git a/tree-sitter/highlights/ispc.scm b/tree-sitter/highlights/ispc.scm new file mode 100644 index 0000000000..6770d6bb99 --- /dev/null +++ b/tree-sitter/highlights/ispc.scm @@ -0,0 +1,225 @@ +; inherits: c + +[ + "soa" + "task" + "launch" + "unmasked" + "template" + "typename" + (sync_expression) +] @keyword + +[ + "in" + "new" + "delete" +] @keyword.operator + +[ + "cdo" + "cfor" + "cwhile" + "foreach" + "foreach_tiled" + "foreach_active" + "foreach_unique" +] @repeat + +[ + "cif" +] @conditional + +[ + "varying" + "uniform" +] @type.qualifier + +"__regcall" @attribute + +(overload_declarator name: _ @function) +(foreach_statement range_operator: _ @operator) + +(short_vector ["<" ">"] @punctuation.bracket) +(soa_qualifier ["<" ">"] @punctuation.bracket) +(template_argument_list ["<" ">"] @punctuation.bracket) +(template_parameter_list ["<" ">"] @punctuation.bracket) + +(llvm_identifier) @function.builtin + +; built-in variables +((identifier) @variable.builtin + (#any-of? @variable.builtin + "programCount" + "programIndex" + "taskCount" + "taskCount0" + "taskCount1" + "taskCount2" + "taskIndex" + "taskIndex0" + "taskIndex1" + "taskIndex2" + "threadCount" + "threadIndex" + )) + +; preprocessor constants +((identifier) @constant.builtin + (#any-of? @constant.builtin + "ISPC" + "ISPC_FP16_SUPPORTED" + "ISPC_FP64_SUPPORTED" + "ISPC_LLVM_INTRINSICS_ENABLED" + "ISPC_MAJOR_VERSION" + "ISPC_MINOR_VERSION" + "ISPC_POINTER_SIZE" + "ISPC_TARGET_AVX" + "ISPC_TARGET_AVX2" + "ISPC_TARGET_AVX512KNL" + "ISPC_TARGET_AVX512SKX" + "ISPC_TARGET_AVX512SPR" + "ISPC_TARGET_NEON" + "ISPC_TARGET_SSE2" + "ISPC_TARGET_SSE4" + "ISPC_UINT_IS_DEFINED" + "PI" + "TARGET_ELEMENT_WIDTH" + "TARGET_WIDTH" + )) + +; standard library built-in +((type_identifier) @type.builtin + (#lua-match? @type.builtin "^RNGState")) + +(call_expression + function: (identifier) @function.builtin + (#any-of? @function.builtin + "abs" + "acos" + "all" + "alloca" + "and" + "any" + "aos_to_soa2" + "aos_to_soa3" + "aos_to_soa4" + "asin" + "assert" + "assume" + "atan" + "atan2" + "atomic_add_global" + "atomic_add_local" + "atomic_and_global" + "atomic_and_local" + "atomic_compare_exchange_global" + "atomic_compare_exchange_local" + "atomic_max_global" + "atomic_max_local" + "atomic_min_global" + "atomic_min_local" + "atomic_or_global" + "atomic_or_local" + "atomic_subtract_global" + "atomic_subtract_local" + "atomic_swap_global" + "atomic_swap_local" + "atomic_xor_global" + "atomic_xor_local" + "avg_down" + "avg_up" + "broadcast" + "ceil" + "clamp" + "clock" + "cos" + "count_leading_zeros" + "count_trailing_zeros" + "doublebits" + "exclusive_scan_add" + "exclusive_scan_and" + "exclusive_scan_or" + "exp" + "extract" + "fastmath" + "float16bits" + "floatbits" + "float_to_half" + "float_to_half_fast" + "float_to_srgb8" + "floor" + "frandom" + "frexp" + "half_to_float" + "half_to_float_fast" + "insert" + "intbits" + "invoke_sycl" + "isnan" + "ISPCAlloc" + "ISPCLaunch" + "ISPCSync" + "lanemask" + "ldexp" + "log" + "max" + "memcpy" + "memcpy64" + "memmove" + "memmove64" + "memory_barrier" + "memset" + "memset64" + "min" + "none" + "num_cores" + "or" + "packed_load_active" + "packed_store_active" + "packed_store_active2" + "packmask" + "popcnt" + "pow" + "prefetch_l1" + "prefetch_l2" + "prefetch_l3" + "prefetch_nt" + "prefetchw_l1" + "prefetchw_l2" + "prefetchw_l3" + "print" + "random" + "rcp" + "rcp_fast" + "rdrand" + "reduce_add" + "reduce_equal" + "reduce_max" + "reduce_min" + "rotate" + "round" + "rsqrt" + "rsqrt_fast" + "saturating_add" + "saturating_div" + "saturating_mul" + "saturating_sub" + "seed_rng" + "select" + "shift" + "shuffle" + "signbits" + "sign_extend" + "sin" + "sincos" + "soa_to_aos2" + "soa_to_aos3" + "soa_to_aos4" + "sqrt" + "streaming_load" + "streaming_load_uniform" + "streaming_store" + "tan" + "trunc" + )) diff --git a/tree-sitter/highlights/janet_simple.scm b/tree-sitter/highlights/janet_simple.scm new file mode 100644 index 0000000000..5d5a6007af --- /dev/null +++ b/tree-sitter/highlights/janet_simple.scm @@ -0,0 +1,299 @@ +;; >> Literals + +(kwd_lit) @symbol +(str_lit) @string @spell +(long_str_lit) @string @spell +(buf_lit) @string @spell +(long_buf_lit) @string @spell +(num_lit) @number +(bool_lit) @boolean +(nil_lit) @constant.builtin +(comment) @comment @spell + +["{" "@{" "}" + "[" "@[" "]" + "(" "@(" ")"] @punctuation.bracket + +;; >> Symbols + +;; General symbol highlighting +(sym_lit) @variable + +;; General function calls +(par_tup_lit + . + (sym_lit) @function.call) + +(short_fn_lit + . + (sym_lit) @function.call) + +;; Quoted symbols + +(quote_lit + (sym_lit) @symbol) + +(qq_lit + (sym_lit) @symbol) + +;; Dynamic variables + +((sym_lit) @variable.builtin + (#lua-match? @variable.builtin "^[*].+[*]$")) + +;; Comment + +((sym_lit) @comment + (#any-of? @comment "comment")) + +;; Special forms and builtin macros +;; +;; # special forms were manually added at the beginning +;; +;; # for macros +;; (each name (all-bindings) +;; (when-let [info (dyn (symbol name))] +;; (when (info :macro) +;; (print name)))) + +((sym_lit) @function.macro + (#any-of? @function.macro + ;; special forms + "break" + "def" "do" + "fn" + "if" + "quasiquote" "quote" + "set" "splice" + "unquote" "upscope" + "var" + "while" + ;; macros + "%=" "*=" + "++" "+=" + "--" "-=" + "->" "->>" "-?>" "-?>>" + "/=" + "and" "as->" "as-macro" "as?->" "assert" + "case" "chr" "comment" "compif" "comptime" "compwhen" "cond" "coro" + "def-" "default" "defdyn" "defer" "defmacro" "defmacro-" + "defn" "defn-" + "delay" "doc" + "each" "eachk" "eachp" + "eachy" ;; XXX: obsolete + "edefer" + "ev/do-thread" "ev/gather" "ev/spawn" "ev/spawn-thread" + "ev/with-deadline" + "ffi/defbind" + "fiber-fn" + "for" "forever" "forv" + "generate" + "if-let" "if-not" "if-with" "import" + "juxt" + "label" "let" "loop" + "match" + "or" + "prompt" "protect" + "repeat" + "seq" "short-fn" + "tabseq" "toggle" "tracev" "try" + "unless" "use" + "var-" "varfn" + "when" "when-let" "when-with" + "with" "with-dyns" "with-syms" "with-vars")) + +;; All builtin functions +;; +;; (each name (all-bindings) +;; (when-let [info (dyn (symbol name))] +;; (when (and (nil? (info :macro)) +;; (or (function? (info :value)) +;; (cfunction? (info :value)))) +;; (print name)))) + +((sym_lit) @function.builtin + (#any-of? @function.builtin + "%" "*" "+" "-" "/" + "<" "<=" "=" ">" ">=" + ;; debugging -- start janet with -d and use (debug) to see these + ".break" ".breakall" ".bytecode" + ".clear" ".clearall" + ".disasm" + ".fiber" + ".fn" ".frame" + ".locals" + ".next" ".nextc" + ".ppasm" + ".signal" ".slot" ".slots" ".source" ".stack" ".step" + ;; back to regularly scheduled program + "abstract?" "accumulate" "accumulate2" "all" "all-bindings" + "all-dynamics" "any?" "apply" + "array" + "array/clear" "array/concat" "array/ensure" "array/fill" + "array/insert" "array/new" "array/new-filled" "array/peek" + "array/pop" "array/push" "array/remove" "array/slice" "array/trim" + "array?" + "asm" + "bad-compile" "bad-parse" + "band" "blshift" "bnot" + "boolean?" + "bor" "brshift" "brushift" + "buffer" + "buffer/bit" "buffer/bit-clear" "buffer/bit-set" + "buffer/bit-toggle" "buffer/blit" "buffer/clear" "buffer/fill" + "buffer/format" "buffer/new" "buffer/new-filled" "buffer/popn" + "buffer/push" "buffer/push-at" "buffer/push-byte" + "buffer/push-string" "buffer/push-word" "buffer/slice" + "buffer/trim" + "buffer?" + "bxor" + "bytes?" + "cancel" + "cfunction?" + "cli-main" + "cmp" "comp" "compare" "compare<" "compare<=" "compare=" + "compare>" "compare>=" + "compile" "complement" "count" "curenv" + "debug" + "debug/arg-stack" "debug/break" "debug/fbreak" "debug/lineage" + "debug/stack" "debug/stacktrace" "debug/step" "debug/unbreak" + "debug/unfbreak" + "debugger" "debugger-on-status" + "dec" "deep-not=" "deep=" "defglobal" "describe" + "dictionary?" + "disasm" "distinct" "doc*" "doc-format" "doc-of" "dofile" + "drop" "drop-until" "drop-while" "dyn" + "eflush" "empty?" "env-lookup" + "eprin" "eprinf" "eprint" "eprintf" "error" "errorf" + "ev/acquire-lock" "ev/acquire-rlock" "ev/acquire-wlock" + "ev/all-tasks" "ev/call" "ev/cancel" "ev/capacity" "ev/chan" + "ev/chan-close" "ev/chunk" "ev/close" "ev/count" "ev/deadline" + "ev/full" "ev/give" "ev/give-supervisor" "ev/go" "ev/lock" + "ev/read" "ev/release-lock" "ev/release-rlock" + "ev/release-wlock" "ev/rselect" "ev/rwlock" "ev/select" + "ev/sleep" "ev/take" "ev/thread" "ev/thread-chan" "ev/write" + "eval" "eval-string" "even?" "every?" "extreme" + "false?" + "ffi/align" "ffi/call" "ffi/close" "ffi/context" "ffi/free" + "ffi/jitfn" "ffi/lookup" "ffi/malloc" "ffi/native" + "ffi/pointer-buffer" "ffi/read" "ffi/signature" "ffi/size" + "ffi/struct" "ffi/trampoline" "ffi/write" + "fiber/can-resume?" "fiber/current" "fiber/getenv" + "fiber/last-value" "fiber/maxstack" "fiber/new" "fiber/root" + "fiber/setenv" "fiber/setmaxstack" "fiber/status" + "fiber?" + "file/close" "file/flush" "file/open" "file/read" "file/seek" + "file/tell" "file/temp" "file/write" + "filter" "find" "find-index" "first" "flatten" "flatten-into" + "flush" "flycheck" "freeze" "frequencies" "from-pairs" + "function?" + "gccollect" "gcinterval" "gcsetinterval" + "gensym" "get" "get-in" "getline" "getproto" "group-by" + "hash" + "idempotent?" "identity" "import*" "in" "inc" "index-of" + "indexed?" + "int/s64" "int/to-bytes" "int/to-number" "int/u64" + "int?" + "interleave" "interpose" "invert" + "juxt*" + "keep" "keep-syntax" "keep-syntax!" "keys" + "keyword" + "keyword/slice" + "keyword?" + "kvs" + "last" "length" "load-image" + "macex" "macex1" "maclintf" + "make-env" "make-image" "map" "mapcat" "marshal" + "math/abs" "math/acos" "math/acosh" "math/asin" "math/asinh" + "math/atan" "math/atan2" "math/atanh" "math/cbrt" "math/ceil" + "math/cos" "math/cosh" "math/erf" "math/erfc" "math/exp" + "math/exp2" "math/expm1" "math/floor" "math/gamma" "math/gcd" + "math/hypot" "math/lcm" "math/log" "math/log-gamma" + "math/log10" "math/log1p" "math/log2" "math/next" "math/pow" + "math/random" "math/rng" "math/rng-buffer" "math/rng-int" + "math/rng-uniform" "math/round" "math/seedrandom" "math/sin" + "math/sinh" "math/sqrt" "math/tan" "math/tanh" "math/trunc" + "max" "max-of" "mean" "memcmp" "merge" "merge-into" + "merge-module" "min" "min-of" "mod" + "module/add-paths" "module/expand-path" "module/find" + "module/value" + "nan?" "nat?" "native" "neg?" + "net/accept" "net/accept-loop" "net/address" + "net/address-unpack" "net/chunk" "net/close" "net/connect" + "net/flush" "net/listen" "net/localname" "net/peername" + "net/read" "net/recv-from" "net/send-to" "net/server" + "net/shutdown" "net/write" + "next" + "nil?" + "not" "not=" + "number?" + "odd?" "one?" + "os/arch" "os/cd" "os/chmod" "os/clock" "os/compiler" + "os/cpu-count" "os/cryptorand" "os/cwd" "os/date" "os/dir" + "os/environ" "os/execute" "os/exit" "os/getenv" "os/link" + "os/lstat" "os/mkdir" "os/mktime" "os/open" "os/perm-int" + "os/perm-string" "os/pipe" "os/proc-close" "os/proc-kill" + "os/proc-wait" "os/readlink" "os/realpath" "os/rename" + "os/rm" "os/rmdir" "os/setenv" "os/shell" "os/sleep" + "os/spawn" "os/stat" "os/symlink" "os/time" "os/touch" + "os/umask" "os/which" + "pairs" + "parse" "parse-all" + "parser/byte" "parser/clone" "parser/consume" "parser/eof" + "parser/error" "parser/flush" "parser/has-more" + "parser/insert" "parser/new" "parser/produce" "parser/state" + "parser/status" "parser/where" + "partial" "partition" "partition-by" + "peg/compile" "peg/find" "peg/find-all" "peg/match" + "peg/replace" "peg/replace-all" + "pos?" "postwalk" "pp" "prewalk" + "prin" "prinf" "print" "printf" + "product" "propagate" "put" "put-in" + "quit" + "range" "reduce" "reduce2" "repl" "require" "resume" + "return" "reverse" "reverse!" "run-context" + "sandbox" "scan-number" "setdyn" "signal" "slice" "slurp" + "some" "sort" "sort-by" "sorted" "sorted-by" "spit" + "string" + "string/ascii-lower" "string/ascii-upper" "string/bytes" + "string/check-set" "string/find" "string/find-all" + "string/format" "string/from-bytes" "string/has-prefix?" + "string/has-suffix?" "string/join" "string/repeat" + "string/replace" "string/replace-all" "string/reverse" + "string/slice" "string/split" "string/trim" "string/triml" + "string/trimr" + "string?" + "struct" + "struct/getproto" "struct/proto-flatten" "struct/to-table" + "struct/with-proto" + "struct?" + "sum" + "symbol" + "symbol/slice" + "symbol?" + "table" + "table/clear" "table/clone" "table/getproto" "table/new" + "table/proto-flatten" "table/rawget" "table/setproto" + "table/to-struct" + "table?" + "take" "take-until" "take-while" + ;; XXX: obsolete + "tarray/buffer" "tarray/copy-bytes" "tarray/length" + "tarray/new" "tarray/properties" "tarray/slice" + "tarray/swap-bytes" + ;; XXX: obsolete + "thread/close" "thread/current" "thread/exit" "thread/new" + "thread/receive" "thread/send" + ;; end of obsolete + "trace" "true?" "truthy?" + "tuple" + "tuple/brackets" "tuple/setmap" "tuple/slice" + "tuple/sourcemap" "tuple/type" + "tuple?" + "type" + "unmarshal" "untrace" "update" "update-in" + "values" "varglobal" + "walk" "warn-compile" + "xprin" "xprinf" "xprint" "xprintf" + "yield" + "zero?" "zipcoll")) diff --git a/tree-sitter/highlights/java.scm b/tree-sitter/highlights/java.scm new file mode 100644 index 0000000000..28c4500594 --- /dev/null +++ b/tree-sitter/highlights/java.scm @@ -0,0 +1,296 @@ +; CREDITS @maxbrunsfeld (maxbrunsfeld@gmail.com) + +; Variables + +(identifier) @variable + +; Methods + +(method_declaration + name: (identifier) @method) +(method_invocation + name: (identifier) @method.call) + +(super) @function.builtin + +; Parameters + +(formal_parameter + name: (identifier) @parameter) + +(catch_formal_parameter + name: (identifier) @parameter) + +(spread_parameter + (variable_declarator + name: (identifier) @parameter)) ; int... foo + +;; Lambda parameter + +(inferred_parameters (identifier) @parameter) ; (x,y) -> ... + +(lambda_expression + parameters: (identifier) @parameter) ; x -> ... + +; Annotations + +(annotation + name: (identifier) @attribute) +(marker_annotation + name: (identifier) @attribute) + +; Operators + +[ + "@" + "+" + ":" + "++" + "-" + "--" + "&" + "&&" + "|" + "||" + "!" + "!=" + "==" + "*" + "/" + "%" + "<" + "<=" + ">" + ">=" + "=" + "-=" + "+=" + "*=" + "/=" + "%=" + "->" + "^" + "^=" + "&=" + "|=" + "~" + ">>" + ">>>" + "<<" + "::" +] @operator + +; Types + +(interface_declaration + name: (identifier) @type) +(annotation_type_declaration + name: (identifier) @type) +(class_declaration + name: (identifier) @type) +(record_declaration + name: (identifier) @type) +(enum_declaration + name: (identifier) @type) +(constructor_declaration + name: (identifier) @type) +(type_identifier) @type +((method_invocation + object: (identifier) @type) + (#lua-match? @type "^[A-Z]")) +((method_reference + . (identifier) @type) + (#lua-match? @type "^[A-Z]")) + +((field_access + object: (identifier) @type) + (#lua-match? @type "^[A-Z]")) +((scoped_identifier + scope: (identifier) @type) + (#lua-match? @type "^[A-Z]")) + +; Fields + +(field_declaration + declarator: (variable_declarator + name: (identifier) @field)) + +(field_access + field: (identifier) @field) + +[ + (boolean_type) + (integral_type) + (floating_point_type) + (void_type) +] @type.builtin + +; Variables + +((identifier) @constant + (#lua-match? @constant "^[A-Z_][A-Z%d_]+$")) + +(this) @variable.builtin + +; Literals + +(string_literal) @string + +(escape_sequence) @string.escape + +(character_literal) @character + +[ + (hex_integer_literal) + (decimal_integer_literal) + (octal_integer_literal) + (binary_integer_literal) +] @number + +[ + (decimal_floating_point_literal) + (hex_floating_point_literal) +] @float + +[ + (true) + (false) +] @boolean + +(null_literal) @constant.builtin + +; Keywords + +[ + "assert" + "class" + "record" + "default" + "enum" + "extends" + "implements" + "instanceof" + "interface" + "@interface" + "permits" + "to" + "with" +] @keyword + +(synchronized_statement + "synchronized" @keyword) + +[ + "abstract" + "final" + "native" + "non-sealed" + "open" + "private" + "protected" + "public" + "sealed" + "static" + "strictfp" + "transitive" +] @type.qualifier + +(modifiers + "synchronized" @type.qualifier) + +[ + "transient" + "volatile" +] @storageclass + +[ + "return" + "yield" +] @keyword.return + +[ + "new" +] @keyword.operator + +; Conditionals + +[ + "if" + "else" + "switch" + "case" +] @conditional + +(ternary_expression ["?" ":"] @conditional.ternary) + +; Loops + +[ + "for" + "while" + "do" + "continue" + "break" +] @repeat + +; Includes + +[ + "exports" + "import" + "module" + "opens" + "package" + "provides" + "requires" + "uses" +] @include + +; Punctuation + +[ + ";" + "." + "..." + "," +] @punctuation.delimiter + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +(type_arguments [ "<" ">" ] @punctuation.bracket) +(type_parameters [ "<" ">" ] @punctuation.bracket) + +; Exceptions + +[ + "throw" + "throws" + "finally" + "try" + "catch" +] @exception + +; Labels + +(labeled_statement + (identifier) @label) + +; Comments + +[ + (line_comment) + (block_comment) +] @comment @spell + +((block_comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +((line_comment) @comment.documentation + (#lua-match? @comment.documentation "^///[^/]")) + +((line_comment) @comment.documentation + (#lua-match? @comment.documentation "^///$")) diff --git a/tree-sitter/highlights/javascript.scm b/tree-sitter/highlights/javascript.scm new file mode 100644 index 0000000000..8fc69e6c7c --- /dev/null +++ b/tree-sitter/highlights/javascript.scm @@ -0,0 +1,55 @@ +; inherits: ecma,jsx + +;;; Parameters +(formal_parameters (identifier) @parameter) + +(formal_parameters + (rest_pattern + (identifier) @parameter)) + +;; ({ a }) => null +(formal_parameters + (object_pattern + (shorthand_property_identifier_pattern) @parameter)) + +;; ({ a = b }) => null +(formal_parameters + (object_pattern + (object_assignment_pattern + (shorthand_property_identifier_pattern) @parameter))) + +;; ({ a: b }) => null +(formal_parameters + (object_pattern + (pair_pattern + value: (identifier) @parameter))) + +;; ([ a ]) => null +(formal_parameters + (array_pattern + (identifier) @parameter)) + +;; ({ a } = { a }) => null +(formal_parameters + (assignment_pattern + (object_pattern + (shorthand_property_identifier_pattern) @parameter))) + +;; ({ a = b } = { a }) => null +(formal_parameters + (assignment_pattern + (object_pattern + (object_assignment_pattern + (shorthand_property_identifier_pattern) @parameter)))) + +;; a => null +(arrow_function + parameter: (identifier) @parameter) + +;; optional parameters +(formal_parameters + (assignment_pattern + left: (identifier) @parameter)) + +;; punctuation +(optional_chain) @punctuation.delimiter diff --git a/tree-sitter/highlights/jq.scm b/tree-sitter/highlights/jq.scm new file mode 100644 index 0000000000..120cbe2eb1 --- /dev/null +++ b/tree-sitter/highlights/jq.scm @@ -0,0 +1,330 @@ +; Variables + +(variable) @variable + +((variable) @constant.builtin + (#eq? @constant.builtin "$ENV")) + +((variable) @constant.macro + (#eq? @constant.macro "$__loc__")) + +; Properties + +(index + (identifier) @property) + +; Labels + +(query + label: (variable) @label) + +(query + break_statement: (variable) @label) + +; Literals + +(number) @number + +(string) @string + +[ + "true" + "false" +] @boolean + +("null") @type.builtin + +; Interpolation + +["\\(" ")"] @character.special + +; Format + +(format) @attribute + +; Functions + +(funcdef + (identifier) @function) + +(funcdefargs + (identifier) @parameter) + +[ + "reduce" + "foreach" +] @function.builtin + +; jq -n 'builtins | map(split("/")[0]) | unique | .[]' +((funcname) @function.builtin + (#any-of? @function.builtin + "IN" + "INDEX" + "JOIN" + "acos" + "acosh" + "add" + "all" + "any" + "arrays" + "ascii_downcase" + "ascii_upcase" + "asin" + "asinh" + "atan" + "atan2" + "atanh" + "booleans" + "bsearch" + "builtins" + "capture" + "cbrt" + "ceil" + "combinations" + "contains" + "copysign" + "cos" + "cosh" + "debug" + "del" + "delpaths" + "drem" + "empty" + "endswith" + "env" + "erf" + "erfc" + "error" + "exp" + "exp10" + "exp2" + "explode" + "expm1" + "fabs" + "fdim" + "finites" + "first" + "flatten" + "floor" + "fma" + "fmax" + "fmin" + "fmod" + "format" + "frexp" + "from_entries" + "fromdate" + "fromdateiso8601" + "fromjson" + "fromstream" + "gamma" + "get_jq_origin" + "get_prog_origin" + "get_search_list" + "getpath" + "gmtime" + "group_by" + "gsub" + "halt" + "halt_error" + "has" + "hypot" + "implode" + "in" + "index" + "indices" + "infinite" + "input" + "input_filename" + "input_line_number" + "inputs" + "inside" + "isempty" + "isfinite" + "isinfinite" + "isnan" + "isnormal" + "iterables" + "j0" + "j1" + "jn" + "join" + "keys" + "keys_unsorted" + "last" + "ldexp" + "leaf_paths" + "length" + "lgamma" + "lgamma_r" + "limit" + "localtime" + "log" + "log10" + "log1p" + "log2" + "logb" + "ltrimstr" + "map" + "map_values" + "match" + "max" + "max_by" + "min" + "min_by" + "mktime" + "modf" + "modulemeta" + "nan" + "nearbyint" + "nextafter" + "nexttoward" + "normals" + "not" + "now" + "nth" + "nulls" + "numbers" + "objects" + "path" + "paths" + "pow" + "pow10" + "range" + "recurse" + "recurse_down" + "remainder" + "repeat" + "reverse" + "rindex" + "rint" + "round" + "rtrimstr" + "scalars" + "scalars_or_empty" + "scalb" + "scalbln" + "scan" + "select" + "setpath" + "significand" + "sin" + "sinh" + "sort" + "sort_by" + "split" + "splits" + "sqrt" + "startswith" + "stderr" + "strflocaltime" + "strftime" + "strings" + "strptime" + "sub" + "tan" + "tanh" + "test" + "tgamma" + "to_entries" + "todate" + "todateiso8601" + "tojson" + "tonumber" + "tostream" + "tostring" + "transpose" + "trunc" + "truncate_stream" + "type" + "unique" + "unique_by" + "until" + "utf8bytelength" + "values" + "walk" + "while" + "with_entries" + "y0" + "y1" + "yn")) + +; Keywords + +[ + "def" + "as" + "label" + "module" + "break" +] @keyword + +[ + "import" + "include" +] @include + +[ + "if" + "then" + "elif" + "else" + "end" +] @conditional + +[ + "try" + "catch" +] @exception + +[ + "or" + "and" +] @keyword.operator + +; Operators + +[ + "." + "==" + "!=" + ">" + ">=" + "<=" + "<" + "=" + "+" + "-" + "*" + "/" + "%" + "+=" + "-=" + "*=" + "/=" + "%=" + "//=" + "|" + "?" + "//" + "?//" + (recurse) ; ".." +] @operator + +; Punctuation + +[ + ";" + "," + ":" +] @punctuation.delimiter + +[ + "[" "]" + "{" "}" + "(" ")" +] @punctuation.bracket + +; Comments + +(comment) @comment @spell diff --git a/tree-sitter/highlights/jsdoc.scm b/tree-sitter/highlights/jsdoc.scm new file mode 100644 index 0000000000..4b4266c9fd --- /dev/null +++ b/tree-sitter/highlights/jsdoc.scm @@ -0,0 +1,2 @@ +(tag_name) @keyword +(type) @type diff --git a/tree-sitter/highlights/json.scm b/tree-sitter/highlights/json.scm new file mode 100644 index 0000000000..c4bfade8ab --- /dev/null +++ b/tree-sitter/highlights/json.scm @@ -0,0 +1,32 @@ +[ + (true) + (false) +] @boolean + +(null) @constant.builtin + +(number) @number + +(pair key: (string) @label) +(pair value: (string) @string) + +(array (string) @string) + +(string_content) @spell + +(ERROR) @error + +["," ":"] @punctuation.delimiter + +[ + "[" "]" + "{" "}" +] @punctuation.bracket + +(("\"" @conceal) + (#set! conceal "")) + +(escape_sequence) @string.escape +((escape_sequence) @conceal + (#eq? @conceal "\\\"") + (#set! conceal "\"")) diff --git a/tree-sitter/highlights/json5.scm b/tree-sitter/highlights/json5.scm new file mode 100644 index 0000000000..4e41971ba7 --- /dev/null +++ b/tree-sitter/highlights/json5.scm @@ -0,0 +1,17 @@ +[ + "true" + "false" +] @boolean + +"null" @constant + +(string) @string + +(number) @number + +(comment) @comment + +(member + name: (_) @keyword) + +(ERROR) @error diff --git a/tree-sitter/highlights/jsonc.scm b/tree-sitter/highlights/jsonc.scm new file mode 100644 index 0000000000..e50112155f --- /dev/null +++ b/tree-sitter/highlights/jsonc.scm @@ -0,0 +1,3 @@ +; inherits: json + +(comment) @comment @spell diff --git a/tree-sitter/highlights/jsonnet.scm b/tree-sitter/highlights/jsonnet.scm new file mode 100644 index 0000000000..4267c988fe --- /dev/null +++ b/tree-sitter/highlights/jsonnet.scm @@ -0,0 +1,96 @@ +[ + (true) + (false) +] @boolean + +(comment) @comment +(id) @variable +(import) @include +(null) @constant.builtin +(number) @number +(string) @string + +(fieldname (id) @label) + +[ + "[" + "]" + "{" + "}" + "(" + ")" +] @punctuation.bracket + +[ + "." + "," + ";" + ":" + "::" + ":::" +] @punctuation.delimiter + +(unaryop) @operator +[ + "+" + "-" + "*" + "/" + "%" + "^" + "==" + "!=" + "<=" + ">=" + "<" + ">" + "=" + "&" + "|" + "<<" + ">>" + "&&" + "||" +] @operator + +"for" @repeat + +"function" @keyword.function + +"in" @keyword.operator + +[ + (local) + "assert" +] @keyword + +[ + "else" + "if" + "then" +] @conditional + +[ + (dollar) + (self) +] @variable.builtin +((id) @variable.builtin + (#eq? @variable.builtin "std")) + +; Function declaration +(bind + function: (id) @function + params: (params + (param + identifier: (id) @parameter))) + +; Function call +(expr + (expr (id) @function.call) + "(" + (args + (named_argument + (id) @parameter))? + ")") + +(ERROR) @error diff --git a/tree-sitter/highlights/jsx.scm b/tree-sitter/highlights/jsx.scm new file mode 100644 index 0000000000..718add87fa --- /dev/null +++ b/tree-sitter/highlights/jsx.scm @@ -0,0 +1,35 @@ +(jsx_element + open_tag: (jsx_opening_element ["<" ">"] @tag.delimiter)) +(jsx_element + close_tag: (jsx_closing_element [""] @tag.delimiter)) +(jsx_self_closing_element ["<" "/>"] @tag.delimiter) +(jsx_attribute (property_identifier) @tag.attribute) + +(jsx_opening_element + name: (identifier) @tag) + +(jsx_closing_element + name: (identifier) @tag) + +(jsx_self_closing_element + name: (identifier) @tag) + +(jsx_opening_element ((identifier) @constructor + (#lua-match? @constructor "^[A-Z]"))) + +; Handle the dot operator effectively - +(jsx_opening_element ((member_expression (identifier) @tag (property_identifier) @constructor))) + +(jsx_closing_element ((identifier) @constructor + (#lua-match? @constructor "^[A-Z]"))) + +; Handle the dot operator effectively - +(jsx_closing_element ((member_expression (identifier) @tag (property_identifier) @constructor))) + +(jsx_self_closing_element ((identifier) @constructor + (#lua-match? @constructor "^[A-Z]"))) + +; Handle the dot operator effectively - +(jsx_self_closing_element ((member_expression (identifier) @tag (property_identifier) @constructor))) + +(jsx_text) @none diff --git a/tree-sitter/highlights/julia.scm b/tree-sitter/highlights/julia.scm new file mode 100644 index 0000000000..10e7ce6d01 --- /dev/null +++ b/tree-sitter/highlights/julia.scm @@ -0,0 +1,299 @@ +;;; Identifiers + +(identifier) @variable + +; ;; If you want type highlighting based on Julia naming conventions (this might collide with mathematical notation) +; ((identifier) @type +; (match? @type "^[A-Z][^_]")) ; exception: Highlight `A_foo` sort of identifiers as variables + +(macro_identifier) @function.macro +(macro_identifier + (identifier) @function.macro) ; for any one using the variable highlight + +(macro_definition + name: (identifier) @function.macro) + +(quote_expression + ":" @symbol + [(identifier) (operator)] @symbol) + +(field_expression + (identifier) @field .) + + +;;; Function names + +;; Definitions + +(function_definition + name: (identifier) @function) +(short_function_definition + name: (identifier) @function) + +(function_definition + name: (field_expression (identifier) @function .)) +(short_function_definition + name: (field_expression (identifier) @function .)) + +;; calls + +(call_expression + (identifier) @function.call) +(call_expression + (field_expression (identifier) @function.call .)) + +(broadcast_call_expression + (identifier) @function.call) +(broadcast_call_expression + (field_expression (identifier) @function.call .)) + +;; Builtins + +((identifier) @function.builtin + (#any-of? @function.builtin + "_abstracttype" "_apply_iterate" "_apply_pure" "_call_in_world" "_call_in_world_total" + "_call_latest" "_equiv_typedef" "_expr" "_primitivetype" "_setsuper!" "_structtype" + "_typebody!" "_typevar" "applicable" "apply_type" "arrayref" "arrayset" "arraysize" + "const_arrayref" "donotdelete" "fieldtype" "get_binding_type" "getfield" "ifelse" "invoke" "isa" + "isdefined" "modifyfield!" "nfields" "replacefield!" "set_binding_type!" "setfield!" "sizeof" + "svec" "swapfield!" "throw" "tuple" "typeassert" "typeof")) + + +;;; Parameters + +(parameter_list + (identifier) @parameter) +(optional_parameter . + (identifier) @parameter) +(slurp_parameter + (identifier) @parameter) + +(typed_parameter + parameter: (identifier)? @parameter + type: (_) @type) + +(function_expression + . (identifier) @parameter) ; Single parameter arrow functions + + +;;; Types + +;; Definitions + +(abstract_definition + name: (identifier) @type.definition) @keyword +(primitive_definition + name: (identifier) @type.definition) @keyword +(struct_definition + name: (identifier) @type) +(type_clause + [(identifier) @type + (field_expression (identifier) @type .)]) + +;; Annotations + +(parametrized_type_expression + (_) @type + (curly_expression (_) @type)) + +(type_parameter_list + (identifier) @type) + +(typed_expression + (identifier) @type .) + +(function_definition + return_type: (identifier) @type) +(short_function_definition + return_type: (identifier) @type) + +(where_clause + (identifier) @type) +(where_clause + (curly_expression (_) @type)) + +;; Builtins + +((identifier) @type.builtin + (#any-of? @type.builtin + "Type" "DataType" "Any" "Union" "UnionAll" "Tuple" "NTuple" "NamedTuple" + "Val" "Nothing" "Some" "Enum" "Expr" "Symbol" "Module" "Function" "ComposedFunction" + "Number" "Real" "AbstractFloat" "Integer" "Signed" "AbstractIrrational" + "Fix1" "Fix2" "Missing" "Cmd" "EnvDict" "VersionNumber" "ArgumentError" + "AssertionError" "BoundsError" "CompositeException" "DimensionMismatch" + "DivideError" "DomainError" "EOFError" "ErrorException" "InexactError" + "InterruptException" "KeyError" "LoadError" "MethodError" "OutOfMemoryError" + "ReadOnlyMemoryError" "OverflowError" "ProcessFailedException" "StackOverflowError" + "SystemError" "TypeError" "UndefKeywordError" "UndefRefError" "UndefVarError" + "StringIndexError" "InitError" "ExponentialBackOff" "Timer" "AsyncCondition" + "ParseError" "QuoteNode" "IteratorSize" "IteratorEltype" "AbstractRange" + "OrdinalRange" "AbstractUnitRange" "StepRange" "UnitRange" "LinRange" "AbstractDict" + "Dict" "IdDict" "WeakKeyDict" "ImmutableDict" "AbstractSet" "Set" "BitSet" "Pair" + "Pairs" "OneTo" " StepRangeLen" "RoundingMode" "Float16" "Float32" "Float64" + "BigFloat" "Bool" "Int" "Int8" "UInt8" "Int16" "UInt16" "Int32" "UInt32" "Int64" + "UInt64" "Int128" "UInt128" "BigInt" "Complex" "Rational" "Irrational" "AbstractChar" + "Char" "SubString" "Regex" "SubstitutionString" "RegexMatch" "AbstractArray" + "AbstractVector" "AbstractMatrix" "AbstractVecOrMat" "Array" "UndefInitializer" + "Vector" "Matrix" "VecOrMat" "DenseArray" "DenseVector" "DenseMatrix" "DenseVecOrMat" + "StridedArray" "StridedVector" "StridedMatrix" "StridedVecOrMat" "BitArray" "Dims" + "SubArray" "Task" "Condition" "Event" "Semaphore" "AbstractLniock" "ReentrantLock" + "Channel" "Atomic" "SpinLock" "RawFD" "IOStream" "IOBuffer" "AbstractDisplay" "MIME" + "TextDisplay" "PartialQuickSort" "Ordering" "ReverseOrdering" "By" "Lt" "Perm" + "Stateful" "CFunction" "Ptr" "Ref" "Cchar" "Cuchar" "Cshort" "Cstring" "Cushort" + "Cint" "Cuint" "Clong" "Culong" "Clonglong" "Culonglong" "Cintmax_t" "Cuintmax_t" + "Csize_t" "Cssize_t" "Cptrdiff_t" "Cwchar_t" "Cwstring" "Cfloat" "Cdouble" "Tmstruct" + "StackFrame" "StackTrace")) + +((identifier) @variable.builtin + (#any-of? @variable.builtin "begin" "end") + (#has-ancestor? @variable.builtin index_expression)) + +((identifier) @variable.builtin + (#any-of? @variable.builtin "begin" "end") + (#has-ancestor? @variable.builtin range_expression)) + +;;; Keywords + +[ + "global" + "local" + "macro" + "struct" + "end" +] @keyword + + +(compound_statement + ["begin" "end"] @keyword) +(quote_statement + ["quote" "end"] @keyword) +(let_statement + ["let" "end"] @keyword) + +(if_statement + ["if" "end"] @conditional) +(elseif_clause + "elseif" @conditional) +(else_clause + "else" @conditional) +(if_clause + "if" @conditional) ; `if` clause in comprehensions +(ternary_expression + ["?" ":"] @conditional.ternary) + +(try_statement + ["try" "end"] @exception) +(finally_clause + "finally" @exception) +(catch_clause + "catch" @exception) + +(for_statement + ["for" "end"] @repeat) +(while_statement + ["while" "end"] @repeat) +(for_clause + "for" @repeat) +[ + (break_statement) + (continue_statement) +] @repeat + +(module_definition + ["module" "baremodule" "end"] @include) +(import_statement + ["import" "using"] @include) +(import_alias + "as" @include) +(export_statement + "export" @include) + +(macro_definition + ["macro" "end" @keyword]) + +(function_definition + ["function" "end"] @keyword.function) +(do_clause + ["do" "end"] @keyword.function) +(return_statement + "return" @keyword.return) + +[ + "const" + "mutable" +] @type.qualifier + + +;;; Operators & Punctuation + +[ + "=" + "∈" + (operator) +] @operator + +(adjoint_expression "'" @operator) +(range_expression ":" @operator) + +((operator) @keyword.operator + (#any-of? @keyword.operator "in" "isa")) + +(for_binding "in" @keyword.operator) + +(where_clause "where" @keyword.operator) +(where_expression "where" @keyword.operator) + +[ + "," + "." + ";" + "::" + "->" +] @punctuation.delimiter + +[ + "..." +] @punctuation.special + +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + + +;;; Literals + +(boolean_literal) @boolean +(integer_literal) @number +(float_literal) @float + +((identifier) @float + (#any-of? @float "NaN" "NaN16" "NaN32" + "Inf" "Inf16" "Inf32")) + +((identifier) @constant.builtin + (#any-of? @constant.builtin "nothing" "missing")) + +(character_literal) @character +(escape_sequence) @string.escape + +(string_literal) @string +(prefixed_string_literal + prefix: (identifier) @function.macro) @string + +(command_literal) @string.special +(prefixed_command_literal + prefix: (identifier) @function.macro) @string.special + +((string_literal) @string.documentation + . [ + (module_definition) + (abstract_definition) + (struct_definition) + (function_definition) + (short_function_definition) + (assignment) + (const_statement) + ]) + +[ + (line_comment) + (block_comment) +] @comment @spell diff --git a/tree-sitter/highlights/kdl.scm b/tree-sitter/highlights/kdl.scm new file mode 100644 index 0000000000..d903128a88 --- /dev/null +++ b/tree-sitter/highlights/kdl.scm @@ -0,0 +1,58 @@ +; Types + +(node (identifier) @type) + +(type) @type + +(annotation_type) @type.builtin + +; Properties + +(prop (identifier) @property) + +; Variables + +(identifier) @variable + +; Operators +[ + "=" + "+" + "-" +] @operator + +; Literals + +(string) @string + +(escape) @string.escape + +(number) @number + +(number (decimal) @float) +(number (exponent) @float) + +(boolean) @boolean + +"null" @constant.builtin + +; Punctuation + +["{" "}"] @punctuation.bracket + +["(" ")"] @punctuation.bracket + +[ + ";" +] @punctuation.delimiter + +; Comments + +[ + (single_line_comment) + (multi_line_comment) +] @comment @spell + +(node (node_comment) (#set! "priority" 105)) @comment +(node (node_field (node_field_comment) (#set! "priority" 105)) @comment) +(node_children (node_children_comment) (#set! "priority" 105)) @comment diff --git a/tree-sitter/highlights/kotlin.scm b/tree-sitter/highlights/kotlin.scm new file mode 100644 index 0000000000..f7b1b8c4d6 --- /dev/null +++ b/tree-sitter/highlights/kotlin.scm @@ -0,0 +1,422 @@ +;;; Identifiers + +(simple_identifier) @variable + +; `it` keyword inside lambdas +; FIXME: This will highlight the keyword outside of lambdas since tree-sitter +; does not allow us to check for arbitrary nestation +((simple_identifier) @variable.builtin +(#eq? @variable.builtin "it")) + +; `field` keyword inside property getter/setter +; FIXME: This will highlight the keyword outside of getters and setters +; since tree-sitter does not allow us to check for arbitrary nestation +((simple_identifier) @variable.builtin +(#eq? @variable.builtin "field")) + +; `this` this keyword inside classes +(this_expression) @variable.builtin + +; `super` keyword inside classes +(super_expression) @variable.builtin + +(class_parameter + (simple_identifier) @property) + +(class_body + (property_declaration + (variable_declaration + (simple_identifier) @property))) + +; id_1.id_2.id_3: `id_2` and `id_3` are assumed as object properties +(_ + (navigation_suffix + (simple_identifier) @property)) + +; SCREAMING CASE identifiers are assumed to be constants +((simple_identifier) @constant +(#lua-match? @constant "^[A-Z][A-Z0-9_]*$")) + +(_ + (navigation_suffix + (simple_identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z0-9_]*$"))) + +(enum_entry + (simple_identifier) @constant) + +(type_identifier) @type + +; '?' operator, replacement for Java @Nullable +(nullable_type) @punctuation.special + +(type_alias + (type_identifier) @type.definition) + +((type_identifier) @type.builtin + (#any-of? @type.builtin + "Byte" + "Short" + "Int" + "Long" + "UByte" + "UShort" + "UInt" + "ULong" + "Float" + "Double" + "Boolean" + "Char" + "String" + "Array" + "ByteArray" + "ShortArray" + "IntArray" + "LongArray" + "UByteArray" + "UShortArray" + "UIntArray" + "ULongArray" + "FloatArray" + "DoubleArray" + "BooleanArray" + "CharArray" + "Map" + "Set" + "List" + "EmptyMap" + "EmptySet" + "EmptyList" + "MutableMap" + "MutableSet" + "MutableList" +)) + +(package_header "package" @keyword + . (identifier (simple_identifier) @namespace)) + +(import_header + "import" @include) + +; The last `simple_identifier` in a `import_header` will always either be a function +; or a type. Classes can appear anywhere in the import path, unlike functions +(import_header + (identifier + (simple_identifier) @type @_import) + (import_alias + (type_identifier) @type.definition)? + (#lua-match? @_import "^[A-Z]")) + +(import_header + (identifier + (simple_identifier) @function @_import .) + (import_alias + (type_identifier) @function)? + (#lua-match? @_import "^[a-z]")) + +; TODO: Separate labeled returns/breaks/continue/super/this +; Must be implemented in the parser first +(label) @label + +;;; Function definitions + +(function_declaration + (simple_identifier) @function) + +(getter + ("get") @function.builtin) +(setter + ("set") @function.builtin) + +(primary_constructor) @constructor +(secondary_constructor + ("constructor") @constructor) + +(constructor_invocation + (user_type + (type_identifier) @constructor)) + +(anonymous_initializer + ("init") @constructor) + +(parameter + (simple_identifier) @parameter) + +(parameter_with_optional_type + (simple_identifier) @parameter) + +; lambda parameters +(lambda_literal + (lambda_parameters + (variable_declaration + (simple_identifier) @parameter))) + +;;; Function calls + +; function() +(call_expression + . (simple_identifier) @function.call) + +; ::function +(callable_reference + . (simple_identifier) @function.call) + +; object.function() or object.property.function() +(call_expression + (navigation_expression + (navigation_suffix + (simple_identifier) @function.call) . )) + +(call_expression + . (simple_identifier) @function.builtin + (#any-of? @function.builtin + "arrayOf" + "arrayOfNulls" + "byteArrayOf" + "shortArrayOf" + "intArrayOf" + "longArrayOf" + "ubyteArrayOf" + "ushortArrayOf" + "uintArrayOf" + "ulongArrayOf" + "floatArrayOf" + "doubleArrayOf" + "booleanArrayOf" + "charArrayOf" + "emptyArray" + "mapOf" + "setOf" + "listOf" + "emptyMap" + "emptySet" + "emptyList" + "mutableMapOf" + "mutableSetOf" + "mutableListOf" + "print" + "println" + "error" + "TODO" + "run" + "runCatching" + "repeat" + "lazy" + "lazyOf" + "enumValues" + "enumValueOf" + "assert" + "check" + "checkNotNull" + "require" + "requireNotNull" + "with" + "suspend" + "synchronized" +)) + +;;; Literals + +[ + (line_comment) + (multiline_comment) +] @comment @spell + +((multiline_comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +(shebang_line) @preproc + +(real_literal) @float +[ + (integer_literal) + (long_literal) + (hex_literal) + (bin_literal) + (unsigned_literal) +] @number + +[ + "null" ; should be highlighted the same as booleans + (boolean_literal) +] @boolean + +(character_literal) @character + +(string_literal) @string + +; NOTE: Escapes not allowed in multi-line strings +(character_literal (character_escape_seq) @string.escape) + +; There are 3 ways to define a regex +; - "[abc]?".toRegex() +(call_expression + (navigation_expression + ((string_literal) @string.regex) + (navigation_suffix + ((simple_identifier) @_function + (#eq? @_function "toRegex"))))) + +; - Regex("[abc]?") +(call_expression + ((simple_identifier) @_function + (#eq? @_function "Regex")) + (call_suffix + (value_arguments + (value_argument + (string_literal) @string.regex)))) + +; - Regex.fromLiteral("[abc]?") +(call_expression + (navigation_expression + ((simple_identifier) @_class + (#eq? @_class "Regex")) + (navigation_suffix + ((simple_identifier) @_function + (#eq? @_function "fromLiteral")))) + (call_suffix + (value_arguments + (value_argument + (string_literal) @string.regex)))) + +;;; Keywords + +(type_alias "typealias" @keyword) + +(companion_object "companion" @keyword) + +[ + (class_modifier) + (member_modifier) + (function_modifier) + (property_modifier) + (platform_modifier) + (variance_modifier) + (parameter_modifier) + (visibility_modifier) + (reification_modifier) + (inheritance_modifier) +] @type.qualifier + +[ + "val" + "var" + "enum" + "class" + "object" + "interface" +; "typeof" ; NOTE: It is reserved for future use +] @keyword + +[ + "suspend" +] @keyword.coroutine + +[ + "fun" +] @keyword.function + +(jump_expression) @keyword.return + +[ + "if" + "else" + "when" +] @conditional + +[ + "for" + "do" + "while" +] @repeat + +[ + "try" + "catch" + "throw" + "finally" +] @exception + + +(annotation + "@" @attribute (use_site_target)? @attribute) +(annotation + (user_type + (type_identifier) @attribute)) +(annotation + (constructor_invocation + (user_type + (type_identifier) @attribute))) + +(file_annotation + "@" @attribute "file" @attribute ":" @attribute) +(file_annotation + (user_type + (type_identifier) @attribute)) +(file_annotation + (constructor_invocation + (user_type + (type_identifier) @attribute))) + +;;; Operators & Punctuation + +[ + "!" + "!=" + "!==" + "=" + "==" + "===" + ">" + ">=" + "<" + "<=" + "||" + "&&" + "+" + "++" + "+=" + "-" + "--" + "-=" + "*" + "*=" + "/" + "/=" + "%" + "%=" + "?." + "?:" + "!!" + "is" + "!is" + "in" + "!in" + "as" + "as?" + ".." + "->" +] @operator + +[ + "(" ")" + "[" "]" + "{" "}" +] @punctuation.bracket + +[ + "." + "," + ";" + ":" + "::" +] @punctuation.delimiter + +; NOTE: `interpolated_identifier`s can be highlighted in any way +(string_literal + "$" @punctuation.special + (interpolated_identifier) @none @variable) +(string_literal + "${" @punctuation.special + (interpolated_expression) @none + "}" @punctuation.special) diff --git a/tree-sitter/highlights/lalrpop.scm b/tree-sitter/highlights/lalrpop.scm new file mode 100644 index 0000000000..b6fda252e4 --- /dev/null +++ b/tree-sitter/highlights/lalrpop.scm @@ -0,0 +1,60 @@ +[ + "enum" + "extern" + "grammar" + "match" + "type" + "pub" +] @keyword + +[ + "match" + "else" +] @conditional + +[ + "+" + "*" + "?" + ; TODO: inaccessible node + ; => + "=>@L" + "=>@R" +] @operator + +(grammar_type_params + ["<" ">"] @punctuation.bracket) + +(symbol + ["<" ">"] @punctuation.bracket) + +(binding_symbol + ["<" ">"] @punctuation.bracket) + +(binding_symbol + name: (identifier) @parameter) + +(bare_symbol + (macro + ((macro_id) @type.definition))) + +(bare_symbol + (identifier) @type.definition) + +(nonterminal_name + (macro_id) @type.definition) + +(nonterminal_name + (identifier) @type.definition) + +(nonterminal + (type_ref) @type.builtin) + +["(" ")" "[" "]"] @punctuation.bracket + +[";" ":"] @punctuation.delimiter + +(lifetime (identifier) @storageclass) + +(string_literal) @string +(regex_literal) @string diff --git a/tree-sitter/highlights/latex.scm b/tree-sitter/highlights/latex.scm new file mode 100644 index 0000000000..5d68cdfb01 --- /dev/null +++ b/tree-sitter/highlights/latex.scm @@ -0,0 +1,249 @@ +;; General syntax +(ERROR) @error + +(command_name) @function +(caption + command: _ @function) + +(key_value_pair + key: (_) @parameter + value: (_)) + +[ + (line_comment) + (block_comment) + (comment_environment) +] @comment + +((line_comment) @preproc + (#lua-match? @preproc "^%% !TeX")) + +[ + (brack_group) + (brack_group_argc) +] @parameter + +[(operator) "="] @operator + +"\\item" @punctuation.special + +((word) @punctuation.delimiter +(#eq? @punctuation.delimiter "&")) + +["[" "]" "{" "}"] @punctuation.bracket ; "(" ")" has no syntactical meaning in LaTeX + +;; General environments +(begin + command: _ @text.environment + name: (curly_group_text (text) @text.environment.name)) + +(end + command: _ @text.environment + name: (curly_group_text (text) @text.environment.name)) + +;; Definitions and references +(new_command_definition + command: _ @function.macro + declaration: (curly_group_command_name (_) @function)) +(old_command_definition + command: _ @function.macro + declaration: (_) @function) +(let_command_definition + command: _ @function.macro + declaration: (_) @function) + +(environment_definition + command: _ @function.macro + name: (curly_group_text (_) @text.reference)) + +(theorem_definition + command: _ @function.macro + name: (curly_group_text (_) @text.environment.name)) + +(paired_delimiter_definition + command: _ @function.macro + declaration: (curly_group_command_name (_) @function)) + +(label_definition + command: _ @function.macro + name: (curly_group_text (_) @text.reference)) +(label_reference_range + command: _ @function.macro + from: (curly_group_text (_) @text.reference) + to: (curly_group_text (_) @text.reference)) +(label_reference + command: _ @function.macro + names: (curly_group_text_list (_) @text.reference)) +(label_number + command: _ @function.macro + name: (curly_group_text (_) @text.reference) + number: (_) @text.reference) + +(citation + command: _ @function.macro + keys: (curly_group_text_list) @text.reference) + +(glossary_entry_definition + command: _ @function.macro + name: (curly_group_text (_) @text.reference)) +(glossary_entry_reference + command: _ @function.macro + name: (curly_group_text (_) @text.reference)) + +(acronym_definition + command: _ @function.macro + name: (curly_group_text (_) @text.reference)) +(acronym_reference + command: _ @function.macro + name: (curly_group_text (_) @text.reference)) + +(color_definition + command: _ @function.macro + name: (curly_group_text (_) @text.reference)) +(color_reference + command: _ @function.macro + name: (curly_group_text (_) @text.reference)) + +;; Math +[ + (displayed_equation) + (inline_formula) +] @text.math + +(math_environment + (begin + command: _ @text.math + name: (curly_group_text (text) @text.math))) + +(math_environment + (text) @text.math) + +(math_environment + (end + command: _ @text.math + name: (curly_group_text (text) @text.math))) + +;; Sectioning +(title_declaration + command: _ @namespace + options: (brack_group (_) @text.title.1)? + text: (curly_group (_) @text.title.1)) + +(author_declaration + command: _ @namespace + authors: (curly_group_author_list + ((author)+ @text.title.1))) + +(chapter + command: _ @namespace + toc: (brack_group (_) @text.title.2)? + text: (curly_group (_) @text.title.2)) + +(part + command: _ @namespace + toc: (brack_group (_) @text.title.2)? + text: (curly_group (_) @text.title.2)) + +(section + command: _ @namespace + toc: (brack_group (_) @text.title.3)? + text: (curly_group (_) @text.title.3)) + +(subsection + command: _ @namespace + toc: (brack_group (_) @text.title.4)? + text: (curly_group (_) @text.title.4)) + +(subsubsection + command: _ @namespace + toc: (brack_group (_) @text.title.5)? + text: (curly_group (_) @text.title.5)) + +(paragraph + command: _ @namespace + toc: (brack_group (_) @text.title.6)? + text: (curly_group (_) @text.title.6)) + +(subparagraph + command: _ @namespace + toc: (brack_group (_) @text.title.6)? + text: (curly_group (_) @text.title.6)) + +;; Beamer frames +(generic_environment + (begin + name: (curly_group_text + (text) @text.environment.name) + (#any-of? @text.environment.name "frame")) + . + (curly_group (_) @text.title)) + +((generic_command + command: (command_name) @_name + arg: (curly_group + (text) @text.title)) + (#eq? @_name "\\frametitle")) + +;; Formatting +((generic_command + command: (command_name) @_name + arg: (curly_group (_) @text.emphasis)) + (#eq? @_name "\\emph")) + +((generic_command + command: (command_name) @_name + arg: (curly_group (_) @text.emphasis)) + (#match? @_name "^(\\\\textit|\\\\mathit)$")) + +((generic_command + command: (command_name) @_name + arg: (curly_group (_) @text.strong)) + (#match? @_name "^(\\\\textbf|\\\\mathbf)$")) + +((generic_command + command: (command_name) @_name + . + arg: (curly_group (_) @text.uri)) + (#match? @_name "^(\\\\url|\\\\href)$")) + +;; File inclusion commands +(class_include + command: _ @include + path: (curly_group_path) @string) + +(package_include + command: _ @include + paths: (curly_group_path_list) @string) + +(latex_include + command: _ @include + path: (curly_group_path) @string) +(import_include + command: _ @include + directory: (curly_group_path) @string + file: (curly_group_path) @string) + +(bibtex_include + command: _ @include + path: (curly_group_path) @string) +(biblatex_include + "\\addbibresource" @include + glob: (curly_group_glob_pattern) @string.regex) + +(graphics_include + command: _ @include + path: (curly_group_path) @string) +(tikz_library_import + command: _ @include + paths: (curly_group_path_list) @string) + +(text) @spell +(inline_formula) @nospell +(displayed_equation) @nospell +(key_value_pair) @nospell +(generic_environment + begin: _ @nospell + end: _ @nospell) +(citation + keys: _ @nospell) +(command_name) @nospell diff --git a/tree-sitter/highlights/ledger.scm b/tree-sitter/highlights/ledger.scm new file mode 100644 index 0000000000..963de18826 --- /dev/null +++ b/tree-sitter/highlights/ledger.scm @@ -0,0 +1,54 @@ +[ + (block_comment) + (comment) + (note) + (test) +] @comment + +[ + (quantity) + (negative_quantity) +] @number + +[ + (date) + (effective_date) + (time) + (interval) +] @string.special + +[ + (commodity) + (option) + (option_value) + (check_in) + (check_out) +] @text.literal + +((account) @field) + +"include" @include + +[ + "account" + "alias" + "assert" + "check" + "commodity" + "comment" + "def" + "default" + "end" + "eval" + "format" + "nomarket" + "note" + "payee" + "test" + "A" + "Y" + "N" + "D" + "C" + "P" +] @keyword diff --git a/tree-sitter/highlights/llvm.scm b/tree-sitter/highlights/llvm.scm new file mode 100644 index 0000000000..e2857b02c8 --- /dev/null +++ b/tree-sitter/highlights/llvm.scm @@ -0,0 +1,166 @@ +[ + (local_var) + (global_var) +] @variable + +(type) @type +(type_keyword) @type.builtin + +(type [ + (local_var) + (global_var) + ] @type) + +(global_type + (local_var) @type.definition) + +(argument) @parameter + +(_ inst_name: _ @keyword.operator) + +[ + "catch" + "filter" +] @keyword.operator + +[ + "to" + "nuw" + "nsw" + "exact" + "unwind" + "from" + "cleanup" + "swifterror" + "volatile" + "inbounds" + "inrange" +] @keyword +(icmp_cond) @keyword +(fcmp_cond) @keyword + +(fast_math) @keyword + +(_ callee: _ @function) +(function_header name: _ @function) + +[ + "declare" + "define" +] @keyword.function +(calling_conv) @keyword.function + +[ + "target" + "triple" + "datalayout" + "source_filename" + "addrspace" + "blockaddress" + "align" + "syncscope" + "within" + "uselistorder" + "uselistorder_bb" + "module" + "asm" + "sideeffect" + "alignstack" + "inteldialect" + "unwind" + "type" + "global" + "constant" + "externally_initialized" + "alias" + "ifunc" + "section" + "comdat" + "any" + "exactmatch" + "largest" + "nodeduplicate" + "samesize" + "distinct" + "attributes" + "vscale" +] @keyword + + +[ + "no_cfi" + (dso_local) + (linkage_aux) + (visibility) +] @type.qualifier + +[ + "thread_local" + "localdynamic" + "initialexec" + "localexec" + (unnamed_addr) + (dll_storage_class) +] @storageclass + +(attribute_name) @attribute + +(function_header [ + (linkage) + (calling_conv) + (unnamed_addr) + ] @keyword.function) + +(number) @number +(comment) @comment +(string) @string +(cstring) @string +(label) @label +(_ inst_name: "ret" @keyword.return) +(float) @float + +[ + (struct_value) + (array_value) + (vector_value) +] @constructor + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<" + ">" + "<{" + "}>" +] @punctuation.bracket + +[ + "," + ":" +] @punctuation.delimiter + +[ + "=" + "|" + "x" + "..." +] @operator + +[ + "true" + "false" +] @boolean + +[ + "undef" + "poison" + "null" + "none" + "zeroinitializer" +] @constant.builtin + +(ERROR) @error diff --git a/tree-sitter/highlights/lua.scm b/tree-sitter/highlights/lua.scm new file mode 100644 index 0000000000..557ea7da0a --- /dev/null +++ b/tree-sitter/highlights/lua.scm @@ -0,0 +1,247 @@ +;; Keywords + +"return" @keyword.return + +[ + "goto" + "in" + "local" +] @keyword + +(break_statement) @keyword + +(do_statement +[ + "do" + "end" +] @keyword) + +(while_statement +[ + "while" + "do" + "end" +] @repeat) + +(repeat_statement +[ + "repeat" + "until" +] @repeat) + +(if_statement +[ + "if" + "elseif" + "else" + "then" + "end" +] @conditional) + +(elseif_statement +[ + "elseif" + "then" + "end" +] @conditional) + +(else_statement +[ + "else" + "end" +] @conditional) + +(for_statement +[ + "for" + "do" + "end" +] @repeat) + +(function_declaration +[ + "function" + "end" +] @keyword.function) + +(function_definition +[ + "function" + "end" +] @keyword.function) + +;; Operators + +[ + "and" + "not" + "or" +] @keyword.operator + +[ + "+" + "-" + "*" + "/" + "%" + "^" + "#" + "==" + "~=" + "<=" + ">=" + "<" + ">" + "=" + "&" + "~" + "|" + "<<" + ">>" + "//" + ".." +] @operator + +;; Punctuations + +[ + ";" + ":" + "::" + "," + "." +] @punctuation.delimiter + +;; Brackets + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +;; Variables + +(identifier) @variable + +((identifier) @constant.builtin + (#eq? @constant.builtin "_VERSION")) + +((identifier) @variable.builtin + (#eq? @variable.builtin "self")) + +((identifier) @namespace.builtin + (#any-of? @namespace.builtin "_G" "debug" "io" "jit" "math" "os" "package" "string" "table" "utf8")) + +((identifier) @keyword.coroutine + (#eq? @keyword.coroutine "coroutine")) + +(variable_list + attribute: (attribute + (["<" ">"] @punctuation.bracket + (identifier) @attribute))) + +;; Labels + +(label_statement (identifier) @label) + +(goto_statement (identifier) @label) + +;; Constants + +((identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) + +(vararg_expression) @constant + +(nil) @constant.builtin + +[ + (false) + (true) +] @boolean + +;; Tables + +(field name: (identifier) @field) + +(dot_index_expression field: (identifier) @field) + +(table_constructor +[ + "{" + "}" +] @constructor) + +;; Functions + +(parameters (identifier) @parameter) + +(function_declaration + name: [ + (identifier) @function + (dot_index_expression + field: (identifier) @function) + ]) + +(function_declaration + name: (method_index_expression + method: (identifier) @method)) + +(assignment_statement + (variable_list . + name: [ + (identifier) @function + (dot_index_expression + field: (identifier) @function) + ]) + (expression_list . + value: (function_definition))) + +(table_constructor + (field + name: (identifier) @function + value: (function_definition))) + +(function_call + name: [ + (identifier) @function.call + (dot_index_expression + field: (identifier) @function.call) + (method_index_expression + method: (identifier) @method.call) + ]) + +(function_call + (identifier) @function.builtin + (#any-of? @function.builtin + ;; built-in functions in Lua 5.1 + "assert" "collectgarbage" "dofile" "error" "getfenv" "getmetatable" "ipairs" + "load" "loadfile" "loadstring" "module" "next" "pairs" "pcall" "print" + "rawequal" "rawget" "rawlen" "rawset" "require" "select" "setfenv" "setmetatable" + "tonumber" "tostring" "type" "unpack" "xpcall" + "__add" "__band" "__bnot" "__bor" "__bxor" "__call" "__concat" "__div" "__eq" "__gc" + "__idiv" "__index" "__le" "__len" "__lt" "__metatable" "__mod" "__mul" "__name" "__newindex" + "__pairs" "__pow" "__shl" "__shr" "__sub" "__tostring" "__unm")) + +;; Others + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^[-][-][-]")) + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^[-][-](%s?)@")) + +(hash_bang_line) @preproc + +(number) @number + +(string) @string @spell + +;; Error +(ERROR) @error diff --git a/tree-sitter/highlights/luadoc.scm b/tree-sitter/highlights/luadoc.scm new file mode 100644 index 0000000000..ec8bcb7659 --- /dev/null +++ b/tree-sitter/highlights/luadoc.scm @@ -0,0 +1,152 @@ +; Keywords + +[ + "@module" + "@package" +] @include + +[ + "@class" + "@type" + "@param" + "@alias" + "@field" + "@generic" + "@vararg" + "@diagnostic" + "@deprecated" + "@meta" + "@source" + "@version" + "@operator" + "@nodiscard" + "@cast" + "@overload" + "@enum" + "@language" + "@see" + "extends" + (diagnostic_identifier) +] @keyword + +[ + "@async" +] @keyword.coroutine + +(language_injection "@language" (identifier) @keyword) + +(function_type ["fun" "function"] @keyword.function) + +(source_annotation + filename: (identifier) @text.uri @string.special + extension: (identifier) @text.uri @string.special) + +(version_annotation + version: _ @constant.builtin) + +[ + "@return" +] @keyword.return + +; Qualifiers + +[ + "public" + "protected" + "private" + "@public" + "@protected" + "@private" +] @type.qualifier + + +; Variables + +(identifier) @variable + +[ + "..." + "self" +] @variable.builtin + +; Macros + +(alias_annotation (identifier) @function.macro) + +; Parameters + +(param_annotation (identifier) @parameter) + +(parameter (identifier) @parameter) + +; Fields + +(field_annotation (identifier) @field) + +(table_literal_type field: (identifier) @field) + +(member_type ["#" "."] . (identifier) @field) + +; Types + +(table_type "table" @type.builtin) + +(builtin_type) @type.builtin + +(class_annotation (identifier) @type) + +(enum_annotation (identifier) @type) + +((array_type ["[" "]"] @type) + (#set! "priority" 105)) + +(type) @type + +; Operators + +[ + "|" +] @operator + +; Literals + +(string) @namespace ; only used in @module + +(literal_type) @string + +(number) @number + +; Punctuation + +[ "[" "]" ] @punctuation.bracket + +[ "{" "}" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +[ "<" ">" ] @punctuation.bracket + +[ + "," + "." + "#" + ":" +] @punctuation.delimiter + +[ + "@" + "?" +] @punctuation.special + +; Comments + +(comment) @comment @spell + +(at_comment + (identifier) @type + (_) @comment @spell) + +(class_at_comment + (identifier) @type + ("extends"? (identifier)? @type) + (_) @comment @spell) diff --git a/tree-sitter/highlights/luap.scm b/tree-sitter/highlights/luap.scm new file mode 100644 index 0000000000..62d2b31826 --- /dev/null +++ b/tree-sitter/highlights/luap.scm @@ -0,0 +1,35 @@ +"." @character + +[ + (anchor_begin) + (anchor_end) +] @string.escape + +[ + "[" "]" + "(" ")" +] @punctuation.bracket + +[ + (zero_or_more) + (shortest_zero_or_more) + (one_or_more) + (zero_or_one) +] @operator + +(range + from: (character) @constant + "-" @punctuation.delimiter + to: (character) @constant) + +(set + (character) @constant) + +(class) @keyword + +(negated_set + "^" @operator + (character) @constant) + +(balanced_match + (character) @parameter) diff --git a/tree-sitter/highlights/luau.scm b/tree-sitter/highlights/luau.scm new file mode 100644 index 0000000000..5f7d7da90b --- /dev/null +++ b/tree-sitter/highlights/luau.scm @@ -0,0 +1,252 @@ +; Preproc + +(hash_bang_line) @preproc + +;; Keywords + +"return" @keyword.return + +[ + "local" + "type" + "export" +] @keyword + +(do_statement +[ + "do" + "end" +] @keyword) + +(while_statement +[ + "while" + "do" + "end" +] @repeat) + +(repeat_statement +[ + "repeat" + "until" +] @repeat) + +[ + (break_statement) + (continue_statement) +] @repeat + +(if_statement +[ + "if" + "elseif" + "else" + "then" + "end" +] @conditional) + +(elseif_statement +[ + "elseif" + "then" + "end" +] @conditional) + +(else_statement +[ + "else" + "end" +] @conditional) + +(for_statement +[ + "for" + "do" + "end" +] @repeat) + +(function_declaration +[ + "function" + "end" +] @keyword.function) + +(function_definition +[ + "function" + "end" +] @keyword.function) + +;; Operators + +[ + "and" + "not" + "or" + "in" + "typeof" +] @keyword.operator + +[ + "+" + "-" + "*" + "/" + "%" + "^" + "#" + "==" + "~=" + "<=" + ">=" + "<" + ">" + "=" + "&" + "~" + "|" + "?" + "//" + ".." + "+=" + "-=" + "*=" + "/=" + "%=" + "^=" + "..=" +] @operator + +;; Variables + +(identifier) @variable + +; Types + +(type (identifier) @type) + +(type (generic_type (identifier) @type)) + +(builtin_type) @type.builtin + +((identifier) @type + (#lua-match? @type "^[A-Z]")) + +; Typedefs + +(type_definition "type" . (type) @type.definition "=") + +; Constants + +((identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z_0-9]+$")) + +; Builtins + +((identifier) @constant.builtin + (#eq? @constant.builtin "_VERSION")) + +((identifier) @variable.builtin + (#eq? @variable.builtin "self")) + +((identifier) @namespace.builtin + (#any-of? @namespace.builtin "_G" "debug" "io" "jit" "math" "os" "package" "string" "table" "utf8")) + +((identifier) @keyword.coroutine + (#eq? @keyword.coroutine "coroutine")) + +;; Tables + +(field name: (identifier) @field) + +(dot_index_expression field: (identifier) @field) + +(object_type (identifier) @field) + +(table_constructor +[ + "{" + "}" +] @constructor) + +; Functions + +(parameter . (identifier) @parameter) +(function_type (identifier) @parameter) + +(function_call name: (identifier) @function.call) +(function_declaration name: (identifier) @function) + +(function_call name: (dot_index_expression field: (identifier) @function.call)) +(function_declaration name: (dot_index_expression field: (identifier) @function)) + +(method_index_expression method: (identifier) @method.call) + +(function_call + (identifier) @function.builtin + (#any-of? @function.builtin + ;; built-in functions in Lua 5.1 + "assert" "collectgarbage" "dofile" "error" "getfenv" "getmetatable" "ipairs" + "load" "loadfile" "loadstring" "module" "next" "pairs" "pcall" "print" + "rawequal" "rawget" "rawlen" "rawset" "require" "select" "setfenv" "setmetatable" + "tonumber" "tostring" "type" "unpack" "xpcall" "typeof" + "__add" "__band" "__bnot" "__bor" "__bxor" "__call" "__concat" "__div" "__eq" "__gc" + "__idiv" "__index" "__le" "__len" "__lt" "__metatable" "__mod" "__mul" "__name" "__newindex" + "__pairs" "__pow" "__shl" "__shr" "__sub" "__tostring" "__unm")) + +; Literals + +(number) @number + +(string) @string @spell + +(nil) @constant.builtin + +(vararg_expression) @variable.builtin + +[ + (false) + (true) +] @boolean + +;; Punctuations + +[ + ";" + ":" + "::" + "," + "." + "->" +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +(variable_list + attribute: (attribute + (["<" ">"] @punctuation.bracket + (identifier) @attribute))) + +(generic_type [ "<" ">" ] @punctuation.bracket) +(generic_type_list [ "<" ">" ] @punctuation.bracket) + +; Comments + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^[-][-][-]")) + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^[-][-](%s?)@")) + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/m68k.scm b/tree-sitter/highlights/m68k.scm new file mode 100644 index 0000000000..f587f6c9aa --- /dev/null +++ b/tree-sitter/highlights/m68k.scm @@ -0,0 +1,71 @@ +(symbol) @variable + +(label name: (symbol) @label) + +[ + (instruction_mnemonic) + (directive_mnemonic) +] @function.builtin + +(include (directive_mnemonic) @include) +(include_bin (directive_mnemonic) @include) +(include_dir (directive_mnemonic) @include) + + +(size) @attribute + +(macro_definition name: (symbol) @function.macro) +(macro_call name: (symbol) @function.macro) + +[ + (path) + (string_literal) +] @string + +[ + (decimal_literal) + (hexadecimal_literal) + (octal_literal) + (binary_literal) +] @number + +[ + (reptn) + (carg) + (narg) + (macro_arg) +] @variable.builtin + +[ + (control_mnemonic) + (address_register) + (data_register) + (float_register) + (named_register) +] @keyword + +(repeat (control_mnemonic) @repeat) +(conditional (control_mnemonic) @conditional) + +(comment) @comment + +[ + (operator) + "=" + "#" +] @operator + +[ + "." + "," + "/" + "-" +] @punctuation.delimiter + +[ + "(" + ")" + ")+" +] @punctuation.bracket + +(section) @namespace diff --git a/tree-sitter/highlights/make.scm b/tree-sitter/highlights/make.scm new file mode 100644 index 0000000000..17a100edfc --- /dev/null +++ b/tree-sitter/highlights/make.scm @@ -0,0 +1,131 @@ +(comment) @comment @spell + +(conditional + (_ [ + "ifeq" + "else" + "ifneq" + "ifdef" + "ifndef" + ] @conditional) + "endif" @conditional) + +(rule (targets + (word) @function.builtin + (#any-of? @function.builtin + ".DEFAULT" + ".SUFFIXES" + ".DELETE_ON_ERROR" + ".EXPORT_ALL_VARIABLES" + ".IGNORE" + ".INTERMEDIATE" + ".LOW_RESOLUTION_TIME" + ".NOTPARALLEL" + ".ONESHELL" + ".PHONY" + ".POSIX" + ".PRECIOUS" + ".SECONDARY" + ".SECONDEXPANSION" + ".SILENT" + ".SUFFIXES"))) + +(rule ["&:" ":" "::"] @operator) + +(export_directive "export" @keyword) +(override_directive "override" @keyword) +(include_directive ["include" "-include"] @include) + +(variable_assignment + name: (word) @symbol + [ + "?=" + ":=" + "::=" +; ":::=" + "+=" + "=" + ] @operator) + +(shell_assignment + name: (word) @symbol + "!=" @operator) + +(define_directive + "define" @keyword + name: (word) @symbol + [ + "=" + ":=" + "::=" +; ":::=" + "?=" + "!=" + ]? @operator + "endef" @keyword) + +(variable_assignment + (word) @variable.builtin (#any-of? @variable.builtin + ".DEFAULT_GOAL" + ".EXTRA_PREREQS" + ".FEATURES" + ".INCLUDE_DIRS" + ".RECIPEPREFIX" + ".SHELLFLAGS" + ".VARIABLES" + "MAKEARGS" + "MAKEFILE_LIST" + "MAKEFLAGS" + "MAKE_RESTARTS" + "MAKE_TERMERR" + "MAKE_TERMOUT" + "SHELL" + )) + +; Use string to match bash +(variable_reference (word) @string) @operator + +(shell_function + ["$" "(" ")"] @operator + "shell" @function.builtin) + +(function_call ["$" "(" ")"] @operator) +(substitution_reference ["$" "(" ")"] @operator) + +(function_call [ + "subst" + "patsubst" + "strip" + "findstring" + "filter" + "filter-out" + "sort" + "word" + "words" + "wordlist" + "firstword" + "lastword" + "dir" + "notdir" + "suffix" + "basename" + "addsuffix" + "addprefix" + "join" + "wildcard" + "realpath" + "abspath" + "error" + "warning" + "info" + "origin" + "flavor" + "foreach" + "if" + "or" + "and" + "call" + "eval" + "file" + "value" + ] @function.builtin) diff --git a/tree-sitter/highlights/markdown.scm b/tree-sitter/highlights/markdown.scm new file mode 100644 index 0000000000..e78d233cc6 --- /dev/null +++ b/tree-sitter/highlights/markdown.scm @@ -0,0 +1,63 @@ +;From MDeiml/tree-sitter-markdown & Helix +(setext_heading (paragraph) @text.title.1 (setext_h1_underline) @text.title.1.marker) +(setext_heading (paragraph) @text.title.2 (setext_h2_underline) @text.title.2.marker) + +(atx_heading (atx_h1_marker) @text.title.1.marker (inline) @text.title.1) +(atx_heading (atx_h2_marker) @text.title.2.marker (inline) @text.title.2) +(atx_heading (atx_h3_marker) @text.title.3.marker (inline) @text.title.3) +(atx_heading (atx_h4_marker) @text.title.4.marker (inline) @text.title.4) +(atx_heading (atx_h5_marker) @text.title.5.marker (inline) @text.title.5) +(atx_heading (atx_h6_marker) @text.title.6.marker (inline) @text.title.6) + +(link_title) @text.literal +(indented_code_block) @text.literal.block +((fenced_code_block) @text.literal.block (#set! "priority" 90)) + +(info_string) @label + +(pipe_table_header (pipe_table_cell) @text.title) + +(pipe_table_header "|" @punctuation.special) +(pipe_table_row "|" @punctuation.special) +(pipe_table_delimiter_row "|" @punctuation.special) +(pipe_table_delimiter_cell) @punctuation.special + +[ + (fenced_code_block_delimiter) +] @punctuation.delimiter + +(code_fence_content) @none + +[ + (link_destination) +] @text.uri + +[ + (link_label) +] @text.reference + +[ + (list_marker_plus) + (list_marker_minus) + (list_marker_star) + (list_marker_dot) + (list_marker_parenthesis) + (thematic_break) +] @punctuation.special + + +(task_list_marker_unchecked) @text.todo.unchecked +(task_list_marker_checked) @text.todo.checked + +(block_quote) @text.quote + +[ + (block_continuation) + (block_quote_marker) +] @punctuation.special + +[ + (backslash_escape) +] @string.escape + +(inline) @spell diff --git a/tree-sitter/highlights/markdown_inline.scm b/tree-sitter/highlights/markdown_inline.scm new file mode 100644 index 0000000000..cd5da530d7 --- /dev/null +++ b/tree-sitter/highlights/markdown_inline.scm @@ -0,0 +1,94 @@ +;; From MDeiml/tree-sitter-markdown +[ + (code_span) + (link_title) +] @text.literal @nospell + +[ + (emphasis_delimiter) + (code_span_delimiter) +] @punctuation.delimiter + +(emphasis) @text.emphasis + +(strong_emphasis) @text.strong + +(strikethrough) @text.strike + +[ + (link_destination) + (uri_autolink) +] @text.uri @nospell + +(shortcut_link (link_text) @nospell) + +[ + (link_label) + (link_text) + (image_description) +] @text.reference + +[ + (backslash_escape) + (hard_line_break) +] @string.escape + +(image "!" @punctuation.special) +(image ["[" "]" "(" ")"] @punctuation.bracket) +(inline_link ["[" "]" "(" ")"] @punctuation.bracket) +(shortcut_link ["[" "]"] @punctuation.bracket) + +; Conceal codeblock and text style markers +([ + (code_span_delimiter) + (emphasis_delimiter) +] @conceal +(#set! conceal "")) + +; Conceal inline links +(inline_link + [ + "[" + "]" + "(" + (link_destination) + ")" + ] @conceal + (#set! conceal "")) + +; Conceal image links +(image + [ + "!" + "[" + "]" + "(" + (link_destination) + ")" + ] @conceal + (#set! conceal "")) + +; Conceal full reference links +(full_reference_link + [ + "[" + "]" + (link_label) + ] @conceal + (#set! conceal "")) + +; Conceal collapsed reference links +(collapsed_reference_link + [ + "[" + "]" + ] @conceal + (#set! conceal "")) + +; Conceal shortcut links +(shortcut_link + [ + "[" + "]" + ] @conceal + (#set! conceal "")) diff --git a/tree-sitter/highlights/matlab.scm b/tree-sitter/highlights/matlab.scm new file mode 100644 index 0000000000..8dd10d6a91 --- /dev/null +++ b/tree-sitter/highlights/matlab.scm @@ -0,0 +1,154 @@ +; Includes + +((command_name) @include + (#eq? @include "import")) + +; Keywords + +[ + "arguments" + "classdef" + "end" + "enumeration" + "events" + "global" + "methods" + "persistent" + "properties" +] @keyword + +; Conditionals + +(if_statement [ "if" "end" ] @conditional) +(elseif_clause "elseif" @conditional) +(else_clause "else" @conditional) +(switch_statement [ "switch" "end" ] @conditional) +(case_clause "case" @conditional) +(otherwise_clause "otherwise" @conditional) +(break_statement) @conditional + +; Repeats + +(for_statement [ "for" "parfor" "end" ] @repeat) +(while_statement [ "while" "end" ] @repeat) +(continue_statement) @repeat + +; Exceptions + +(try_statement [ "try" "end" ] @exception) +(catch_clause "catch" @exception) + +; Variables + +(identifier) @variable + +; Constants + +(events (identifier) @constant) +(attribute (identifier) @constant) + +"~" @constant.builtin + +; Fields/Properties + +(field_expression field: (identifier) @field) + +(superclass "." (identifier) @property) + +(property_name "." (identifier) @property) + +(property name: (identifier) @property) + +; Types + +(class_definition name: (identifier) @type) + +(attributes (identifier) @constant) + +(enum . (identifier) @type) + +((identifier) @type + (#lua-match? @type "^_*[A-Z][a-zA-Z0-9_]+$")) + +; Functions + +(function_definition + "function" @keyword.function + name: (identifier) @function + [ "end" "endfunction" ]? @keyword.function) + +(function_signature name: (identifier) @function) + +(function_call + name: (identifier) @function.call) + +(handle_operator (identifier) @function) + +(validation_functions (identifier) @function) + +(command (command_name) @function.call) +(command_argument) @parameter + +(return_statement) @keyword.return + +; Parameters + +(function_arguments (identifier) @parameter) + +; Punctuation + +[ ";" "," "." ] @punctuation.delimiter + +[ "(" ")" "[" "]" "{" "}" ] @punctuation.bracket + +; Operators + +[ + "+" + ".+" + "-" + ".*" + "*" + ".*" + "/" + "./" + "\\" + ".\\" + "^" + ".^" + "'" + ".'" + "|" + "&" + "?" + "@" + "<" + "<=" + ">" + ">=" + "==" + "~=" + "=" + "&&" + "||" + ":" +] @operator + +; Literals + +(string) @string + +(escape_sequence) @string.escape +(formatting_sequence) @string.special + +(number) @number + +(boolean) @boolean + +; Comments + +[ (comment) (line_continuation) ] @comment @spell + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/menhir.scm b/tree-sitter/highlights/menhir.scm new file mode 100644 index 0000000000..64789ee495 --- /dev/null +++ b/tree-sitter/highlights/menhir.scm @@ -0,0 +1,29 @@ +["%parameter" "%token" "%type" "%start" "%attribute" "%left" "%right" "%nonassoc" "%public" "%inline" "%prec"] @keyword +["%on_error_reduce"] @exception + +["let"] @keyword.function + +[(equality_symbol) ":" "|" ";" ","] @punctuation.delimiter + +["=" "~" "_"] @operator +(modifier) @operator + +["<" ">" "{" "}" "%{" "%}" "%%"] @punctuation.special + +["(" ")"] @punctuation.bracket + +(old_rule [(symbol)] @function) +(new_rule [(lid)] @function) + +(precedence [(symbol)] @parameter) + +(funcall) @function.call + +; Not very accurant but does a decent job +(uid) @constant + +(ocaml_type) @type +(ocaml) @none + +[(comment) (line_comment) (ocaml_comment)] @comment +(ERROR) @error diff --git a/tree-sitter/highlights/mermaid.scm b/tree-sitter/highlights/mermaid.scm new file mode 100644 index 0000000000..2f3e31b900 --- /dev/null +++ b/tree-sitter/highlights/mermaid.scm @@ -0,0 +1,177 @@ +; adapted from https://github.com/monaqa/tree-sitter-mermaid + +[ + "sequenceDiagram" + "classDiagram" + "classDiagram-v2" + "stateDiagram" + "stateDiagram-v2" + "gantt" + "pie" + "flowchart" + "erdiagram" + + "participant" + "as" + "activate" + "deactivate" + "note " + "over" + "link" + "links" + ; "left of" + ; "right of" + "properties" + "details" + "title" + "loop" + "rect" + "opt" + "alt" + "else" + "par" + "and" + "end" + (sequence_stmt_autonumber) + (note_placement_left) + (note_placement_right) + + "class" + + "state " + + "dateformat" + "inclusiveenddates" + "topaxis" + "axisformat" + "includes" + "excludes" + "todaymarker" + "title" + "section" + + "direction" + "subgraph" + + ] @keyword + +(comment) @comment @spell + +[ + ":" + (sequence_signal_plus_sign) + (sequence_signal_minus_sign) + + (class_visibility_public) + (class_visibility_private) + (class_visibility_protected) + (class_visibility_internal) + + (state_division) + ] @punctuation.delimiter + +[ + "(" + ")" + "{" + "}" + ] @punctuation.bracket + +[ + "-->" + (solid_arrow) + (dotted_arrow) + (solid_open_arrow) + (dotted_open_arrow) + (solid_cross) + (dotted_cross) + (solid_point) + (dotted_point) + ] @operator + +[ + (class_reltype_aggregation) + (class_reltype_extension) + (class_reltype_composition) + (class_reltype_dependency) + (class_linetype_solid) + (class_linetype_dotted) + "&" + ] @operator + +(sequence_actor) @field +(class_name) @field + +(state_name) @field + +(gantt_task_text) @field + +[ + (class_annotation_line) + (class_stmt_annotation) + (class_generics) + + (state_annotation_fork) + (state_annotation_join) + (state_annotation_choice) + ] @attribute + +(directive) @include + +(pie_label) @string +(pie_value) @float + +[ +(flowchart_direction_lr) +(flowchart_direction_rl) +(flowchart_direction_tb) +(flowchart_direction_bt) + ] @constant + +(flow_vertex_id) @field + +[ + (flow_link_arrow) + (flow_link_arrow_start) + ] @operator + +(flow_link_arrowtext "|" @punctuation.bracket) + +(flow_vertex_square [ "[" "]" ] @punctuation.bracket ) +(flow_vertex_circle ["((" "))"] @punctuation.bracket ) +(flow_vertex_ellipse ["(-" "-)"] @punctuation.bracket ) +(flow_vertex_stadium ["([" "])"] @punctuation.bracket ) +(flow_vertex_subroutine ["[[" "]]"] @punctuation.bracket ) +(flow_vertex_rect ["[|" "|]"] @punctuation.bracket ) +(flow_vertex_cylinder ["[(" ")]"] @punctuation.bracket ) +(flow_vertex_round ["(" ")"] @punctuation.bracket ) +(flow_vertex_diamond ["{" "}"] @punctuation.bracket ) +(flow_vertex_hexagon ["{{" "}}"] @punctuation.bracket ) +(flow_vertex_odd [">" "]"] @punctuation.bracket ) +(flow_vertex_trapezoid ["[/" "\\]"] @punctuation.bracket ) +(flow_vertex_inv_trapezoid ["[\\" "/]"] @punctuation.bracket ) +(flow_vertex_leanright ["[/" "/]"] @punctuation.bracket ) +(flow_vertex_leanleft ["[\\" "\\]"] @punctuation.bracket ) + +(flow_stmt_subgraph ["[" "]"] @punctuation.bracket ) + +[ + (er_cardinarity_zero_or_one) + (er_cardinarity_zero_or_more) + (er_cardinarity_one_or_more) + (er_cardinarity_only_one) + (er_reltype_non_identifying) + (er_reltype_identifying) + ] @operator + +(er_entity_name) @field + +(er_attribute_type) @type +(er_attribute_name) @field + +[ + (er_attribute_key_type_pk) + (er_attribute_key_type_fk) + ] @type.qualifier + +(er_attribute_comment) @string @spell diff --git a/tree-sitter/highlights/meson.scm b/tree-sitter/highlights/meson.scm new file mode 100644 index 0000000000..f687f5ae07 --- /dev/null +++ b/tree-sitter/highlights/meson.scm @@ -0,0 +1,76 @@ +(comment) @comment +(number) @number +(bool) @boolean + +(identifier) @variable + +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + +[ + ":" + "," + "." +] @punctuation.delimiter + +[ + "and" + "not" + "or" + "in" +] @keyword.operator + +[ + "=" + "==" + "!=" + "+" + "/" + "/=" + "+=" + "-=" + ">" + ">=" +] @operator + +(ternaryoperator + ["?" ":"] @conditional.ternary) + +[ + "if" + "elif" + "else" + "endif" +] @conditional + +[ + "foreach" + "endforeach" + (keyword_break) + (keyword_continue) +] @repeat + +(string) @string + +"@" @punctuation.special + +(normal_command + command: (identifier) @function) +(pair + key: (identifier) @property) + +(escape_sequence) @string.escape + +((identifier) @variable.builtin + (#any-of? @variable.builtin + "meson" + "host_machine" + "build_machine" + "target_machine" + )) diff --git a/tree-sitter/highlights/mlir.scm b/tree-sitter/highlights/mlir.scm new file mode 100644 index 0000000000..3cc3c671a2 --- /dev/null +++ b/tree-sitter/highlights/mlir.scm @@ -0,0 +1,335 @@ +[ + "ins" + "outs" + "else" + "do" + "loc" + "attributes" + "into" + "to" + "from" + "step" + "low" + "high" + "iter_args" + "padding_value" + "inner_tiles" + "gather_dims" + "scatter_dims" + "outer_dims_perm" + "inner_dims_pos" + "shared_outs" + "default" + (arith_cmp_predicate) +] @keyword + +[ + "module" + "unrealized_conversion_cast" + + "func.call" + "call" + "func.call_indirect" + "call_indirect" + "func.constant" + "constant" + "func.func" + "func.return" + "return" + + "llvm.func" + "llvm.return" + + "cf.assert" + "cf.br" + "cf.cond_br" + "cf.switch" + + "scf.condition" + "scf.execute_region" + "scf.if" + "scf.index_switch" + "scf.for" + "scf.forall" + "scf.forall.in_parallel" + "scf.parallel" + "scf.reduce" + "scf.reduce.return" + "scf.while" + "scf.yield" + + "arith.constant" + "arith.addi" + "arith.subi" + "arith.divsi" + "arith.divui" + "arith.ceildivsi" + "arith.ceildivui" + "arith.floordivsi" + "arith.remsi" + "arith.remui" + "arith.muli" + "arith.mulsi_extended" + "arith.mului_extended" + "arith.andi" + "arith.ori" + "arith.xori" + "arith.maxsi" + "arith.maxui" + "arith.minsi" + "arith.minui" + "arith.shli" + "arith.shrsi" + "arith.shrui" + "arith.addui_extended" + "arith.addf" + "arith.divf" + "arith.maxf" + "arith.minf" + "arith.mulf" + "arith.remf" + "arith.subf" + "arith.negf" + "arith.cmpi" + "arith.cmpf" + "arith.extf" + "arith.extsi" + "arith.extui" + "arith.fptosi" + "arith.fptoui" + "arith.index_cast" + "arith.index_castui" + "arith.sitofp" + "arith.uitofp" + "arith.bitcast" + "arith.truncf" + "arith.select" + + "math.absf" + "math.atan" + "math.cbrt" + "math.ceil" + "math.cos" + "math.erf" + "math.exp" + "math.exp2" + "math.expm1" + "math.floor" + "math.log" + "math.log10" + "math.log1p" + "math.log2" + "math.round" + "math.roundeven" + "math.rsqrt" + "math.sin" + "math.sqrt" + "math.tan" + "math.tanh" + "math.trunc" + "math.absi" + "math.ctlz" + "math.cttz" + "math.ctpop" + "math.atan2" + "math.copysign" + "math.fpowi" + "math.powf" + "math.ipowi" + "math.fma" + + "memref.alloc" + "memref.cast" + "memref.copy" + "memref.collapse_shape" + "memref.expand_shape" + "memref.prefetch" + "memref.rank" + "memref.realloc" + "memref.view" + + "vector.bitcast" + "vector.broadcast" + "vector.shape_cast" + "vector.type_cast" + "vector.constant_mask" + "vector.create_mask" + "vector.extract" + "vector.load" + "vector.scalable.extract" + "vector.fma" + "vector.flat_transpose" + "vector.insert" + "vector.scalable.insert" + "vector.shuffle" + "vector.store" + "vector.insert_strided_slice" + "vector.matrix_multiply" + "vector.print" + "vector.splat" + "vector.transfer_read" + "vector.transfer_write" + "vector.yield" + + "tensor.empty" + "tensor.cast" + "tensor.dim" + "tensor.collapse_shape" + "tensor.expand_shape" + "tensor.extract" + "tensor.insert" + "tensor.extract_slice" + "tensor.insert_slice" + "tensor.parallel_insert_slice" + "tensor.from_elements" + "tensor.gather" + "tensor.scatter" + "tensor.pad" + "tensor.reshape" + "tensor.splat" + "tensor.pack" + "tensor.unpack" + "tensor.generate" + "tensor.rank" + "tensor.yield" + + "bufferization.alloc_tensor" + "bufferization.to_memref" + "bufferization.to_tensor" + + "linalg.batch_matmul" + "linalg.batch_matmul_transpose_b" + "linalg.batch_matvec" + "linalg.batch_reduce_matmul" + "linalg.broadcast" + "linalg.conv_1d_ncw_fcw" + "linalg.conv_1d_nwc_wcf" + "linalg.conv_1d" + "linalg.conv_2d_nchw_fchw" + "linalg.conv_2d_ngchw_fgchw" + "linalg.conv_2d_nhwc_fhwc" + "linalg.conv_2d_nhwc_hwcf" + "linalg.conv_2d_nhwc_hwcf_q" + "linalg.conv_2d" + "linalg.conv_3d_ndhwc_dhwcf" + "linalg.conv_3d_ndhwc_dhwcf_q" + "linalg.conv_3d" + "linalg.copy" + "linalg.depthwise_conv_1d_nwc_wcm" + "linalg.depthwise_conv_2d_nchw_chw" + "linalg.depthwise_conv_2d_nhwc_hwc" + "linalg.depthwise_conv_2d_nhwc_hwc_q" + "linalg.depthwise_conv_2d_nhwc_hwcm" + "linalg.depthwise_conv_2d_nhwc_hwcm_q" + "linalg.depthwise_conv_3d_ndhwc_dhwc" + "linalg.depthwise_conv_3d_ndhwc_dhwcm" + "linalg.dot" + "linalg.elemwise_binary" + "linalg.elemwise_unary" + "linalg.fill" + "linalg.fill_rng_2d" + "linalg.matmul" + "linalg.matmul_transpose_b" + "linalg.matmul_unsigned" + "linalg.matvec" + "linalg.mmt4d" + "linalg.pooling_nchw_max" + "linalg.pooling_nchw_sum" + "linalg.pooling_ncw_max" + "linalg.pooling_ncw_sum" + "linalg.pooling_ndhwc_max" + "linalg.pooling_ndhwc_min" + "linalg.pooling_ndhwc_sum" + "linalg.pooling_nhwc_max" + "linalg.pooling_nhwc_max_unsigned" + "linalg.pooling_nhwc_min" + "linalg.pooling_nhwc_min_unsigned" + "linalg.pooling_nhwc_sum" + "linalg.pooling_nwc_max" + "linalg.pooling_nwc_max_unsigned" + "linalg.pooling_nwc_min" + "linalg.pooling_nwc_min_unsigned" + "linalg.pooling_nwc_sum" + "linalg.quantized_batch_matmul" + "linalg.quantized_matmul" + "linalg.vecmat" + "linalg.generic" + "linalg.index" + "linalg.map" + "linalg.yield" +] @function.builtin + +(generic_operation) @function + +(builtin_type) @type.builtin + +[ + (type_alias) + (dialect_type) + (type_alias_def) +] @type + +[ + (integer_literal) + (complex_literal) +] @number + +(float_literal) @float +(bool_literal) @boolean + +[ + (tensor_literal) + (array_literal) + (unit_literal) +] @constant.builtin + +(string_literal) @string + +[ + (attribute_alias_def) + (attribute_alias) + (bare_attribute_entry) + (attribute) + (fastmath_attr) + (scatter_dims_attr) + (gather_dims_attr) + (outer_dims_perm_attr) + (inner_dims_pos_attr) + (inner_tiles_attr) + (unique_attr) + (nofold_attr) + (isWrite_attr) + (localityHint_attr) + (isDataCache_attr) + (restrict_attr) + (writable_attr) +] @attribute + +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + +[ + ":" + "," +] @punctuation.delimiter + +[ + "=" + "->" +] @operator + +(func_dialect name: (symbol_ref_id) @function) +(llvm_dialect name: (symbol_ref_id) @function) + +(func_arg_list (value_use) @parameter) +(block_arg_list (value_use) @parameter) + +(caret_id) @tag +(value_use) @variable +(comment) @comment diff --git a/tree-sitter/highlights/nickel.scm b/tree-sitter/highlights/nickel.scm new file mode 100644 index 0000000000..f4df280301 --- /dev/null +++ b/tree-sitter/highlights/nickel.scm @@ -0,0 +1,61 @@ +(comment) @comment @spell + +[ + "forall" + "in" + "let" + "default" + "doc" + "rec" +] @keyword + +"fun" @keyword.function + +"import" @include + +[ "if" "then" "else" ] @conditional +"match" @conditional + +(types) @type +"Array" @type.builtin + +; BUILTIN Constants +(bool) @boolean +"null" @constant.builtin + +(num_literal) @number + +(infix_op) @operator + +(type_atom) @type +(enum_tag) @variable + +(chunk_literal_single) @string +(chunk_literal_multi) @string + +(str_esc_char) @string.escape + +[ + "{" "}" + "(" ")" + "[|" "|]" +] @punctuation.bracket + +(multstr_start) @punctuation.bracket +(multstr_end) @punctuation.bracket +(interpolation_start) @punctuation.bracket +(interpolation_end) @punctuation.bracket + +(record_field) @field + +(builtin) @function.builtin + +(fun_expr pats: + (pattern id: + (ident) @parameter + ) +) + +(applicative t1: + (applicative (record_operand) @function) +) diff --git a/tree-sitter/highlights/ninja.scm b/tree-sitter/highlights/ninja.scm new file mode 100644 index 0000000000..1b31b560cc --- /dev/null +++ b/tree-sitter/highlights/ninja.scm @@ -0,0 +1,98 @@ +[ + "default" + "pool" + "rule" + "build" +] @keyword + +[ + "include" + "subninja" +] @include + +[ + ":" +] @punctuation.delimiter + +[ + "=" + "|" + "||" + "|@" +] @operator + +[ + "$" + "{" + "}" +] @punctuation.special + +;; +;; Names +;; ===== +(pool name: (identifier) @type) +(rule name: (identifier) @function) +(let name: (identifier) @constant) +(expansion (identifier) @constant) +(build rule: (identifier) @function) + +;; +;; Paths and Text +;; ============== +(path) @string.special +(text) @string + +;; +;; Builtins +;; ======== +(pool name: (identifier) @type.builtin + (#any-of? @type.builtin "console")) +(build rule: (identifier) @function.builtin + (#any-of? @function.builtin "phony" "dyndep")) + +;; Top level bindings +;; ------------------ +(manifest + (let name: ((identifier) @constant.builtin + (#any-of? @constant.builtin "builddir" + "ninja_required_version")))) + +;; Rules bindings +;; ----------------- +(rule + (body + (let name: (identifier) @constant.builtin + (#not-any-of? @constant.builtin "command" + "depfile" + "deps" + "msvc_deps_prefix" + "description" + "dyndep" + "generator" + "in" + "in_newline" + "out" + "restat" + "rspfile" + "rspfile_content" + "pool")))) + +;; +;; Expansion +;; --------- +(expansion + (identifier) @constant.macro + (#any-of? @constant.macro "in" "out")) + +;; +;; Escape sequences +;; ================ +(quote) @string.escape + +;; +;; Others +;; ====== +[ + (split) + (comment) +] @comment diff --git a/tree-sitter/highlights/nix.scm b/tree-sitter/highlights/nix.scm new file mode 100644 index 0000000000..d30deeaf56 --- /dev/null +++ b/tree-sitter/highlights/nix.scm @@ -0,0 +1,133 @@ +; basic keywords +[ + "assert" + "in" + "inherit" + "let" + "rec" + "with" +] @keyword + +(variable_expression + name: (identifier) @keyword + (#eq? @keyword "derivation") + (#set! "priority" 101)) + +; exceptions +(variable_expression + name: (identifier) @exception + (#any-of? @exception "abort" "throw") + (#set! "priority" 101)) + +; if/then/else +[ + "if" + "then" + "else" +] @conditional + +; field access default (`a.b or c`) +"or" @keyword.operator + +; comments +(comment) @comment + +; strings +([ (string_expression) (indented_string_expression) ] + (#set! "priority" 99)) @string + +; paths and URLs +[ (path_expression) (hpath_expression) (spath_expression) (uri_expression) ] @string.special + +; escape sequences +(escape_sequence) @string.escape + +; delimiters +[ + "." + ";" + "," +] @punctuation.delimiter + +; brackets +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +; `?` in `{ x ? y }:`, used to set defaults for named function arguments +(formal + name: (identifier) @parameter + "?"? @operator) + +; `...` in `{ ... }`, used to ignore unknown named function arguments (see above) +(ellipses) @punctuation.special + +; universal is the parameter of the function expression +; `:` in `x: y`, used to separate function argument from body (see above) +(function_expression + universal: (identifier) @parameter + ":" @punctuation.special) + +; function calls +(apply_expression + function: (variable_expression + name: (identifier) @function.call)) + +; basic identifiers +(variable_expression) @variable + +(variable_expression + name: (identifier) @include + (#eq? @include "import")) + +(variable_expression + name: (identifier) @boolean + (#any-of? @boolean "true" "false")) + +; builtin functions +(variable_expression name: (identifier) @function.builtin (#any-of? @function.builtin + ; nix eval --impure --expr 'with builtins; filter (x: !(elem x [ "abort" "derivation" "import" "throw" ]) && isFunction builtins.${x}) (attrNames builtins)' + "add" "addErrorContext" "all" "any" "appendContext" "attrNames" "attrValues" "baseNameOf" "bitAnd" "bitOr" "bitXor" "break" "catAttrs" "ceil" "compareVersions" "concatLists" "concatMap" "concatStringsSep" "deepSeq" "derivationStrict" "dirOf" "div" "elem" "elemAt" "fetchGit" "fetchMercurial" "fetchTarball" "fetchTree" "fetchurl" "filter" "filterSource" "findFile" "floor" "foldl'" "fromJSON" "fromTOML" "functionArgs" "genList" "genericClosure" "getAttr" "getContext" "getEnv" "getFlake" "groupBy" "hasAttr" "hasContext" "hashFile" "hashString" "head" "intersectAttrs" "isAttrs" "isBool" "isFloat" "isFunction" "isInt" "isList" "isNull" "isPath" "isString" "length" "lessThan" "listToAttrs" "map" "mapAttrs" "match" "mul" "parseDrvName" "partition" "path" "pathExists" "placeholder" "readDir" "readFile" "removeAttrs" "replaceStrings" "scopedImport" "seq" "sort" "split" "splitVersion" "storePath" "stringLength" "sub" "substring" "tail" "toFile" "toJSON" "toPath" "toString" "toXML" "trace" "traceVerbose" "tryEval" "typeOf" "unsafeDiscardOutputDependency" "unsafeDiscardStringContext" "unsafeGetAttrPos" "zipAttrsWith" + ; primops, `__` in `nix repl` + "__add" "__filter" "__isFunction" "__split" "__addErrorContext" "__filterSource" "__isInt" "__splitVersion" "__all" "__findFile" "__isList" "__storeDir" "__any" "__floor" "__isPath" "__storePath" "__appendContext" "__foldl'" "__isString" "__stringLength" "__attrNames" "__fromJSON" "__langVersion" "__sub" "__attrValues" "__functionArgs" "__length" "__substring" "__bitAnd" "__genList" "__lessThan" "__tail" "__bitOr" "__genericClosure" "__listToAttrs" "__toFile" "__bitXor" "__getAttr" "__mapAttrs" "__toJSON" "__catAttrs" "__getContext" "__match" "__toPath" "__ceil" "__getEnv" "__mul" "__toXML" "__compareVersions" "__getFlake" "__nixPath" "__trace" "__concatLists" "__groupBy" "__nixVersion" "__traceVerbose" "__concatMap" "__hasAttr" "__parseDrvName" "__tryEval" "__concatStringsSep" "__hasContext" "__partition" "__typeOf" "__currentSystem" "__hashFile" "__path" "__unsafeDiscardOutputDependency" "__currentTime" "__hashString" "__pathExists" "__unsafeDiscardStringContext" "__deepSeq" "__head" "__readDir" "__unsafeGetAttrPos" "__div" "__intersectAttrs" "__readFile" "__zipAttrsWith" "__elem" "__isAttrs" "__replaceStrings" "__elemAt" "__isBool" "__seq" "__fetchurl" "__isFloat" "__sort" +)) + +; constants +(variable_expression name: (identifier) @constant.builtin (#any-of? @constant.builtin + ; nix eval --impure --expr 'with builtins; filter (x: !(isFunction builtins.${x} || isBool builtins.${x})) (attrNames builtins)' + "builtins" "currentSystem" "currentTime" "langVersion" "nixPath" "nixVersion" "null" "storeDir" +)) + +; string interpolation (this was very annoying to get working properly) +(interpolation "${" @punctuation.special (_) "}" @punctuation.special) @none + +(select_expression + expression: (_) @_expr + attrpath: (attrpath attr: (identifier) @field) + (#not-eq? @_expr "builtins") +) +(attrset_expression (binding_set (binding . (attrpath (identifier) @field)))) +(rec_attrset_expression (binding_set (binding . (attrpath (identifier) @field)))) + +; unary operators +(unary_expression operator: _ @operator) + +; binary operators +(binary_expression operator: _ @operator) + +; integers, also highlight a unary - +[ + (unary_expression "-" (integer_expression)) + (integer_expression) +] @number + +; floats, also highlight a unary - +[ + (unary_expression "-" (float_expression)) + (float_expression) +] @float diff --git a/tree-sitter/highlights/objc.scm b/tree-sitter/highlights/objc.scm new file mode 100644 index 0000000000..8c70728d40 --- /dev/null +++ b/tree-sitter/highlights/objc.scm @@ -0,0 +1,216 @@ +; inherits: c + +; Preprocs + +(preproc_undef + name: (_) @constant) @preproc + +; Includes + +(module_import "@import" @include path: (identifier) @namespace) + +((preproc_include + _ @include path: (_)) + (#any-of? @include "#include" "#import")) + +; Type Qualifiers + +[ + "@optional" + "@required" + "__covariant" + "__contravariant" + (visibility_specification) +] @type.qualifier + +; Storageclasses + +[ + "@autoreleasepool" + "@synthesize" + "@dynamic" + "volatile" + (protocol_qualifier) +] @storageclass + +; Keywords + +[ + "@protocol" + "@interface" + "@implementation" + "@compatibility_alias" + "@property" + "@selector" + "@defs" + "availability" + "@end" +] @keyword + +(class_declaration "@" @keyword "class" @keyword) ; I hate Obj-C for allowing "@ class" :) + +(method_definition ["+" "-"] @keyword.function) +(method_declaration ["+" "-"] @keyword.function) + +[ + "__typeof__" + "__typeof" + "typeof" + "in" +] @keyword.operator + +[ + "@synchronized" + "oneway" +] @keyword.coroutine + +; Exceptions + +[ + "@try" + "__try" + "@catch" + "__catch" + "@finally" + "__finally" + "@throw" +] @exception + +; Variables + +((identifier) @variable.builtin + (#any-of? @variable.builtin "self" "super")) + +; Functions & Methods + +[ + "objc_bridge_related" + "@available" + "__builtin_available" + "va_arg" + "asm" +] @function.builtin + +(method_definition (identifier) @method) + +(method_declaration (identifier) @method) + +(method_identifier (identifier)? @method ":" @method (identifier)? @method) + +(message_expression method: (identifier) @method.call) + +; Constructors + +((message_expression method: (identifier) @constructor) + (#eq? @constructor "init")) + +; Attributes + +(availability_attribute_specifier + [ + "CF_FORMAT_FUNCTION" "NS_AVAILABLE" "__IOS_AVAILABLE" "NS_AVAILABLE_IOS" + "API_AVAILABLE" "API_UNAVAILABLE" "API_DEPRECATED" "NS_ENUM_AVAILABLE_IOS" + "NS_DEPRECATED_IOS" "NS_ENUM_DEPRECATED_IOS" "NS_FORMAT_FUNCTION" "DEPRECATED_MSG_ATTRIBUTE" + "__deprecated_msg" "__deprecated_enum_msg" "NS_SWIFT_NAME" "NS_SWIFT_UNAVAILABLE" + "NS_EXTENSION_UNAVAILABLE_IOS" "NS_CLASS_AVAILABLE_IOS" "NS_CLASS_DEPRECATED_IOS" "__OSX_AVAILABLE_STARTING" + "NS_ROOT_CLASS" "NS_UNAVAILABLE" "NS_REQUIRES_NIL_TERMINATION" "CF_RETURNS_RETAINED" + "CF_RETURNS_NOT_RETAINED" "DEPRECATED_ATTRIBUTE" "UI_APPEARANCE_SELECTOR" "UNAVAILABLE_ATTRIBUTE" + ]) @attribute + +; Macros + +(type_qualifier + [ + "_Complex" + "_Nonnull" + "_Nullable" + "_Nullable_result" + "_Null_unspecified" + "__autoreleasing" + "__block" + "__bridge" + "__bridge_retained" + "__bridge_transfer" + "__complex" + "__kindof" + "__nonnull" + "__nullable" + "__ptrauth_objc_class_ro" + "__ptrauth_objc_isa_pointer" + "__ptrauth_objc_super_pointer" + "__strong" + "__thread" + "__unsafe_unretained" + "__unused" + "__weak" + ]) @function.macro.builtin + +[ "__real" "__imag" ] @function.macro.builtin + +((call_expression function: (identifier) @function.macro) + (#eq? @function.macro "testassert")) + +; Types + +(class_declaration (identifier) @type) + +(class_interface "@interface" . (identifier) @type superclass: _? @type category: _? @namespace) + +(class_implementation "@implementation" . (identifier) @type superclass: _? @type category: _? @namespace) + +(protocol_forward_declaration (identifier) @type) ; @interface :( + +(protocol_reference_list (identifier) @type) ; ^ + +[ + "BOOL" + "IMP" + "SEL" + "Class" + "id" +] @type.builtin + +; Constants + +(property_attribute (identifier) @constant "="?) + +[ "__asm" "__asm__" ] @constant.macro + +; Properties + +(property_implementation "@synthesize" (identifier) @property) + +((identifier) @property + (#has-ancestor? @property struct_declaration)) + +; Parameters + +(method_parameter ":" @method (identifier) @parameter) + +(method_parameter declarator: (identifier) @parameter) + +(parameter_declaration + declarator: (function_declarator + declarator: (parenthesized_declarator + (block_pointer_declarator + declarator: (identifier) @parameter)))) + +"..." @parameter.builtin + +; Operators + +[ + "^" +] @operator + +; Literals + +(platform) @string.special + +(version_number) @text.uri @number + +; Punctuation + +"@" @punctuation.special + +[ "<" ">" ] @punctuation.bracket diff --git a/tree-sitter/highlights/ocaml.scm b/tree-sitter/highlights/ocaml.scm new file mode 100644 index 0000000000..db70575e88 --- /dev/null +++ b/tree-sitter/highlights/ocaml.scm @@ -0,0 +1,177 @@ +; Modules +;-------- + +[(module_name) (module_type_name)] @namespace + +; Types +;------ + +( + (type_constructor) @type.builtin + (#any-of? @type.builtin + "int" "char" "bytes" "string" "float" + "bool" "unit" "exn" "array" "list" "option" + "int32" "int64" "nativeint" "format6" "lazy_t") +) + +[(class_name) (class_type_name) (type_constructor)] @type + +[(constructor_name) (tag)] @constructor + +; Variables +;---------- + +[(value_name) (type_variable)] @variable + +(value_pattern) @parameter + +; Functions +;---------- + +(let_binding + pattern: (value_name) @function + (parameter)) + +(let_binding + pattern: (value_name) @function + body: [(fun_expression) (function_expression)]) + +(value_specification (value_name) @function) + +(external (value_name) @function) + +(method_name) @method + +; Application +;------------ + +(infix_expression + left: (value_path (value_name) @function) + operator: (concat_operator) @_operator + (#eq? @_operator "@@")) + +(infix_expression + operator: (rel_operator) @_operator + right: (value_path (value_name) @function) + (#eq? @_operator "|>")) + +(application_expression + function: (value_path (value_name) @function)) + +((value_name) @function.builtin + (#any-of? @function.builtin "raise" "raise_notrace" "failwith" "invalid_arg")) + +; Properties +;----------- + +[(label_name) (field_name) (instance_variable_name)] @property + +; Constants +;---------- + +; Don't let normal parens take priority over this +((unit) @constant.builtin (#set! "priority" 105)) + +(boolean) @boolean + +[(number) (signed_number)] @number + +(character) @character + +(string) @string + +(quoted_string "{" @string "}" @string) @string + +(escape_sequence) @string.escape + +[ + (conversion_specification) + (pretty_printing_indication) +] @string.special + +; Keywords +;--------- + +[ + "and" "as" "assert" "begin" "class" + "constraint" "end" "external" "in" + "inherit" "initializer" "let" "match" + "method" "module" "new" "object" "of" + "sig" "struct" "type" "val" "when" "with" +] @keyword + +[ + "lazy" "mutable" "nonrec" + "rec" "private" "virtual" +] @type.qualifier + +["fun" "function" "functor"] @keyword.function + +["if" "then" "else"] @conditional + +["exception" "try"] @exception + +["include" "open"] @include + +["for" "to" "downto" "while" "do" "done"] @repeat + +; Punctuation +;------------ + +(attribute ["[@" "]"] @punctuation.special) +(item_attribute ["[@@" "]"] @punctuation.special) +(floating_attribute ["[@@@" "]"] @punctuation.special) +(extension ["[%" "]"] @punctuation.special) +(item_extension ["[%%" "]"] @punctuation.special) +(quoted_extension ["{%" "}"] @punctuation.special) +(quoted_item_extension ["{%%" "}"] @punctuation.special) + +"%" @punctuation.special + +["(" ")" "[" "]" "{" "}" "[|" "|]" "[<" "[>"] @punctuation.bracket + +(object_type ["<" ">"] @punctuation.bracket) + +[ + "," "." ";" ":" "=" "|" "~" "?" "+" "-" "!" ">" "&" + "->" ";;" ":>" "+=" ":=" ".." +] @punctuation.delimiter + +; Operators +;---------- + +[ + (prefix_operator) + (sign_operator) + (pow_operator) + (mult_operator) + (add_operator) + (concat_operator) + (rel_operator) + (and_operator) + (or_operator) + (assign_operator) + (hash_operator) + (indexing_operator) + (let_operator) + (and_operator) + (match_operator) +] @operator + +(match_expression (match_operator) @keyword) + +(value_definition [(let_operator) (let_and_operator)] @keyword) + +["*" "#" "::" "<-"] @operator + +; Attributes +;----------- + +(attribute_id) @property + +; Comments +;--------- + +[(comment) (line_number_directive) (directive) (shebang)] @comment + +(ERROR) @error diff --git a/tree-sitter/highlights/ocaml_interface.scm b/tree-sitter/highlights/ocaml_interface.scm new file mode 100644 index 0000000000..6d3dfbcf27 --- /dev/null +++ b/tree-sitter/highlights/ocaml_interface.scm @@ -0,0 +1 @@ +; inherits: ocaml diff --git a/tree-sitter/highlights/ocamllex.scm b/tree-sitter/highlights/ocamllex.scm new file mode 100644 index 0000000000..060a57dc8d --- /dev/null +++ b/tree-sitter/highlights/ocamllex.scm @@ -0,0 +1,41 @@ +; Allow OCaml highlighter + +(ocaml) @none + +; Regular expressions + +(regexp_name) @variable + +[(eof) (any)] @constant + +(character) @character + +(string) @string +(escape_sequence) @string.escape + +(character_set "^" @punctuation.special) +(character_range "-" @punctuation.delimiter) + +(regexp_difference ["#"] @operator) +(regexp_repetition ["?" "*" "+"] @operator) +(regexp_alternative ["|"] @operator) + +; Rules + +(lexer_entry_name) @function +(lexer_argument) @parameter + +(lexer_entry ["=" "|"] @punctuation.delimiter) + +; keywords + +["and" "as" "let" "parse" "refill" "rule" "shortest"] @keyword + +; Punctuation + +["[" "]" "(" ")" "{" "}"] @punctuation.bracket + +; Misc + +(comment) @comment +(ERROR) @error diff --git a/tree-sitter/highlights/odin.scm b/tree-sitter/highlights/odin.scm new file mode 100644 index 0000000000..e9ded53977 --- /dev/null +++ b/tree-sitter/highlights/odin.scm @@ -0,0 +1,293 @@ +; Preprocs + +[ + (calling_convention) + (tag) +] @preproc + +; Includes + +[ + "import" + "package" +] @include + +; Keywords + +[ + "foreign" + "using" + "struct" + "enum" + "union" + "defer" + "cast" + "transmute" + "auto_cast" + "map" + "bit_set" + "matrix" +] @keyword + +[ + "proc" +] @keyword.function + +[ + "return" + "or_return" +] @keyword.return + +[ + "distinct" + "dynamic" +] @storageclass + +; Conditionals + +[ + "if" + "else" + "when" + "switch" + "case" + "where" + "break" + (fallthrough_statement) +] @conditional + +((ternary_expression + [ + "?" + ":" + "if" + "else" + "when" + ] @conditional.ternary) + (#set! "priority" 105)) + +; Repeats + +[ + "for" + "do" + "continue" +] @repeat + +; Variables + +(identifier) @variable + +; Namespaces + +(package_declaration (identifier) @namespace) + +(import_declaration alias: (identifier) @namespace) + +(foreign_block (identifier) @namespace) + +(using_statement (identifier) @namespace) + +; Parameters + +(parameter (identifier) @parameter ":" "="? (identifier)? @constant) + +(default_parameter (identifier) @parameter ":=") + +(named_type (identifier) @parameter) + +(call_expression argument: (identifier) @parameter "=") + +; Functions + +(procedure_declaration (identifier) @type) + +(procedure_declaration (identifier) @function (procedure (block))) + +(procedure_declaration (identifier) @function (procedure (uninitialized))) + +(overloaded_procedure_declaration (identifier) @function) + +(call_expression function: (identifier) @function.call) + +; Types + +(type (identifier) @type) + +((type (identifier) @type.builtin) + (#any-of? @type.builtin + "bool" "byte" "b8" "b16" "b32" "b64" + "int" "i8" "i16" "i32" "i64" "i128" + "uint" "u8" "u16" "u32" "u64" "u128" "uintptr" + "i16le" "i32le" "i64le" "i128le" "u16le" "u32le" "u64le" "u128le" + "i16be" "i32be" "i64be" "i128be" "u16be" "u32be" "u64be" "u128be" + "float" "double" "f16" "f32" "f64" "f16le" "f32le" "f64le" "f16be" "f32be" "f64be" + "complex32" "complex64" "complex128" "complex_float" "complex_double" + "quaternion64" "quaternion128" "quaternion256" + "rune" "string" "cstring" "rawptr" "typeid" "any")) + +"..." @type.builtin + +(struct_declaration (identifier) @type "::") + +(enum_declaration (identifier) @type "::") + +(union_declaration (identifier) @type "::") + +(const_declaration (identifier) @type "::" [(array_type) (distinct_type) (bit_set_type) (pointer_type)]) + +(struct . (identifier) @type) + +(field_type . (identifier) @namespace "." (identifier) @type) + +(bit_set_type (identifier) @type ";") + +(procedure_type (parameters (parameter (identifier) @type))) + +(polymorphic_parameters (identifier) @type) + +((identifier) @type + (#lua-match? @type "^[A-Z][a-zA-Z0-9]*$") + (#not-has-parent? @type parameter procedure_declaration)) + +; Fields + +(member_expression "." (identifier) @field) + +(struct_type "{" (identifier) @field) + +(struct_field (identifier) @field "="?) + +(field (identifier) @field) + +; Constants + +((identifier) @constant + (#lua-match? @constant "^_*[A-Z][A-Z0-9_]*$") + (#not-has-parent? @constant type parameter)) + +(member_expression . "." (identifier) @constant) + +(enum_declaration "{" (identifier) @constant) + +; Macros + +((call_expression function: (identifier) @function.macro) + (#lua-match? @function.macro "^_*[A-Z][A-Z0-9_]*$")) + +; Attributes + +(attribute (identifier) @attribute "="?) + +; Labels + +(label_statement (identifier) @label ":") + +; Literals + +(number) @number + +(float) @float + +(string) @string + +(character) @character + +(escape_sequence) @string.escape + +(boolean) @boolean + +[ + (uninitialized) + (nil) +] @constant.builtin + +((identifier) @variable.builtin + (#any-of? @variable.builtin "context" "self")) + +; Operators + +[ + ":=" + "=" + "+" + "-" + "*" + "/" + "%" + "%%" + ">" + ">=" + "<" + "<=" + "==" + "!=" + "~=" + "|" + "~" + "&" + "&~" + "<<" + ">>" + "||" + "&&" + "!" + "^" + ".." + "+=" + "-=" + "*=" + "/=" + "%=" + "&=" + "|=" + "^=" + "<<=" + ">>=" + "||=" + "&&=" + "&~=" + "..=" + "..<" + "?" +] @operator + +[ + "or_else" + "in" + "not_in" +] @keyword.operator + +; Punctuation + +[ "{" "}" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ + "::" + "->" + "." + "," + ":" + (separator) +] @punctuation.delimiter + + +[ + "@" + "$" +] @punctuation.special + +; Comments + +[ + (comment) + (block_comment) +] @comment @spell + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/pascal.scm b/tree-sitter/highlights/pascal.scm new file mode 100644 index 0000000000..e11e0eaa2e --- /dev/null +++ b/tree-sitter/highlights/pascal.scm @@ -0,0 +1,424 @@ +; -- Keywords +[ + (kProgram) + (kLibrary) + (kUnit) + + (kBegin) + (kEnd) + (kAsm) + + (kVar) + (kThreadvar) + (kConst) + (kConstref) + (kResourcestring) + (kOut) + (kType) + (kLabel) + (kExports) + + (kProperty) + (kRead) + (kWrite) + (kImplements) + + (kClass) + (kInterface) + (kObject) + (kRecord) + (kObjcclass) + (kObjccategory) + (kObjcprotocol) + (kArray) + (kFile) + (kString) + (kSet) + (kOf) + (kHelper) + + (kInherited) + + (kGeneric) + (kSpecialize) + + (kFunction) + (kProcedure) + (kConstructor) + (kDestructor) + (kOperator) + (kReference) + + (kInterface) + (kImplementation) + (kInitialization) + (kFinalization) + + (kTry) + (kExcept) + (kFinally) + (kRaise) + (kOn) + (kCase) + (kWith) + (kGoto) +] @keyword + +[ + (kFor) + (kTo) + (kDownto) + (kDo) + (kWhile) + (kRepeat) + (kUntil) +] @repeat + +[ + (kIf) + (kThen) + (kElse) +] @conditional + +[ + (kPublished) + (kPublic) + (kProtected) + (kPrivate) + + (kStrict) + (kRequired) + (kOptional) +] @type.qualifier + +[ + (kPacked) + + (kAbsolute) +] @storageclass + +(kUses) @include + +; -- Attributes + +[ + (kDefault) + (kIndex) + (kNodefault) + (kStored) + + (kStatic) + (kVirtual) + (kAbstract) + (kSealed) + (kDynamic) + (kOverride) + (kOverload) + (kReintroduce) + (kInline) + + (kForward) + + (kStdcall) + (kCdecl) + (kCppdecl) + (kPascal) + (kRegister) + (kMwpascal) + (kExternal) + (kName) + (kMessage) + (kDeprecated) + (kExperimental) + (kPlatform) + (kUnimplemented) + (kCvar) + (kExport) + (kFar) + (kNear) + (kSafecall) + (kAssembler) + (kNostackframe) + (kInterrupt) + (kNoreturn) + (kIocheck) + (kLocal) + (kHardfloat) + (kSoftfloat) + (kMs_abi_default) + (kMs_abi_cdecl) + (kSaveregisters) + (kSysv_abi_default) + (kSysv_abi_cdecl) + (kVectorcall) + (kVarargs) + (kWinapi) + (kAlias) + (kDelayed) + + (rttiAttributes) + (procAttribute) + +] @attribute + +(procAttribute (kPublic) @attribute) + +; -- Punctuation & operators + +[ + "(" + ")" + "[" + "]" +] @punctuation.bracket + +[ + ";" + "," + ":" + (kEndDot) +] @punctuation.delimiter + +[ + ".." +] @punctuation.special + +[ + (kDot) + (kAdd) + (kSub) + (kMul) + (kFdiv) + (kAssign) + (kAssignAdd) + (kAssignSub) + (kAssignMul) + (kAssignDiv) + (kEq) + (kLt) + (kLte) + (kGt) + (kGte) + (kNeq) + (kAt) + (kHat) +] @operator + +[ + (kOr) + (kXor) + (kDiv) + (kMod) + (kAnd) + (kShl) + (kShr) + (kNot) + (kIs) + (kAs) + (kIn) +] @keyword.operator + +; -- Builtin constants + +[ + (kTrue) + (kFalse) +] @boolean + +[ + (kNil) +] @constant.builtin + +; -- Literals + +(literalNumber) @number +(literalString) @string + +; -- Variables + +(exprBinary (identifier) @variable) +(exprUnary (identifier) @variable) +(assignment (identifier) @variable) +(exprBrackets (identifier) @variable) +(exprParens (identifier) @variable) +(exprDot (identifier) @variable) +(exprTpl (identifier) @variable) +(exprArgs (identifier) @variable) +(defaultValue (identifier) @variable) + +; -- Comments + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^///[^/]")) +((comment) @comment.documentation + (#lua-match? @comment.documentation "^///$")) + +((comment) @comment.documentation + . [(unit) (declProc)]) + +(declTypes + (comment) @comment.documentation + . (declType)) + +(declSection + (comment) @comment.documentation + . [(declField) (declProc)]) + +(declEnum + (comment) @comment.documentation + . (declEnumValue)) + +(declConsts + (comment) @comment.documentation + . (declConst)) + +(declVars + (comment) @comment.documentation + . (declVar)) + +(pp) @preproc + +; -- Type declaration + +(declType name: (identifier) @type) +(declType name: (genericTpl entity: (identifier) @type)) + +; -- Procedure & function declarations + +; foobar +(declProc name: (identifier) @function) +; foobar +(declProc name: (genericTpl entity: (identifier) @function)) +; foo.bar +(declProc name: (genericDot rhs: (identifier) @function)) +; foo.bar +(declProc name: (genericDot rhs: (genericTpl entity: (identifier) @function))) + +; Treat property declarations like functions + +(declProp name: (identifier) @function) +(declProp getter: (identifier) @property) +(declProp setter: (identifier) @property) + +; -- Function parameters + +(declArg name: (identifier) @parameter) + +; -- Template parameters + +(genericArg name: (identifier) @parameter) +(genericArg type: (typeref) @type) + +(declProc name: (genericDot lhs: (identifier) @type)) +(declType (genericDot (identifier) @type)) + +(genericDot (genericTpl (identifier) @type)) +(genericDot (genericDot (identifier) @type)) + +(genericTpl entity: (identifier) @type) +(genericTpl entity: (genericDot (identifier) @type)) + +; -- Exception parameters + +(exceptionHandler variable: (identifier) @parameter) + +; -- Type usage + +(typeref) @type + +; -- Constant usage + +[ + (caseLabel) + (label) +] @constant + +(procAttribute (identifier) @constant) +(procExternal (identifier) @constant) + +; -- Variable & constant declarations +; (This is only questionable because we cannot detect types of identifiers +; declared in other units, so the results will be inconsistent) + +(declVar name: (identifier) @variable) +(declConst name: (identifier) @constant) +(declEnumValue name: (identifier) @constant) + +; -- Fields + +(exprDot rhs: (identifier) @property) +(exprDot rhs: (exprDot) @property) +(declClass (declField name:(identifier) @property)) +(declSection (declField name:(identifier) @property)) +(declSection (declVars (declVar name:(identifier) @property))) + +(recInitializerField name:(identifier) @property) + + +;;; ---------------------------------------------- ;;; +;;; EVERYTHING BELOW THIS IS OF QUESTIONABLE VALUE ;;; +;;; ---------------------------------------------- ;;; + + +; -- Procedure name in calls with parentheses +; (Pascal doesn't require parentheses for procedure calls, so this will not +; detect all calls) + +; foobar +(exprCall entity: (identifier) @function) +; foobar +(exprCall entity: (exprTpl entity: (identifier) @function)) +; foo.bar +(exprCall entity: (exprDot rhs: (identifier) @function)) +; foo.bar +(exprCall entity: (exprDot rhs: (exprTpl entity: (identifier) @function))) + +(inherited) @function + +; -- Heuristic for procedure/function calls without parentheses +; (If a statement consists only of an identifier, assume it's a procedure) +; (This will still not match all procedure calls, and also may produce false +; positives in rare cases, but only for nonsensical code) + +(statement (identifier) @function) +(statement (exprDot rhs: (identifier) @function)) +(statement (exprTpl entity: (identifier) @function)) +(statement (exprDot rhs: (exprTpl entity: (identifier) @function))) + +; -- Break, Continue & Exit +; (Not ideal: ideally, there would be a way to check if these special +; identifiers are shadowed by a local variable) +(statement ((identifier) @keyword.return + (#lua-match? @keyword.return "^[eE][xX][iI][tT]$"))) +(statement (exprCall entity: ((identifier) @keyword.return + (#lua-match? @keyword.return "^[eE][xX][iI][tT]$")))) +(statement ((identifier) @repeat + (#lua-match? @repeat "^[bB][rR][eE][aA][kK]$"))) +(statement ((identifier) @repeat + (#lua-match? @repeat "^[cC][oO][nN][tT][iI][nN][uU][eE]$"))) + +; -- Identifier type inference + +; VERY QUESTIONABLE: Highlighting of identifiers based on spelling +(exprBinary ((identifier) @constant + (#match? @constant "^[A-Z][A-Z0-9_]+$|^[a-z]{2,3}[A-Z].+$"))) +(exprUnary ((identifier) @constant + (#match? @constant "^[A-Z][A-Z0-9_]+$|^[a-z]{2,3}[A-Z].+$"))) +(assignment rhs: ((identifier) @constant + (#match? @constant "^[A-Z][A-Z0-9_]+$|^[a-z]{2,3}[A-Z].+$"))) +(exprBrackets ((identifier) @constant + (#match? @constant "^[A-Z][A-Z0-9_]+$|^[a-z]{2,3}[A-Z].+$"))) +(exprParens ((identifier) @constant + (#match? @constant "^[A-Z][A-Z0-9_]+$|^[a-z]{2,3}[A-Z].+$"))) +(exprDot rhs: ((identifier) @constant + (#match? @constant "^[A-Z][A-Z0-9_]+$|^[a-z]{2,3}[A-Z].+$"))) +(exprTpl args: ((identifier) @constant + (#match? @constant "^[A-Z][A-Z0-9_]+$|^[a-z]{2,3}[A-Z].+$"))) +(exprArgs ((identifier) @constant + (#match? @constant "^[A-Z][A-Z0-9_]+$|^[a-z]{2,3}[A-Z].+$"))) +(declEnumValue ((identifier) @constant + (#match? @constant "^[A-Z][A-Z0-9_]+$|^[a-z]{2,3}[A-Z].+$"))) +(defaultValue ((identifier) @constant + (#match? @constant "^[A-Z][A-Z0-9_]+$|^[a-z]{2,3}[A-Z].+$"))) diff --git a/tree-sitter/highlights/passwd.scm b/tree-sitter/highlights/passwd.scm new file mode 100644 index 0000000000..3e078a1234 --- /dev/null +++ b/tree-sitter/highlights/passwd.scm @@ -0,0 +1,16 @@ +(user) @namespace + +(auth) @symbol + +(gecos) @string + +(home) @text.uri @constant + +(shell) @text.uri @string.special + +[ + (gid) + (uid) +] @number + +(separator) @punctuation.delimiter diff --git a/tree-sitter/highlights/pem.scm b/tree-sitter/highlights/pem.scm new file mode 100644 index 0000000000..9192e88c03 --- /dev/null +++ b/tree-sitter/highlights/pem.scm @@ -0,0 +1,11 @@ +["BEGIN" "END"] @keyword + +(dashes) @punctuation.delimiter + +(label) @label + +(data) @none + +(comment) @comment + +(ERROR) @error diff --git a/tree-sitter/highlights/perl.scm b/tree-sitter/highlights/perl.scm new file mode 100644 index 0000000000..ef1ca57ea1 --- /dev/null +++ b/tree-sitter/highlights/perl.scm @@ -0,0 +1,187 @@ +; Misc keywords +[ + "my" "our" "local" + "next" "last" "redo" + "goto" + "package" +; "do" +; "eval" +] @keyword + +; Keywords for including +[ "use" "no" "require" ] @include + +; Keywords that mark conditional statements +[ "if" "elsif" "unless" "else" ] @conditional +(ternary_expression + ["?" ":"] @conditional.ternary) + +; Keywords that mark repeating loops +[ "while" "until" "for" "foreach" ] @repeat + +; Keyword for return expressions +[ "return" ] @keyword.return + +; Keywords for phaser blocks +; TODO: Ideally these would be @keyword.phaser but vim-treesitter doesn't +; have such a thing yet +[ "BEGIN" "CHECK" "UNITCHECK" "INIT" "END" ] @keyword.function + +; Keywords to define a function +[ "sub" ] @keyword.function + +; Lots of builtin functions, except tree-sitter-perl doesn't emit most of +; these yet +;[ +; "print" "printf" "sprintf" "say" +; "push" "pop" "shift" "unshift" "splice" +; "exists" "delete" "keys" "values" +; "each" +;] @function.builtin + +; Keywords that are regular infix operators +[ + "and" "or" "not" "xor" + "eq" "ne" "lt" "le" "ge" "gt" "cmp" +] @keyword.operator + +; Variables +[ + (scalar_variable) + (array_variable) + (hash_variable) +] @variable + +; Special builtin variables +[ + (special_scalar_variable) + (special_array_variable) + (special_hash_variable) + (special_literal) + (super) +] @variable.builtin + +; Integer numbers +[ + (integer) + (hexadecimal) +] @number + +; Float numbers +[ + (floating_point) + (scientific_notation) +] @float + +; version sortof counts as a kind of multipart integer +(version) @constant + +; Package names are types +(package_name) @type + +; The special SUPER:: could be called a namespace. It isn't really but it +; should highlight differently and we might as well do it this way +(super) @namespace + +; Comments are comments +(comments) @comment +(comments) @spell + +((source_file . (comments) @preproc) + (#lua-match? @preproc "^#!/")) + +; POD should be handled specially with its own embedded subtype but for now +; we'll just have to do this. +(pod_statement) @text + +(method_invocation + function_name: (identifier) @method.call) +(call_expression + function_name: (identifier) @function.call) + +;; ---------- + +(use_constant_statement + constant: (identifier) @constant) + +(named_block_statement + function_name: (identifier) @function) + +(function_definition + name: (identifier) @function) + +[ +(function) +(map) +(grep) +(bless) +] @function + +[ +"(" +")" +"[" +"]" +"{" +"}" +] @punctuation.bracket +(standard_input_to_variable) @punctuation.bracket + +[ +"=~" +"!~" +"=" +"==" +"+" +"-" +"." +"//" +"||" +(arrow_operator) +(hash_arrow_operator) +(array_dereference) +(hash_dereference) +(to_reference) +(type_glob) +(hash_access_variable) +] @operator + +[ +(regex_option) +(regex_option_for_substitution) +(regex_option_for_transliteration) +] @parameter + +(type_glob + (identifier) @variable) + +[ +(word_list_qw) +(command_qx_quoted) +(string_single_quoted) +(string_double_quoted) +(string_qq_quoted) +(bareword) +(transliteration_tr_or_y) +] @string + +[ +(pattern_matcher) +(regex_pattern_qr) +(patter_matcher_m) +(substitution_pattern_s) +] @string.regex + +(escape_sequence) @string.escape + +[ +"," +(semi_colon) +(start_delimiter) +(end_delimiter) +(ellipsis_statement) +] @punctuation.delimiter + +(function_attribute) @field + +(function_signature) @type diff --git a/tree-sitter/highlights/php.scm b/tree-sitter/highlights/php.scm new file mode 100644 index 0000000000..4b524b0186 --- /dev/null +++ b/tree-sitter/highlights/php.scm @@ -0,0 +1,314 @@ +; Variables + +(variable_name) @variable + +; Constants + +((name) @constant + (#lua-match? @constant "^_?[A-Z][A-Z%d_]*$")) +((name) @constant.builtin + (#lua-match? @constant.builtin "^__[A-Z][A-Z%d_]+__$")) + +(const_declaration (const_element (name) @constant)) + +; Types + +[ + (primitive_type) + (cast_type) + ] @type.builtin +(named_type + [(name) @type + (qualified_name (name) @type)]) +(class_declaration + name: (name) @type) +(base_clause + [(name) @type + (qualified_name (name) @type)]) +(enum_declaration + name: (name) @type) +(interface_declaration + name: (name) @type) +(namespace_use_clause + [(name) @type + (qualified_name (name) @type)]) +(namespace_aliasing_clause (name) @type.definition) +(class_interface_clause + [(name) @type + (qualified_name (name) @type)]) +(scoped_call_expression + scope: [(name) @type + (qualified_name (name) @type)]) +(class_constant_access_expression + . [(name) @type + (qualified_name (name) @type)] + (name) @constant) +(trait_declaration + name: (name) @type) +(use_declaration + (name) @type) +(binary_expression + operator: "instanceof" + right: [(name) @type + (qualified_name (name) @type)]) + +; Functions, methods, constructors + +(array_creation_expression "array" @function.builtin) +(list_literal "list" @function.builtin) + +(method_declaration + name: (name) @method) + +(function_call_expression + function: (qualified_name (name) @function.call)) + +(function_call_expression + (name) @function.call) + +(scoped_call_expression + name: (name) @function.call) + +(member_call_expression + name: (name) @method.call) + +(function_definition + name: (name) @function) + +(nullsafe_member_call_expression + name: (name) @method) + +(method_declaration + name: (name) @constructor + (#eq? @constructor "__construct")) +(object_creation_expression + [(name) @constructor + (qualified_name (name) @constructor)]) + +; Parameters +[ + (simple_parameter) + (variadic_parameter) +] @parameter + +(argument + (name) @parameter) + +; Member + +(property_element + (variable_name) @property) + +(member_access_expression + name: (variable_name (name)) @property) + +(member_access_expression + name: (name) @property) + +; Variables + +(relative_scope) @variable.builtin + +((variable_name) @variable.builtin + (#eq? @variable.builtin "$this")) + +; Namespace +(namespace_definition + name: (namespace_name (name) @namespace)) +(namespace_name_as_prefix + (namespace_name (name) @namespace)) + +; Attributes +(attribute_list) @attribute + +; Conditions ( ? : ) +(conditional_expression) @conditional + +; Directives +(declare_directive ["strict_types" "ticks" "encoding"] @parameter) + +; Basic tokens + +[ + (string) + (encapsed_string) + (heredoc_body) + (nowdoc_body) + (shell_command_expression) ; backtick operator: `ls -la` + ] @string @spell +(escape_sequence) @string.escape + +(boolean) @boolean +(null) @constant.builtin +(integer) @number +(float) @float +(comment) @comment @spell + +(named_label_statement) @label +; Keywords + +[ + "and" + "as" + "instanceof" + "or" + "xor" +] @keyword.operator + +[ + "fn" + "function" +] @keyword.function + +[ + "break" + "class" + "clone" + "declare" + "default" + "echo" + "enddeclare" + "enum" + "extends" + "global" + "goto" + "implements" + "insteadof" + "interface" + "namespace" + "new" + "trait" + "unset" + ] @keyword + +[ + "abstract" + "const" + "final" + "private" + "protected" + "public" + "readonly" + "static" +] @type.qualifier + +[ + "return" + "yield" +] @keyword.return + +[ + "case" + "else" + "elseif" + "endif" + "endswitch" + "if" + "switch" + "match" + "??" + ] @conditional + +[ + "continue" + "do" + "endfor" + "endforeach" + "endwhile" + "for" + "foreach" + "while" + ] @repeat + +[ + "catch" + "finally" + "throw" + "try" + ] @exception + +[ + "include_once" + "include" + "require_once" + "require" + "use" + ] @include + +[ + "," + ";" + ":" + "\\" + ] @punctuation.delimiter + +[ + (php_tag) + "?>" + "(" + ")" + "[" + "]" + "{" + "}" + "#[" + ] @punctuation.bracket + +[ + "=" + + "." + "-" + "*" + "/" + "+" + "%" + "**" + + "~" + "|" + "^" + "&" + "<<" + ">>" + + "->" + "?->" + + "=>" + + "<" + "<=" + ">=" + ">" + "<>" + "==" + "!=" + "===" + "!==" + + "!" + "&&" + "||" + + ".=" + "-=" + "+=" + "*=" + "/=" + "%=" + "**=" + "&=" + "|=" + "^=" + "<<=" + ">>=" + "??=" + "--" + "++" + + "@" + "::" +] @operator + +(ERROR) @error diff --git a/tree-sitter/highlights/phpdoc.scm b/tree-sitter/highlights/phpdoc.scm new file mode 100644 index 0000000000..5cb3dba06c --- /dev/null +++ b/tree-sitter/highlights/phpdoc.scm @@ -0,0 +1,45 @@ +(tag_name) @attribute +(tag + (tag_name) @_tag (#eq? @_tag "@param") + (variable_name) @parameter +) +(tag + (tag_name) @_tag (#eq? @_tag "@property") + (variable_name) @property +) +(tag + (tag_name) @_tag (#eq? @_tag "@var") + (variable_name) @variable +) +(tag + (tag_name) @_tag (#eq? @_tag "@method") + (name) @method +) +(parameter + (variable_name) @parameter) +(type_list + [ + (array_type) + (primitive_type) + (named_type) + (optional_type) + ] @type) +(tag + (description (text) @text)) +(tag + [ + (author_name) + (version) + ] @text) +(tag + (email_address) @text.uri +) + +(type_list "|" @keyword) +(variable_name "$" @keyword) +(tag + (tag_name) @_tag_name + ["<" ">"] @keyword + (#eq? @_tag_name "@author")) + +(text) @spell diff --git a/tree-sitter/highlights/pioasm.scm b/tree-sitter/highlights/pioasm.scm new file mode 100644 index 0000000000..aa176aa249 --- /dev/null +++ b/tree-sitter/highlights/pioasm.scm @@ -0,0 +1,34 @@ +[ (line_comment) (block_comment) ] @comment + +(label_decl) @label + +(string) @string + +(instruction opcode: _ @function.call) + +[ "pins" "x" "y" "null" "isr" "osr" "osre" "status" "pc" "exec" ] @constant.builtin +(wait_source [ "irq" "gpio" "pin" ] @constant.builtin) + +(out_target "pindirs" @constant.builtin) +(set_target "pindirs" @constant.builtin) +(directive "pindirs" @attribute) + +(condition [ "--" "!=" ] @operator) +(expression [ "+" "-" "*" "/" "|" "&" "^" "::" ] @operator) +(not) @operator + +[ (optional) (irq_modifiers) ] @type.qualifier + +[ "block" "noblock" "rel" ] @attribute + +[ "iffull" "ifempty" ] @conditional + +"public" @storageclass + +(integer) @number + +(directive (identifier) @variable) +(directive (symbol_def (identifier) @variable)) +(value (identifier) @variable) + +(directive directive: _ @preproc) diff --git a/tree-sitter/highlights/po.scm b/tree-sitter/highlights/po.scm new file mode 100644 index 0000000000..bddeb5ba9d --- /dev/null +++ b/tree-sitter/highlights/po.scm @@ -0,0 +1,33 @@ +; Keywords + +[ + "msgctxt" + "msgid" + "msgid_plural" + "msgstr" + "msgstr_plural" +] @keyword + +; Punctuation + +[ "[" "]" ] @punctuation.bracket + +; Literals + +(string) @string + +(escape_sequence) @string.escape + +(number) @number + +; Comments + +(comment) @comment @spell + +(comment (reference (text) @string.special.path)) + +(comment (flag (text) @preproc)) + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/poe_filter.scm b/tree-sitter/highlights/poe_filter.scm new file mode 100644 index 0000000000..889e302ade --- /dev/null +++ b/tree-sitter/highlights/poe_filter.scm @@ -0,0 +1,38 @@ +["Show" "Hide" "Minimal"] @namespace + +(condition (name) @conditional) +(action (name) @keyword) +(continue) @label + +(operator) @operator + +(string) @string @spell + +(file) @string + +[ + (quality) + (rarity) + (influence) + (colour) + (shape) +] @constant.builtin + +(sockets) @variable.builtin + +(number) @number + +(boolean) @boolean + +[ + (disable) + "Temp" +] @constant + +(comment) @comment @spell + +"\"" @punctuation.delimiter + +("\"" @conceal + (#not-has-parent? @conceal string file) + (#set! conceal "")) diff --git a/tree-sitter/highlights/pony.scm b/tree-sitter/highlights/pony.scm new file mode 100644 index 0000000000..1968693f49 --- /dev/null +++ b/tree-sitter/highlights/pony.scm @@ -0,0 +1,292 @@ +; Includes + +[ + "use" +] @include + +; Keywords + +[ + "type" + "actor" + "class" + "primitive" + "interface" + "trait" + "struct" + "embed" + "let" + "var" + (compile_intrinsic) + "as" + "consume" + "recover" + "object" + "where" +] @keyword + +[ + "fun" +] @keyword.function + +[ + "be" +] @keyword.coroutine + +[ + "in" + "is" +] @keyword.operator + +[ + "return" +] @keyword.return + +; Qualifiers + +[ + "iso" + "trn" + "ref" + "val" + "box" + "tag" + "#read" + "#send" + "#share" + "#alias" + "#any" +] @type.qualifier + +; Conditionals + +[ + "if" + "ifdef" + "iftype" + "then" + "else" + "elseif" + "match" +] @conditional + +(if_statement "end" @conditional) + +(iftype_statement "end" @conditional) + +(match_statement "end" @conditional) + +; Repeats + +[ + "repeat" + "until" + "while" + "for" + "continue" + "do" + "break" +] @repeat + +(do_block "end" @repeat) + +(repeat_statement "end" @repeat) + +; Exceptions + +[ + "try" + (error) + "compile_error" +] @exception + +(try_statement "end" @exception) + +(recover_statement "end" @exception) + +; Attributes + +(annotation) @attribute + +; Variables + +(identifier) @variable + +(this) @variable.builtin + +; Fields + +(field name: (identifier) @field) + +(member_expression "." (identifier) @field) + +; Constructors + +(constructor "new" @keyword.operator (identifier) @constructor) + +; Methods + +(method (identifier) @method) + +(behavior (identifier) @method) + +(ffi_method (identifier) @method) + +((ffi_method (string) @string.special) + (#set! "priority" 105)) + +(call_expression + callee: + [ + (identifier) @method.call + (ffi_identifier (identifier) @method.call) + (member_expression "." (identifier) @method.call) + ]) + +; Parameters + +(parameter name: (identifier) @parameter) +(lambda_parameter name: (identifier) @parameter) + +; Types + +(type_alias (identifier) @type.definition) + +(base_type name: (identifier) @type) + +(generic_parameter (identifier) @type) + +(lambda_type (identifier)? @method) + +((identifier) @type + (#lua-match? @type "^_*[A-Z][a-zA-Z0-9_]*$")) + +; Operators + +(unary_expression + operator: ["not" "addressof" "digestof"] @keyword.operator) + +(binary_expression + operator: ["and" "or" "xor" "is" "isnt"] @keyword.operator) + +[ + "=" + "?" + "|" + "&" + "-~" + "+" + "-" + "*" + "/" + "%" + "%%" + "<<" + ">>" + "==" + "!=" + ">" + ">=" + "<=" + "<" + "+~" + "-~" + "*~" + "/~" + "%~" + "%%~" + "<<~" + ">>~" + "==~" + "!=~" + ">~" + ">=~" + "<=~" + "<~" + "+?" + "-?" + "*?" + "/?" + "%?" + "%%?" + "<:" +] @operator + +; Literals + +(string) @string + +(source_file (string) @string.documentation) +(actor_definition (string) @string.documentation) +(class_definition (string) @string.documentation) +(primitive_definition (string) @string.documentation) +(interface_definition (string) @string.documentation) +(trait_definition (string) @string.documentation) +(struct_definition (string) @string.documentation) +(type_alias (string) @string.documentation) +(field (string) @string.documentation) + +(constructor + [ + (string) @string.documentation + (block . (string) @string.documentation) + ]) + +(method + [ + (string) @string.documentation + (block . (string) @string.documentation) + ]) + +(behavior + [ + (string) @string.documentation + (block . (string) @string.documentation) + ]) + +(character) @character + +(escape_sequence) @string.escape + +(number) @number + +(float) @float + +(boolean) @boolean + +; Punctuation + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +[ + "." + "," + ";" + ":" + "~" + ".>" + "->" + "=>" +] @punctuation.delimiter + +[ + "@" + "!" + "^" + "..." +] @punctuation.special + +; Comments + +[ + (line_comment) + (block_comment) +] @comment @spell + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/prisma.scm b/tree-sitter/highlights/prisma.scm new file mode 100644 index 0000000000..cfdc3a6686 --- /dev/null +++ b/tree-sitter/highlights/prisma.scm @@ -0,0 +1,39 @@ +(variable) @variable + +[ + "datasource" + "enum" + "generator" + "model" + "type" + "view" +] @keyword + +(comment) @comment @spell + +(developer_comment) @comment.documentation @spell + +[ + (attribute) + (call_expression) +] @function + +(arguments) @property +(column_type) @type +(enumeral) @constant +(column_declaration (identifier) @variable) +(string) @string + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "=" + "@" +] @operator diff --git a/tree-sitter/highlights/promql.scm b/tree-sitter/highlights/promql.scm new file mode 100644 index 0000000000..5fda580b58 --- /dev/null +++ b/tree-sitter/highlights/promql.scm @@ -0,0 +1,41 @@ +; highlights.scm + +[ + "*" + "/" + "%" + "+" + "-" + ">" + ">=" + "<" + "<=" + "=" + "=~" + "!=" + "!~" +] @operator + +[ + "{" + "}" + "[" + "]" + "(" + ")" +] @punctuation.bracket + +(float_literal) @float +(string_literal) @string + +(metric_name) @type +(range_selection) @text.strong @type +(subquery_range_selection) @text.strong @type + +(label_name) @field +(label_value) @text.underline @string.regex + +(function_name) @function.call + +(comment) @comment @spell +(ERROR) @error diff --git a/tree-sitter/highlights/proto.scm b/tree-sitter/highlights/proto.scm new file mode 100644 index 0000000000..a24314834f --- /dev/null +++ b/tree-sitter/highlights/proto.scm @@ -0,0 +1,69 @@ +[ + "syntax" + "option" + "service" + "rpc" + "returns" + "message" + "enum" + "oneof" + "optional" + "repeated" + "reserved" + "to" +] @keyword + +[ + "package" + "import" +] @include + +[ + (key_type) + (type) + (message_name) + (enum_name) + (service_name) + (rpc_name) + (message_or_enum_type) +] @type + +(enum_field + (identifier) @constant) + +[ + (string) + "\"proto3\"" +] @string + +(int_lit) @number + +(float_lit) @float + +[ + (true) + (false) +] @boolean + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<" + ">" +] @punctuation.bracket + +[ + ";" + "," +] @punctuation.delimiter + +"=" @operator diff --git a/tree-sitter/highlights/prql.scm b/tree-sitter/highlights/prql.scm new file mode 100644 index 0000000000..c5bc475f19 --- /dev/null +++ b/tree-sitter/highlights/prql.scm @@ -0,0 +1,149 @@ +[ + (keyword_from) + (keyword_filter) + (keyword_derive) + (keyword_group) + (keyword_aggregate) + (keyword_sort) + (keyword_take) + (keyword_window) + (keyword_join) + (keyword_select) + (keyword_append) + (keyword_remove) + (keyword_intersect) + (keyword_rolling) + (keyword_rows) + (keyword_expanding) + (keyword_let) + (keyword_prql) + (keyword_from_text) +] @keyword + +(keyword_loop) @repeat + +(keyword_case) @conditional + +[ + (literal_string) + (f_string) + (s_string) +] @string + +(assignment + alias: (field) @field) + +alias: (identifier) @field + +(comment) @comment @spell + +(function_call + (identifier) @function.call) + +[ + "+" + "-" + "*" + "/" + "=" + "==" + "<" + "<=" + "!=" + ">=" + ">" + "&&" + "||" + "//" + "~=" + (bang) +] @operator + +[ + "(" + ")" + "{" + "}" +] @punctuation.bracket + +[ + "," + "." + (pipe) + "->" +] @punctuation.delimiter + +(integer) @number + +(decimal_number) @float + +[ + (keyword_min) + (keyword_max) + (keyword_count) + (keyword_count_distinct) + (keyword_average) + (keyword_avg) + (keyword_sum) + (keyword_stddev) + (keyword_count) + (keyword_lag) + (keyword_lead) + (keyword_first) + (keyword_last) + (keyword_rank) + (keyword_row_number) + (keyword_round) + (keyword_all) + (keyword_map) +] @function + +[ + (keyword_side) + (keyword_format) +] @attribute + +[ + (keyword_version) + (keyword_target) +] @type.qualifier + +(target) @function.builtin + + [ + (date) + (time) + (timestamp) +] @string.special + +[ + (keyword_left) + (keyword_inner) + (keyword_right) + (keyword_full) + (keyword_csv) + (keyword_json) +] @method.call + +[ + (keyword_true) + (keyword_false) +] @boolean + +[ + (keyword_in) +] @keyword.operator + +(function_definition + (keyword_let) + name: (identifier) @function) + +(parameter + (identifier) @parameter) + +(variable + (keyword_let) + name: (identifier) @constant) + + + (keyword_null) @constant.builtin diff --git a/tree-sitter/highlights/pug.scm b/tree-sitter/highlights/pug.scm new file mode 100644 index 0000000000..57667885a1 --- /dev/null +++ b/tree-sitter/highlights/pug.scm @@ -0,0 +1,80 @@ +(comment) @comment @spell + +(tag_name) @tag +((tag_name) @constant.builtin + ; https://www.script-example.com/html-tag-liste + (#any-of? @constant.builtin + "head" "title" "base" "link" "meta" "style" + "body" "article" "section" "nav" "aside" "h1" "h2" "h3" "h4" "h5" "h6" "hgroup" "header" "footer" "address" + "p" "hr" "pre" "blockquote" "ol" "ul" "menu" "li" "dl" "dt" "dd" "figure" "figcaption" "main" "div" + "a" "em" "strong" "small" "s" "cite" "q" "dfn" "abbr" "ruby" "rt" "rp" "data" "time" "code" "var" "samp" "kbd" "sub" "sup" "i" "b" "u" "mark" "bdi" "bdo" "span" "br" "wbr" + "ins" "del" + "picture" "source" "img" "iframe" "embed" "object" "param" "video" "audio" "track" "map" "area" + "table" "caption" "colgroup" "col" "tbody" "thead" "tfoot" "tr" "td" "th " + "form" "label" "input" "button" "select" "datalist" "optgroup" "option" "textarea" "output" "progress" "meter" "fieldset" "legend" + "details" "summary" "dialog" + "script" "noscript" "template" "slot" "canvas")) + +(id) @constant +(class) @property + +(doctype) @preproc + +(content) @none + +(tag + (attributes + (attribute + (attribute_name) @tag.attribute + "=" @operator))) +((tag + (attributes + (attribute (attribute_name) @keyword))) + (#match? @keyword "^(:|v-bind|v-|\\@)")) +(quoted_attribute_value) @string + +(include (keyword) @include) +(extends (keyword) @include) +(filename) @string.special + +(block_definition (keyword) @keyword) +(block_append (keyword)+ @keyword) +(block_prepend (keyword)+ @keyword) +(block_name) @type + +(conditional (keyword) @conditional) +(case + (keyword) @conditional + (when (keyword) @conditional)+) + +(each (keyword) @repeat) +(while (keyword) @repeat) + +(mixin_use + "+" @punctuation.delimiter + (mixin_name) @function.call) +(mixin_definition + (keyword) @keyword.function + (mixin_name) @function) +(mixin_attributes + (attribute_name) @parameter) + +(filter + ":" @punctuation.delimiter + (filter_name) @method.call) +(filter + (attributes + (attribute (attribute_name) @parameter))) + +[ + "(" ")" + "#{" "}" + ;; unsupported + ; "!{" + ; "#[" "]" +] @punctuation.bracket + +[ "," "." "|" ] @punctuation.delimiter +(buffered_code "=" @punctuation.delimiter) +(unbuffered_code "-" @punctuation.delimiter) +(unescaped_buffered_code "!=" @punctuation.delimiter) diff --git a/tree-sitter/highlights/puppet.scm b/tree-sitter/highlights/puppet.scm new file mode 100644 index 0000000000..dc3a1a3f38 --- /dev/null +++ b/tree-sitter/highlights/puppet.scm @@ -0,0 +1,195 @@ +; Variables + +(identifier) @variable + +; Includes + +"include" @include + +(include_statement (identifier) @type) + +(include_statement (class_identifier (identifier) @type . )) + +; Keywords + +[ + "class" + "inherits" + "node" + "type" + "tag" +] @keyword + +[ + "define" + "function" +] @keyword.function + +[ + "if" + "elsif" + "else" + "unless" + "case" +] @conditional + +(default_case "default" @conditional) + +; Properties + +(attribute name: (identifier) @property) +(attribute name: (variable (identifier) @property)) + +; Parameters + +(lambda (variable (identifier) @parameter)) + +(parameter (variable (identifier) @parameter)) + +(function_call (identifier) @parameter) + +(method_call (identifier) @parameter) + +; Functions + +(function_declaration + "function" . (identifier) @function) + +(function_call + (identifier) @function.call "(") + +(defined_resource_type + "define" . (identifier) @function) + +; Methods + +(function_declaration + "function" . (class_identifier (identifier) @method . )) + +(function_call + (class_identifier (identifier) @method.call . )) + +(defined_resource_type + "define" . (class_identifier (identifier) @method . )) + +(method_call + "." . (identifier) @method.call) + +; Types + +(type) @type + +(builtin_type) @type.builtin + +(class_definition + (identifier) @type) +(class_definition + (class_identifier (identifier) @type . )) + +(class_inherits (identifier) @type) +(class_inherits (class_identifier (identifier) @type . )) + +(resource_declaration + (identifier) @type) +(resource_declaration + (class_identifier (identifier) @type . )) + +(node_definition (node_name (identifier) @type)) + +((identifier) @type + (#lua-match? @type "^[A-Z]")) + +((identifier) @type.builtin + (#any-of? @type.builtin "Boolean" "Integer" "Float" "String" "Array" "Hash" "Regexp" "Variant" "Data" "Undef" "Default" "File")) + +; "Namespaces" + +(class_identifier . (identifier) @namespace) + +; Operators + +[ + "or" + "and" + "in" +] @keyword.operator + +[ + "=" + "+=" + "->" + "~>" + "<<|" + "<|" + "|>" + "|>>" + "?" + ">" + ">=" + "<=" + "<" + "==" + "!=" + "<<" + ">>" + "+" + "-" + "*" + "/" + "%" + "=~" + "!~" +] @operator + +; Punctuation + +[ + "|" + "." + "," + ";" + ":" + "::" + "=>" +] @punctuation.delimiter + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +(interpolation [ "${" "}" ] @punctuation.special) + +[ + "$" + "@" + "@@" +] @punctuation.special + +; Literals + +(number) @number + +(float) @float + +(string) @string + +(escape_sequence) @string.escape + +(regex) @string.regex + +(boolean) @boolean + +[ + (undef) + (default) +] @variable.builtin + +; Comments + +(comment) @comment @spell + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/python.scm b/tree-sitter/highlights/python.scm index c18b748674..bd2fe2ffc7 100644 --- a/tree-sitter/highlights/python.scm +++ b/tree-sitter/highlights/python.scm @@ -1,5 +1,6 @@ ;; From tree-sitter-python licensed under MIT License ; Copyright (c) 2016 Max Brunsfeld +; https://github.com/nvim-treesitter/nvim-treesitter/blob/f95ffd09ed35880c3a46ad2b968df361fa592a76/queries/python/highlights.scm ; Variables (identifier) @variable @@ -29,7 +30,25 @@ ((attribute attribute: (identifier) @field) - (#lua-match? @field "^[%l_].*$")) + (#match? @field "^([A-Z])@!.*$")) + +((identifier) @type.builtin + (#any-of? @type.builtin + ;; https://docs.python.org/3/library/exceptions.html + "BaseException" "Exception" "ArithmeticError" "BufferError" "LookupError" "AssertionError" "AttributeError" + "EOFError" "FloatingPointError" "GeneratorExit" "ImportError" "ModuleNotFoundError" "IndexError" "KeyError" + "KeyboardInterrupt" "MemoryError" "NameError" "NotImplementedError" "OSError" "OverflowError" "RecursionError" + "ReferenceError" "RuntimeError" "StopIteration" "StopAsyncIteration" "SyntaxError" "IndentationError" "TabError" + "SystemError" "SystemExit" "TypeError" "UnboundLocalError" "UnicodeError" "UnicodeEncodeError" "UnicodeDecodeError" + "UnicodeTranslateError" "ValueError" "ZeroDivisionError" "EnvironmentError" "IOError" "WindowsError" + "BlockingIOError" "ChildProcessError" "ConnectionError" "BrokenPipeError" "ConnectionAbortedError" + "ConnectionRefusedError" "ConnectionResetError" "FileExistsError" "FileNotFoundError" "InterruptedError" + "IsADirectoryError" "NotADirectoryError" "PermissionError" "ProcessLookupError" "TimeoutError" "Warning" + "UserWarning" "DeprecationWarning" "PendingDeprecationWarning" "SyntaxWarning" "RuntimeWarning" + "FutureWarning" "ImportWarning" "UnicodeWarning" "BytesWarning" "ResourceWarning" + ;; https://docs.python.org/3/library/stdtypes.html + "bool" "int" "float" "complex" "list" "tuple" "range" "str" + "bytes" "bytearray" "memoryview" "set" "frozenset" "dict" "type")) ((assignment left: (identifier) @type.definition @@ -53,12 +72,12 @@ ((call function: (identifier) @constructor) - (#lua-match? @constructor "^%u")) + (#lua-match? @constructor "^[A-Z]")) ((call function: (attribute attribute: (identifier) @constructor)) - (#lua-match? @constructor "^%u")) + (#lua-match? @constructor "^[A-Z]")) ;; Decorators @@ -152,24 +171,13 @@ (comment) @comment @spell ((module . (comment) @preproc) - (#lua-match? @preproc "^#!/")) + (#match? @preproc "^#!/")) (string) @string (escape_sequence) @string.escape ; doc-strings - -(module . (expression_statement (string) @string.documentation @spell)) - -(class_definition - body: - (block - . (expression_statement (string) @string.documentation @spell))) - -(function_definition - body: - (block - . (expression_statement (string) @string.documentation @spell))) +(expression_statement (string) @spell) ; Tokens @@ -220,8 +228,6 @@ "is" "not" "or" - "is not" - "not in" "del" ] @keyword.operator @@ -233,6 +239,8 @@ [ "assert" + "async" + "await" "class" "exec" "global" @@ -243,11 +251,6 @@ "as" ] @keyword -[ - "async" - "await" -] @keyword.coroutine - [ "return" "yield" @@ -269,7 +272,6 @@ [ "try" "except" - "except*" "raise" "finally" ] @exception @@ -286,8 +288,6 @@ "{" @punctuation.special "}" @punctuation.special) -(type_conversion) @function.macro - ["," "." ":" ";" (ellipsis)] @punctuation.delimiter ;; Class definitions @@ -308,14 +308,14 @@ (expression_statement (assignment left: (identifier) @field)))) - (#lua-match? @field "^%l.*$")) + (#match? @field "^([A-Z])@!.*$")) ((class_definition body: (block (expression_statement (assignment left: (_ (identifier) @field))))) - (#lua-match? @field "^%l.*$")) + (#match? @field "^([A-Z])@!.*$")) ((class_definition (block @@ -323,23 +323,5 @@ name: (identifier) @constructor))) (#any-of? @constructor "__new__" "__init__")) -((identifier) @type.builtin - (#any-of? @type.builtin - ;; https://docs.python.org/3/library/exceptions.html - "BaseException" "Exception" "ArithmeticError" "BufferError" "LookupError" "AssertionError" "AttributeError" - "EOFError" "FloatingPointError" "GeneratorExit" "ImportError" "ModuleNotFoundError" "IndexError" "KeyError" - "KeyboardInterrupt" "MemoryError" "NameError" "NotImplementedError" "OSError" "OverflowError" "RecursionError" - "ReferenceError" "RuntimeError" "StopIteration" "StopAsyncIteration" "SyntaxError" "IndentationError" "TabError" - "SystemError" "SystemExit" "TypeError" "UnboundLocalError" "UnicodeError" "UnicodeEncodeError" "UnicodeDecodeError" - "UnicodeTranslateError" "ValueError" "ZeroDivisionError" "EnvironmentError" "IOError" "WindowsError" - "BlockingIOError" "ChildProcessError" "ConnectionError" "BrokenPipeError" "ConnectionAbortedError" - "ConnectionRefusedError" "ConnectionResetError" "FileExistsError" "FileNotFoundError" "InterruptedError" - "IsADirectoryError" "NotADirectoryError" "PermissionError" "ProcessLookupError" "TimeoutError" "Warning" - "UserWarning" "DeprecationWarning" "PendingDeprecationWarning" "SyntaxWarning" "RuntimeWarning" - "FutureWarning" "ImportWarning" "UnicodeWarning" "BytesWarning" "ResourceWarning" - ;; https://docs.python.org/3/library/stdtypes.html - "bool" "int" "float" "complex" "list" "tuple" "range" "str" - "bytes" "bytearray" "memoryview" "set" "frozenset" "dict" "type" "object")) - ;; Error (ERROR) @error diff --git a/tree-sitter/highlights/ql.scm b/tree-sitter/highlights/ql.scm new file mode 100644 index 0000000000..dcc702263a --- /dev/null +++ b/tree-sitter/highlights/ql.scm @@ -0,0 +1,135 @@ +[ + "as" + "by" + "class" + "extends" + "from" + "implies" + "in" + "module" + "newtype" + "order" + "select" + "where" + + (predicate) + (result) + (specialId) +] @keyword + +[ + "and" + "not" + "or" +] @keyword.operator + +[ + "avg" + "any" + "count" + "concat" + "exists" + "max" + "min" + "instanceof" + "rank" + "sum" + "strictconcat" + "strictcount" + "strictsum" +] @function.builtin + +"import" @include + +[ + "if" + "then" + "else" +] @conditional + +[ + "forall" + "forex" +] @repeat + +[ + "asc" + "desc" +] @type.qualifier + +[ + (true) + (false) +] @boolean + +[ + (this) + (super) +] @variable.builtin + +[ + "boolean" + "float" + "int" + "date" + "string" +] @type.builtin + +(annotName) @attribute + +[ + "<" + "<=" + "=" + ">" + ">=" + "-" + "!=" + "/" + "*" + "%" + "+" + "::" +] @operator + +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + +[ + "," + "|" +] @punctuation.delimiter + +(moduleExpr (simpleId) @namespace) +(module name: (moduleName) @namespace) + +(dataclass name: (className) @type) +(typeExpr name: (className) @type) + +(datatype name: (className) @type.definition) + +(importModuleExpr qualName: (simpleId) @variable) +(varName) @variable + +(integer) @number +(float) @float + +(string) @string + +(aritylessPredicateExpr (literalId) @function) +(memberPredicate name: (predicateName) @function) +(classlessPredicate name: (predicateName) @function) +(charpred (className) @function) + +[ + (line_comment) + (block_comment) +] @comment @spell + +(qldoc) @comment.documentation diff --git a/tree-sitter/highlights/qmldir.scm b/tree-sitter/highlights/qmldir.scm new file mode 100644 index 0000000000..1fd174708a --- /dev/null +++ b/tree-sitter/highlights/qmldir.scm @@ -0,0 +1,24 @@ +; Preproc + +(command (identifier) @preproc) + +; Keywords + +(keyword) @keyword + +; Literals + +(number) @number + +(float) @float + +; Variables + +[ + (identifier) + (unit) +] @variable + +; Comments + +(comment) @comment @spell diff --git a/tree-sitter/highlights/qmljs.scm b/tree-sitter/highlights/qmljs.scm new file mode 100644 index 0000000000..a53a275eb7 --- /dev/null +++ b/tree-sitter/highlights/qmljs.scm @@ -0,0 +1,153 @@ +; inherits: ecma + +"pragma" @include + +;;; Annotations + +(ui_annotation + "@" @operator + type_name: [ + (identifier) @attribute + (nested_identifier (identifier) @attribute) + ]) + +;; type +(ui_property + type: (type_identifier) @type) + +;;; Properties + +(ui_object_definition_binding + name: [ + (identifier) @property + (nested_identifier (identifier) @property) + ]) + +(ui_binding + name: [ + (identifier) @property + (nested_identifier (identifier) @property) + ]) + +;; locals query appears not working unless id: isn't a parameter. +(ui_binding + name: (identifier) @property + (#eq? @property "id") + value: (expression_statement (identifier) @variable)) + +(ui_property + name: (identifier) @property) + +(ui_required + name: (identifier) @property) + +(ui_list_property_type + ["<" ">"] @punctuation.bracket) + +;;; Signals + +(ui_signal + name: (identifier) @function) + +(ui_signal_parameter + (identifier) @variable) + +;;; ui_object_definition +(ui_object_definition + type_name: (identifier) @type) +(ui_object_definition + type_name: (nested_identifier) @type) + +;;; namespace +(nested_identifier + (nested_identifier + (identifier) @namespace) +) + +; Properties +;----------- + +(property_identifier) @property + +;;; function +(call_expression + function: (member_expression + object: (identifier) @variable + property:(property_identifier) @function + ) +) +;;; js + + + +; Literals +;--------- + +[ + (true) + (false) +] @boolean + +[ + (null) + (undefined) +] @constant.builtin + +(comment) @comment + +[ + (string) + (template_string) +] @string + +(regex) @string.regex +(number) @number + +; Tokens +;------- + +[ + "abstract" + + "private" + "protected" + "public" + + "default" + "readonly" + "required" +] @type.qualifier + +; from typescript + +(type_identifier) @type +(predefined_type) @type.builtin + +((identifier) @type + (#lua-match? @type "^%u")) + +(type_arguments + "<" @punctuation.bracket + ">" @punctuation.bracket) + +; Variables + +(required_parameter (identifier) @variable) +(optional_parameter (identifier) @variable) + +; Keywords + +[ + "on" + "property" + "signal" + "declare" + "enum" + "export" + "implements" + "interface" + "keyof" + "namespace" + "type" + "override" +] @keyword diff --git a/tree-sitter/highlights/query.scm b/tree-sitter/highlights/query.scm new file mode 100644 index 0000000000..f2d2ef6c7f --- /dev/null +++ b/tree-sitter/highlights/query.scm @@ -0,0 +1,34 @@ +(string) @string +(escape_sequence) @string.escape +(capture (identifier) @type) +(anonymous_node (identifier) @string) +(predicate name: (identifier) @function) +(named_node name: (identifier) @variable) +(field_definition name: (identifier) @property) +(negated_field "!" @operator (identifier) @property) +(comment) @comment @spell + +(quantifier) @operator +(predicate_type) @punctuation.special + +"." @operator + +[ + "[" + "]" + "(" + ")" +] @punctuation.bracket + +":" @punctuation.delimiter +["@" "#"] @punctuation.special +"_" @constant + +((parameters (identifier) @number) + (#match? @number "^[-+]?[0-9]+(.[0-9]+)?$")) + +((program . (comment)* . (comment) @include) + (#lua-match? @include "^;+ *inherits *:")) + +((program . (comment)* . (comment) @preproc) + (#lua-match? @preproc "^;+ *extends")) diff --git a/tree-sitter/highlights/r.scm b/tree-sitter/highlights/r.scm new file mode 100644 index 0000000000..ca784e6478 --- /dev/null +++ b/tree-sitter/highlights/r.scm @@ -0,0 +1,144 @@ +; highlights.scm + +; Literals +(integer) @number + +(float) @float + +(complex) @number + +(string) @string +(string (escape_sequence) @string.escape) + +(comment) @comment @spell + +((program . (comment) @preproc) + (#lua-match? @preproc "^#!/")) + +(identifier) @variable + +((dollar (identifier) @variable.builtin) + (#eq? @variable.builtin "self")) + +((dollar _ (identifier) @field)) + +; Parameters + +(formal_parameters (identifier) @parameter) + +(formal_parameters + (default_parameter name: (identifier) @parameter)) + +(default_argument name: (identifier) @parameter) + +; Namespace + +(namespace_get namespace: (identifier) @namespace) +(namespace_get_internal namespace: (identifier) @namespace) + +; Operators +[ + "=" + "<-" + "<<-" + "->" +] @operator + +(unary operator: [ + "-" + "+" + "!" + "~" + "?" +] @operator) + +(binary operator: [ + "-" + "+" + "*" + "/" + "^" + "<" + ">" + "<=" + ">=" + "==" + "!=" + "||" + "|" + "&&" + "&" + ":" + "~" +] @operator) + +[ + "|>" + (special) +] @operator + +(lambda_function "\\" @operator) + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +(dollar _ "$" @operator) + +(subset2 + "[[" @punctuation.bracket + "]]" @punctuation.bracket) + +[ + (dots) + (break) + (next) +] @keyword + +[ + (nan) + (na) + (null) + (inf) +] @constant.builtin + +[ + "if" + "else" + "switch" +] @conditional + +[ + "while" + "repeat" + "for" + "in" +] @repeat + +[ + (true) + (false) +] @boolean + +"function" @keyword.function + +; Functions/Methods + +(call function: (identifier) @function.call) + +(call + (namespace_get function: (identifier) @function.call)) + +(call + (namespace_get_internal function: (identifier) @function.call)) + +(call + function: ((dollar _ (identifier) @method.call))) + +; Error +(ERROR) @error diff --git a/tree-sitter/highlights/racket.scm b/tree-sitter/highlights/racket.scm new file mode 100644 index 0000000000..0fc656fa32 --- /dev/null +++ b/tree-sitter/highlights/racket.scm @@ -0,0 +1,141 @@ +;; A highlight query can override the highlights queries before it. +;; So the order is important. +;; We should highlight general rules, then highlight special forms. + +;;------------------------------------------------------------------;; +;; Basic highlights ;; +;;------------------------------------------------------------------;; + +(ERROR) @error + +;; basic ;; + +(number) @number +(character) @character +(boolean) @boolean +(keyword) @symbol + +;; string ;; + +[(string) + (here_string) + (byte_string)] @string +(string) @spell + +(escape_sequence) @string.escape + +(regex) @string.regex + +;; comment ;; + +[(comment) + (block_comment) + (sexp_comment)] @comment + +[(comment) + (block_comment)] @spell + +;; symbol ;; + +(symbol) @variable + +((symbol) @comment + (#lua-match? @comment "^#[cC][iIsS]$")) + +;; extension ;; + +(extension) @keyword +(lang_name) @variable.builtin + +;; quote ;; + +(quote) @symbol + +;; list ;; + +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + +;; procedure ;; + +(list + . + (symbol) @function) + +;;------------------------------------------------------------------;; +;; Builtin highlights ;; +;;------------------------------------------------------------------;; + +;; The following lists are generated by a racket script: +;; https://gist.github.com/6cdh/65619e761753eb4166d15185a6236040 +;; Don't edit them directly. + +;; keyword ;; + +(list + . + (symbol) @keyword + (#any-of? @keyword + "#%app" "#%datum" "#%declare" "#%expression" "#%module-begin" "#%plain-app" "#%plain-lambda" "#%plain-module-begin" "#%printing-module-begin" "#%provide" "#%require" "#%stratified-body" "#%top" "#%top-interaction" "#%variable-reference" "->" "->*" "->*m" "->d" "->dm" "->i" "->m" "..." ":do-in" "==" "=>" "_" "absent" "abstract" "all-defined-out" "all-from-out" "and" "any" "augment" "augment*" "augment-final" "augment-final*" "augride" "augride*" "begin" "begin-for-syntax" "begin0" "case" "case->" "case->m" "case-lambda" "class" "class*" "class-field-accessor" "class-field-mutator" "class/c" "class/derived" "combine-in" "combine-out" "command-line" "compound-unit" "compound-unit/infer" "cond" "cons/dc" "contract" "contract-out" "contract-pos/neg-doubling" "contract-struct" "contracted" "current-contract-region" "define" "define-compound-unit" "define-compound-unit/infer" "define-contract-struct" "define-custom-hash-types" "define-custom-set-types" "define-for-syntax" "define-local-member-name" "define-logger" "define-match-expander" "define-member-name" "define-module-boundary-contract" "define-namespace-anchor" "define-opt/c" "define-sequence-syntax" "define-serializable-class" "define-serializable-class*" "define-signature" "define-signature-form" "define-splicing-for-clause-syntax" "define-struct" "define-struct/contract" "define-struct/derived" "define-syntax" "define-syntax-rule" "define-syntaxes" "define-unit" "define-unit-binding" "define-unit-from-context" "define-unit/contract" "define-unit/new-import-export" "define-unit/s" "define-values" "define-values-for-export" "define-values-for-syntax" "define-values/invoke-unit" "define-values/invoke-unit/infer" "define/augment" "define/augment-final" "define/augride" "define/contract" "define/final-prop" "define/match" "define/overment" "define/override" "define/override-final" "define/private" "define/public" "define/public-final" "define/pubment" "define/subexpression-pos-prop" "define/subexpression-pos-prop/name" "delay" "delay/idle" "delay/name" "delay/strict" "delay/sync" "delay/thread" "do" "else" "except" "except-in" "except-out" "export" "extends" "failure-cont" "field" "field-bound?" "file" "flat-murec-contract" "flat-rec-contract" "for" "for*" "for*/and" "for*/async" "for*/first" "for*/fold" "for*/fold/derived" "for*/foldr" "for*/foldr/derived" "for*/hash" "for*/hasheq" "for*/hasheqv" "for*/last" "for*/list" "for*/lists" "for*/mutable-set" "for*/mutable-seteq" "for*/mutable-seteqv" "for*/or" "for*/product" "for*/set" "for*/seteq" "for*/seteqv" "for*/stream" "for*/sum" "for*/vector" "for*/weak-set" "for*/weak-seteq" "for*/weak-seteqv" "for-label" "for-meta" "for-space" "for-syntax" "for-template" "for/and" "for/async" "for/first" "for/fold" "for/fold/derived" "for/foldr" "for/foldr/derived" "for/hash" "for/hasheq" "for/hasheqv" "for/last" "for/list" "for/lists" "for/mutable-set" "for/mutable-seteq" "for/mutable-seteqv" "for/or" "for/product" "for/set" "for/seteq" "for/seteqv" "for/stream" "for/sum" "for/vector" "for/weak-set" "for/weak-seteq" "for/weak-seteqv" "gen:custom-write" "gen:dict" "gen:equal+hash" "gen:set" "gen:stream" "generic" "get-field" "hash/dc" "if" "implies" "import" "include" "include-at/relative-to" "include-at/relative-to/reader" "include/reader" "inherit" "inherit-field" "inherit/inner" "inherit/super" "init" "init-depend" "init-field" "init-rest" "inner" "inspect" "instantiate" "interface" "interface*" "invariant-assertion" "invoke-unit" "invoke-unit/infer" "lambda" "lazy" "let" "let*" "let*-values" "let-syntax" "let-syntaxes" "let-values" "let/cc" "let/ec" "letrec" "letrec-syntax" "letrec-syntaxes" "letrec-syntaxes+values" "letrec-values" "lib" "link" "local" "local-require" "log-debug" "log-error" "log-fatal" "log-info" "log-warning" "match" "match*" "match*/derived" "match-define" "match-define-values" "match-lambda" "match-lambda*" "match-lambda**" "match-let" "match-let*" "match-let*-values" "match-let-values" "match-letrec" "match-letrec-values" "match/derived" "match/values" "member-name-key" "mixin" "module" "module*" "module+" "nand" "new" "nor" "object-contract" "object/c" "only" "only-in" "only-meta-in" "only-space-in" "open" "opt/c" "or" "overment" "overment*" "override" "override*" "override-final" "override-final*" "parameterize" "parameterize*" "parameterize-break" "parametric->/c" "place" "place*" "place/context" "planet" "prefix" "prefix-in" "prefix-out" "private" "private*" "prompt-tag/c" "prop:dict/contract" "protect-out" "provide" "provide-signature-elements" "provide/contract" "public" "public*" "public-final" "public-final*" "pubment" "pubment*" "quasiquote" "quasisyntax" "quasisyntax/loc" "quote" "quote-syntax" "quote-syntax/prune" "recontract-out" "recursive-contract" "relative-in" "rename" "rename-in" "rename-inner" "rename-out" "rename-super" "require" "send" "send*" "send+" "send-generic" "send/apply" "send/keyword-apply" "set!" "set!-values" "set-field!" "shared" "stream" "stream*" "stream-cons" "stream-lazy" "struct" "struct*" "struct-copy" "struct-field-index" "struct-guard/c" "struct-out" "struct/c" "struct/contract" "struct/ctc" "struct/dc" "struct/derived" "submod" "super" "super-instantiate" "super-make-object" "super-new" "syntax" "syntax-case" "syntax-case*" "syntax-id-rules" "syntax-rules" "syntax/loc" "tag" "this" "this%" "thunk" "thunk*" "time" "unconstrained-domain->" "unit" "unit-from-context" "unit/c" "unit/new-import-export" "unit/s" "unless" "unquote" "unquote-splicing" "unsyntax" "unsyntax-splicing" "values/drop" "when" "with-continuation-mark" "with-contract" "with-contract-continuation-mark" "with-handlers" "with-handlers*" "with-method" "with-syntax" "~?" "~@" "λ" + )) + +;; builtin procedures + +((symbol) @function.builtin + (#any-of? @function.builtin + "*" "*list/c" "+" "-" "/" "<" "" ">/c" ">=" ">=/c" "abort-current-continuation" "abs" "absolute-path?" "acos" "add-between" "add1" "alarm-evt" "and/c" "andmap" "angle" "any/c" "append" "append*" "append-map" "apply" "argmax" "argmin" "arithmetic-shift" "arity-at-least" "arity-at-least-value" "arity-at-least?" "arity-checking-wrapper" "arity-includes?" "arity=?" "arrow-contract-info" "arrow-contract-info-accepts-arglist" "arrow-contract-info-chaperone-procedure" "arrow-contract-info-check-first-order" "arrow-contract-info?" "asin" "assert-unreachable" "assf" "assoc" "assq" "assv" "atan" "bad-number-of-results" "banner" "base->-doms/c" "base->-rngs/c" "base->?" "between/c" "bitwise-and" "bitwise-bit-field" "bitwise-bit-set?" "bitwise-ior" "bitwise-not" "bitwise-xor" "blame-add-car-context" "blame-add-cdr-context" "blame-add-context" "blame-add-missing-party" "blame-add-nth-arg-context" "blame-add-range-context" "blame-add-unknown-context" "blame-context" "blame-contract" "blame-fmt->-string" "blame-missing-party?" "blame-negative" "blame-original?" "blame-positive" "blame-replace-negative" "blame-replaced-negative?" "blame-source" "blame-swap" "blame-swapped?" "blame-update" "blame-value" "blame?" "boolean=?" "boolean?" "bound-identifier=?" "box" "box-cas!" "box-immutable" "box-immutable/c" "box/c" "box?" "break-enabled" "break-parameterization?" "break-thread" "build-chaperone-contract-property" "build-compound-type-name" "build-contract-property" "build-flat-contract-property" "build-list" "build-path" "build-path/convention-type" "build-string" "build-vector" "byte-pregexp" "byte-pregexp?" "byte-ready?" "byte-regexp" "byte-regexp?" "byte?" "bytes" "bytes->immutable-bytes" "bytes->list" "bytes->path" "bytes->path-element" "bytes->string/latin-1" "bytes->string/locale" "bytes->string/utf-8" "bytes-append" "bytes-append*" "bytes-close-converter" "bytes-convert" "bytes-convert-end" "bytes-converter?" "bytes-copy" "bytes-copy!" "bytes-environment-variable-name?" "bytes-fill!" "bytes-join" "bytes-length" "bytes-no-nuls?" "bytes-open-converter" "bytes-ref" "bytes-set!" "bytes-utf-8-index" "bytes-utf-8-length" "bytes-utf-8-ref" "bytes?" "bytes?" "caaaar" "caaadr" "caaar" "caadar" "caaddr" "caadr" "caar" "cadaar" "cadadr" "cadar" "caddar" "cadddr" "caddr" "cadr" "call-in-continuation" "call-in-nested-thread" "call-with-atomic-output-file" "call-with-break-parameterization" "call-with-composable-continuation" "call-with-continuation-barrier" "call-with-continuation-prompt" "call-with-current-continuation" "call-with-default-reading-parameterization" "call-with-escape-continuation" "call-with-exception-handler" "call-with-file-lock/timeout" "call-with-immediate-continuation-mark" "call-with-input-bytes" "call-with-input-file" "call-with-input-file*" "call-with-input-string" "call-with-output-bytes" "call-with-output-file" "call-with-output-file*" "call-with-output-string" "call-with-parameterization" "call-with-semaphore" "call-with-semaphore/enable-break" "call-with-values" "call/cc" "call/ec" "car" "cartesian-product" "cdaaar" "cdaadr" "cdaar" "cdadar" "cdaddr" "cdadr" "cdar" "cddaar" "cddadr" "cddar" "cdddar" "cddddr" "cdddr" "cddr" "cdr" "ceiling" "channel-get" "channel-put" "channel-put-evt" "channel-put-evt?" "channel-try-get" "channel/c" "channel?" "chaperone-box" "chaperone-channel" "chaperone-continuation-mark-key" "chaperone-contract-property?" "chaperone-contract?" "chaperone-evt" "chaperone-hash" "chaperone-hash-set" "chaperone-of?" "chaperone-procedure" "chaperone-procedure*" "chaperone-prompt-tag" "chaperone-struct" "chaperone-struct-type" "chaperone-vector" "chaperone-vector*" "chaperone?" "char->integer" "char-alphabetic?" "char-blank?" "char-ci<=?" "char-ci=?" "char-ci>?" "char-downcase" "char-foldcase" "char-general-category" "char-graphic?" "char-in" "char-in/c" "char-iso-control?" "char-lower-case?" "char-numeric?" "char-punctuation?" "char-ready?" "char-symbolic?" "char-title-case?" "char-titlecase" "char-upcase" "char-upper-case?" "char-utf-8-length" "char-whitespace?" "char<=?" "char=?" "char>?" "char?" "check-duplicate-identifier" "check-duplicates" "checked-procedure-check-and-extract" "choice-evt" "class->interface" "class-info" "class-seal" "class-unseal" "class?" "cleanse-path" "close-input-port" "close-output-port" "coerce-chaperone-contract" "coerce-chaperone-contracts" "coerce-contract" "coerce-contract/f" "coerce-contracts" "coerce-flat-contract" "coerce-flat-contracts" "collect-garbage" "collection-file-path" "collection-path" "combinations" "combine-output" "compile" "compile-allow-set!-undefined" "compile-context-preservation-enabled" "compile-enforce-module-constants" "compile-syntax" "compile-target-machine?" "compiled-expression-recompile" "compiled-expression?" "compiled-module-expression?" "complete-path?" "complex?" "compose" "compose1" "conjoin" "conjugate" "cons" "cons/c" "cons?" "const" "continuation-mark-key/c" "continuation-mark-key?" "continuation-mark-set->context" "continuation-mark-set->iterator" "continuation-mark-set->list" "continuation-mark-set->list*" "continuation-mark-set-first" "continuation-mark-set?" "continuation-marks" "continuation-prompt-available?" "continuation-prompt-tag?" "continuation?" "contract-custom-write-property-proc" "contract-equivalent?" "contract-exercise" "contract-first-order" "contract-first-order-passes?" "contract-late-neg-projection" "contract-name" "contract-proc" "contract-projection" "contract-property?" "contract-random-generate" "contract-random-generate-env?" "contract-random-generate-fail?" "contract-random-generate-get-current-environment" "contract-random-generate-stash" "contract-random-generate/choose" "contract-stronger?" "contract-struct-exercise" "contract-struct-generate" "contract-struct-late-neg-projection" "contract-struct-list-contract?" "contract-val-first-projection" "contract?" "convert-stream" "copy-directory/files" "copy-file" "copy-port" "cos" "cosh" "count" "current-blame-format" "current-break-parameterization" "current-code-inspector" "current-command-line-arguments" "current-compile" "current-compile-realm" "current-compile-target-machine" "current-compiled-file-roots" "current-continuation-marks" "current-custodian" "current-directory" "current-directory-for-user" "current-drive" "current-environment-variables" "current-error-message-adjuster" "current-error-port" "current-eval" "current-evt-pseudo-random-generator" "current-force-delete-permissions" "current-future" "current-gc-milliseconds" "current-get-interaction-evt" "current-get-interaction-input-port" "current-inexact-milliseconds" "current-inexact-monotonic-milliseconds" "current-input-port" "current-inspector" "current-library-collection-links" "current-library-collection-paths" "current-load" "current-load-extension" "current-load-relative-directory" "current-load/use-compiled" "current-locale" "current-logger" "current-memory-use" "current-milliseconds" "current-module-declare-name" "current-module-declare-source" "current-module-name-resolver" "current-module-path-for-load" "current-namespace" "current-output-port" "current-parameterization" "current-plumber" "current-preserved-thread-cell-values" "current-print" "current-process-milliseconds" "current-prompt-read" "current-pseudo-random-generator" "current-read-interaction" "current-reader-guard" "current-readtable" "current-seconds" "current-security-guard" "current-subprocess-custodian-mode" "current-subprocess-keep-file-descriptors" "current-thread" "current-thread-group" "current-thread-initial-stack-size" "current-write-relative-directory" "curry" "curryr" "custodian-box-value" "custodian-box?" "custodian-limit-memory" "custodian-managed-list" "custodian-memory-accounting-available?" "custodian-require-memory" "custodian-shut-down?" "custodian-shutdown-all" "custodian?" "custom-print-quotable-accessor" "custom-print-quotable?" "custom-write-accessor" "custom-write-property-proc" "custom-write?" "date" "date*" "date*-nanosecond" "date*-time-zone-name" "date*?" "date-day" "date-dst?" "date-hour" "date-minute" "date-month" "date-second" "date-time-zone-offset" "date-week-day" "date-year" "date-year-day" "date?" "datum->syntax" "datum-intern-literal" "default-continuation-prompt-tag" "degrees->radians" "delete-directory" "delete-directory/files" "delete-file" "denominator" "dict->list" "dict-can-functional-set?" "dict-can-remove-keys?" "dict-clear" "dict-clear!" "dict-copy" "dict-count" "dict-empty?" "dict-for-each" "dict-has-key?" "dict-implements/c" "dict-implements?" "dict-iter-contract" "dict-iterate-first" "dict-iterate-key" "dict-iterate-next" "dict-iterate-value" "dict-key-contract" "dict-keys" "dict-map" "dict-mutable?" "dict-ref" "dict-ref!" "dict-remove" "dict-remove!" "dict-set" "dict-set!" "dict-set*" "dict-set*!" "dict-update" "dict-update!" "dict-value-contract" "dict-values" "dict?" "directory-exists?" "directory-list" "disjoin" "display" "display-lines" "display-lines-to-file" "display-to-file" "displayln" "double-flonum?" "drop" "drop-common-prefix" "drop-right" "dropf" "dropf-right" "dump-memory-stats" "dup-input-port" "dup-output-port" "dynamic->*" "dynamic-get-field" "dynamic-object/c" "dynamic-place" "dynamic-place*" "dynamic-require" "dynamic-require-for-syntax" "dynamic-send" "dynamic-set-field!" "dynamic-wind" "eighth" "empty?" "environment-variables-copy" "environment-variables-names" "environment-variables-ref" "environment-variables-set!" "environment-variables?" "eof-evt" "eof-object?" "ephemeron-value" "ephemeron?" "eprintf" "eq-contract-val" "eq-contract?" "eq-hash-code" "eq?" "equal-contract-val" "equal-contract?" "equal-hash-code" "equal-secondary-hash-code" "equal?" "equal?/recur" "eqv-hash-code" "eqv?" "error" "error-contract->adjusted-string" "error-display-handler" "error-escape-handler" "error-message->adjusted-string" "error-print-context-length" "error-print-source-location" "error-print-width" "error-syntax->string-handler" "error-value->string-handler" "eval" "eval-jit-enabled" "eval-syntax" "even?" "evt/c" "evt?" "exact->inexact" "exact-ceiling" "exact-floor" "exact-integer?" "exact-nonnegative-integer?" "exact-positive-integer?" "exact-round" "exact-truncate" "exact?" "executable-yield-handler" "exit" "exit-handler" "exn" "exn-continuation-marks" "exn-message" "exn:break" "exn:break-continuation" "exn:break:hang-up" "exn:break:hang-up?" "exn:break:terminate" "exn:break:terminate?" "exn:break?" "exn:fail" "exn:fail:contract" "exn:fail:contract:arity" "exn:fail:contract:arity?" "exn:fail:contract:blame" "exn:fail:contract:blame-object" "exn:fail:contract:blame?" "exn:fail:contract:continuation" "exn:fail:contract:continuation?" "exn:fail:contract:divide-by-zero" "exn:fail:contract:divide-by-zero?" "exn:fail:contract:non-fixnum-result" "exn:fail:contract:non-fixnum-result?" "exn:fail:contract:variable" "exn:fail:contract:variable-id" "exn:fail:contract:variable?" "exn:fail:contract?" "exn:fail:filesystem" "exn:fail:filesystem:errno" "exn:fail:filesystem:errno-errno" "exn:fail:filesystem:errno?" "exn:fail:filesystem:exists" "exn:fail:filesystem:exists?" "exn:fail:filesystem:missing-module" "exn:fail:filesystem:missing-module-path" "exn:fail:filesystem:missing-module?" "exn:fail:filesystem:version" "exn:fail:filesystem:version?" "exn:fail:filesystem?" "exn:fail:network" "exn:fail:network:errno" "exn:fail:network:errno-errno" "exn:fail:network:errno?" "exn:fail:network?" "exn:fail:object" "exn:fail:object?" "exn:fail:out-of-memory" "exn:fail:out-of-memory?" "exn:fail:read" "exn:fail:read-srclocs" "exn:fail:read:eof" "exn:fail:read:eof?" "exn:fail:read:non-char" "exn:fail:read:non-char?" "exn:fail:read?" "exn:fail:syntax" "exn:fail:syntax-exprs" "exn:fail:syntax:missing-module" "exn:fail:syntax:missing-module-path" "exn:fail:syntax:missing-module?" "exn:fail:syntax:unbound" "exn:fail:syntax:unbound?" "exn:fail:syntax?" "exn:fail:unsupported" "exn:fail:unsupported?" "exn:fail:user" "exn:fail:user?" "exn:fail?" "exn:misc:match?" "exn:missing-module-accessor" "exn:missing-module?" "exn:srclocs-accessor" "exn:srclocs?" "exn?" "exp" "expand" "expand-once" "expand-syntax" "expand-syntax-once" "expand-syntax-to-top-form" "expand-to-top-form" "expand-user-path" "explode-path" "expt" "false?" "field-names" "fifth" "file->bytes" "file->bytes-lines" "file->lines" "file->list" "file->string" "file->value" "file-exists?" "file-name-from-path" "file-or-directory-identity" "file-or-directory-modify-seconds" "file-or-directory-permissions" "file-or-directory-stat" "file-or-directory-type" "file-position" "file-position*" "file-size" "file-stream-buffer-mode" "file-stream-port?" "file-truncate" "filename-extension" "filesystem-change-evt" "filesystem-change-evt-cancel" "filesystem-change-evt?" "filesystem-root-list" "filter" "filter-map" "filter-not" "filter-read-input-port" "find-compiled-file-roots" "find-executable-path" "find-files" "find-library-collection-links" "find-library-collection-paths" "find-relative-path" "find-system-path" "findf" "first" "first-or/c" "fixnum?" "flat-contract" "flat-contract-predicate" "flat-contract-property?" "flat-contract-with-explanation" "flat-contract?" "flat-named-contract" "flatten" "floating-point-bytes->real" "flonum?" "floor" "flush-output" "fold-files" "foldl" "foldr" "for-each" "force" "format" "fourth" "fprintf" "free-identifier=?" "free-label-identifier=?" "free-template-identifier=?" "free-transformer-identifier=?" "fsemaphore-count" "fsemaphore-post" "fsemaphore-try-wait?" "fsemaphore-wait" "fsemaphore?" "future" "future?" "futures-enabled?" "gcd" "generate-member-key" "generate-temporaries" "generic-set?" "generic?" "gensym" "get-output-bytes" "get-output-string" "get-preference" "get/build-late-neg-projection" "get/build-val-first-projection" "getenv" "global-port-print-handler" "group-by" "guard-evt" "handle-evt" "handle-evt?" "has-blame?" "has-contract?" "hash" "hash->list" "hash-clear" "hash-clear!" "hash-copy" "hash-copy-clear" "hash-count" "hash-empty?" "hash-ephemeron?" "hash-eq?" "hash-equal?" "hash-eqv?" "hash-for-each" "hash-has-key?" "hash-iterate-first" "hash-iterate-key" "hash-iterate-key+value" "hash-iterate-next" "hash-iterate-pair" "hash-iterate-value" "hash-keys" "hash-keys-subset?" "hash-map" "hash-placeholder?" "hash-ref" "hash-ref!" "hash-ref-key" "hash-remove" "hash-remove!" "hash-set" "hash-set!" "hash-set*" "hash-set*!" "hash-strong?" "hash-update" "hash-update!" "hash-values" "hash-weak?" "hash/c" "hash?" "hasheq" "hasheqv" "identifier-binding" "identifier-binding-portal-syntax" "identifier-binding-symbol" "identifier-distinct-binding" "identifier-label-binding" "identifier-prune-lexical-context" "identifier-prune-to-source-module" "identifier-remove-from-definition-context" "identifier-template-binding" "identifier-transformer-binding" "identifier?" "identity" "if/c" "imag-part" "immutable?" "impersonate-box" "impersonate-channel" "impersonate-continuation-mark-key" "impersonate-hash" "impersonate-hash-set" "impersonate-procedure" "impersonate-procedure*" "impersonate-prompt-tag" "impersonate-struct" "impersonate-vector" "impersonate-vector*" "impersonator-contract?" "impersonator-ephemeron" "impersonator-of?" "impersonator-property-accessor-procedure?" "impersonator-property?" "impersonator?" "implementation?" "implementation?/c" "in-bytes" "in-bytes-lines" "in-combinations" "in-cycle" "in-dict" "in-dict-keys" "in-dict-pairs" "in-dict-values" "in-directory" "in-ephemeron-hash" "in-ephemeron-hash-keys" "in-ephemeron-hash-pairs" "in-ephemeron-hash-values" "in-hash" "in-hash-keys" "in-hash-pairs" "in-hash-values" "in-immutable-hash" "in-immutable-hash-keys" "in-immutable-hash-pairs" "in-immutable-hash-values" "in-immutable-set" "in-inclusive-range" "in-indexed" "in-input-port-bytes" "in-input-port-chars" "in-lines" "in-list" "in-mlist" "in-mutable-hash" "in-mutable-hash-keys" "in-mutable-hash-pairs" "in-mutable-hash-values" "in-mutable-set" "in-naturals" "in-parallel" "in-permutations" "in-port" "in-producer" "in-range" "in-sequences" "in-set" "in-slice" "in-stream" "in-string" "in-syntax" "in-value" "in-values*-sequence" "in-values-sequence" "in-vector" "in-weak-hash" "in-weak-hash-keys" "in-weak-hash-pairs" "in-weak-hash-values" "in-weak-set" "inclusive-range" "index-of" "index-where" "indexes-of" "indexes-where" "inexact->exact" "inexact-real?" "inexact?" "infinite?" "input-port-append" "input-port?" "inspector-superior?" "inspector?" "instanceof/c" "integer->char" "integer->integer-bytes" "integer-bytes->integer" "integer-in" "integer-length" "integer-sqrt" "integer-sqrt/remainder" "integer?" "interface->method-names" "interface-extension?" "interface?" "internal-definition-context-add-scopes" "internal-definition-context-binding-identifiers" "internal-definition-context-introduce" "internal-definition-context-seal" "internal-definition-context-splice-binding-identifier" "internal-definition-context?" "is-a?" "is-a?/c" "keyword->string" "keyword-apply" "keyword-apply/dict" "keywordbytes" "list->mutable-set" "list->mutable-seteq" "list->mutable-seteqv" "list->set" "list->seteq" "list->seteqv" "list->string" "list->vector" "list->weak-set" "list->weak-seteq" "list->weak-seteqv" "list-contract?" "list-prefix?" "list-ref" "list-set" "list-tail" "list-update" "list/c" "list?" "listen-port-number?" "listof" "load" "load-extension" "load-on-demand-enabled" "load-relative" "load-relative-extension" "load/cd" "load/use-compiled" "local-expand" "local-expand/capture-lifts" "local-transformer-expand" "local-transformer-expand/capture-lifts" "locale-string-encoding" "log" "log-all-levels" "log-level-evt" "log-level?" "log-max-level" "log-message" "log-receiver?" "logger-name" "logger?" "magnitude" "make-arity-at-least" "make-base-empty-namespace" "make-base-namespace" "make-bytes" "make-channel" "make-chaperone-contract" "make-continuation-mark-key" "make-continuation-prompt-tag" "make-contract" "make-custodian" "make-custodian-box" "make-custom-hash" "make-custom-hash-types" "make-custom-set" "make-custom-set-types" "make-date" "make-date*" "make-derived-parameter" "make-directory" "make-directory*" "make-do-sequence" "make-empty-namespace" "make-environment-variables" "make-ephemeron" "make-ephemeron-hash" "make-ephemeron-hasheq" "make-ephemeron-hasheqv" "make-exn" "make-exn:break" "make-exn:break:hang-up" "make-exn:break:terminate" "make-exn:fail" "make-exn:fail:contract" "make-exn:fail:contract:arity" "make-exn:fail:contract:blame" "make-exn:fail:contract:continuation" "make-exn:fail:contract:divide-by-zero" "make-exn:fail:contract:non-fixnum-result" "make-exn:fail:contract:variable" "make-exn:fail:filesystem" "make-exn:fail:filesystem:errno" "make-exn:fail:filesystem:exists" "make-exn:fail:filesystem:missing-module" "make-exn:fail:filesystem:version" "make-exn:fail:network" "make-exn:fail:network:errno" "make-exn:fail:object" "make-exn:fail:out-of-memory" "make-exn:fail:read" "make-exn:fail:read:eof" "make-exn:fail:read:non-char" "make-exn:fail:syntax" "make-exn:fail:syntax:missing-module" "make-exn:fail:syntax:unbound" "make-exn:fail:unsupported" "make-exn:fail:user" "make-file-or-directory-link" "make-flat-contract" "make-fsemaphore" "make-generic" "make-handle-get-preference-locked" "make-hash" "make-hash-placeholder" "make-hasheq" "make-hasheq-placeholder" "make-hasheqv" "make-hasheqv-placeholder" "make-immutable-custom-hash" "make-immutable-hash" "make-immutable-hasheq" "make-immutable-hasheqv" "make-impersonator-property" "make-input-port" "make-input-port/read-to-peek" "make-inspector" "make-interned-syntax-introducer" "make-keyword-procedure" "make-known-char-range-list" "make-limited-input-port" "make-list" "make-lock-file-name" "make-log-receiver" "make-logger" "make-mixin-contract" "make-mutable-custom-set" "make-none/c" "make-object" "make-output-port" "make-parameter" "make-parent-directory*" "make-phantom-bytes" "make-pipe" "make-pipe-with-specials" "make-placeholder" "make-plumber" "make-polar" "make-portal-syntax" "make-prefab-struct" "make-primitive-class" "make-proj-contract" "make-pseudo-random-generator" "make-reader-graph" "make-readtable" "make-rectangular" "make-rename-transformer" "make-resolved-module-path" "make-security-guard" "make-semaphore" "make-set!-transformer" "make-shared-bytes" "make-sibling-inspector" "make-special-comment" "make-srcloc" "make-string" "make-struct-field-accessor" "make-struct-field-mutator" "make-struct-type" "make-struct-type-property" "make-syntax-delta-introducer" "make-syntax-introducer" "make-temporary-directory" "make-temporary-directory*" "make-temporary-file" "make-temporary-file*" "make-tentative-pretty-print-output-port" "make-thread-cell" "make-thread-group" "make-vector" "make-weak-box" "make-weak-custom-hash" "make-weak-custom-set" "make-weak-hash" "make-weak-hasheq" "make-weak-hasheqv" "make-will-executor" "map" "match-equality-test" "matches-arity-exactly?" "max" "mcar" "mcdr" "mcons" "member" "member-name-key-hash-code" "member-name-key=?" "member-name-key?" "memf" "memory-order-acquire" "memory-order-release" "memq" "memv" "merge-input" "method-in-interface?" "min" "module->exports" "module->imports" "module->indirect-exports" "module->language-info" "module->namespace" "module->realm" "module-compiled-cross-phase-persistent?" "module-compiled-exports" "module-compiled-imports" "module-compiled-indirect-exports" "module-compiled-language-info" "module-compiled-name" "module-compiled-realm" "module-compiled-submodules" "module-declared?" "module-path-index-join" "module-path-index-resolve" "module-path-index-split" "module-path-index-submodule" "module-path-index?" "module-path?" "module-predefined?" "module-provide-protected?" "modulo" "mpair?" "mutable-set" "mutable-seteq" "mutable-seteqv" "n->th" "nack-guard-evt" "namespace-anchor->empty-namespace" "namespace-anchor->namespace" "namespace-anchor?" "namespace-attach-module" "namespace-attach-module-declaration" "namespace-base-phase" "namespace-call-with-registry-lock" "namespace-mapped-symbols" "namespace-module-identifier" "namespace-module-registry" "namespace-require" "namespace-require/constant" "namespace-require/copy" "namespace-require/expansion-time" "namespace-set-variable-value!" "namespace-symbol->identifier" "namespace-syntax-introduce" "namespace-undefine-variable!" "namespace-unprotect-module" "namespace-variable-value" "namespace?" "nan?" "natural-number/c" "natural?" "negate" "negative-integer?" "negative?" "new-∀/c" "new-∃/c" "newline" "ninth" "non-empty-listof" "non-empty-string?" "none/c" "nonnegative-integer?" "nonpositive-integer?" "normal-case-path" "normalize-arity" "normalize-path" "normalized-arity?" "not" "not/c" "null?" "number->string" "number?" "numerator" "object->vector" "object-info" "object-interface" "object-method-arity-includes?" "object-name" "object-or-false=?" "object=-hash-code" "object=?" "object?" "odd?" "one-of/c" "open-input-bytes" "open-input-file" "open-input-output-file" "open-input-string" "open-output-bytes" "open-output-file" "open-output-nowhere" "open-output-string" "or/c" "order-of-magnitude" "ormap" "output-port?" "pair?" "parameter-procedure=?" "parameter/c" "parameter?" "parameterization?" "parse-command-line" "partition" "path->bytes" "path->complete-path" "path->directory-path" "path->string" "path-add-extension" "path-add-suffix" "path-convention-type" "path-element->bytes" "path-element->string" "path-element?" "path-for-some-system?" "path-get-extension" "path-has-extension?" "path-list-string->path-list" "path-only" "path-replace-extension" "path-replace-suffix" "path-string?" "pathbytes" "port->bytes-lines" "port->lines" "port->list" "port->string" "port-closed-evt" "port-closed?" "port-commit-peeked" "port-count-lines!" "port-count-lines-enabled" "port-counts-lines?" "port-display-handler" "port-file-identity" "port-file-unlock" "port-next-location" "port-number?" "port-print-handler" "port-progress-evt" "port-provides-progress-evts?" "port-read-handler" "port-try-file-lock?" "port-waiting-peer?" "port-write-handler" "port-writes-atomic?" "port-writes-special?" "port?" "portal-syntax-content" "portal-syntax?" "positive-integer?" "positive?" "prefab-key->struct-type" "prefab-key?" "prefab-struct-key" "preferences-lock-file-mode" "pregexp" "pregexp?" "pretty-display" "pretty-format" "pretty-print" "pretty-print-.-symbol-without-bars" "pretty-print-abbreviate-read-macros" "pretty-print-columns" "pretty-print-current-style-table" "pretty-print-depth" "pretty-print-exact-as-decimal" "pretty-print-extend-style-table" "pretty-print-handler" "pretty-print-newline" "pretty-print-post-print-hook" "pretty-print-pre-print-hook" "pretty-print-print-hook" "pretty-print-print-line" "pretty-print-remap-stylable" "pretty-print-show-inexactness" "pretty-print-size-hook" "pretty-print-style-table?" "pretty-printing" "pretty-write" "primitive-closure?" "primitive-result-arity" "primitive?" "print" "print-as-expression" "print-boolean-long-form" "print-box" "print-graph" "print-hash-table" "print-mpair-curly-braces" "print-pair-curly-braces" "print-reader-abbreviations" "print-struct" "print-syntax-width" "print-unreadable" "print-value-columns" "print-vector-length" "printable/c" "printf" "println" "procedure->method" "procedure-arity" "procedure-arity-includes/c" "procedure-arity-includes?" "procedure-arity-mask" "procedure-arity?" "procedure-closure-contents-eq?" "procedure-extract-target" "procedure-impersonator*?" "procedure-keywords" "procedure-realm" "procedure-reduce-arity" "procedure-reduce-arity-mask" "procedure-reduce-keyword-arity" "procedure-reduce-keyword-arity-mask" "procedure-rename" "procedure-result-arity" "procedure-specialize" "procedure-struct-type?" "procedure?" "process" "process*" "process*/ports" "process/ports" "processor-count" "progress-evt?" "promise-forced?" "promise-running?" "promise/c" "promise/name?" "promise?" "prop:arrow-contract-get-info" "prop:arrow-contract?" "prop:orc-contract-get-subcontracts" "prop:orc-contract?" "prop:recursive-contract-unroll" "prop:recursive-contract?" "proper-subset?" "property/c" "pseudo-random-generator->vector" "pseudo-random-generator-vector?" "pseudo-random-generator?" "put-preferences" "putenv" "quotient" "quotient/remainder" "radians->degrees" "raise" "raise-argument-error" "raise-argument-error*" "raise-arguments-error" "raise-arguments-error*" "raise-arity-error" "raise-arity-error*" "raise-arity-mask-error" "raise-arity-mask-error*" "raise-blame-error" "raise-contract-error" "raise-mismatch-error" "raise-not-cons-blame-error" "raise-range-error" "raise-range-error*" "raise-result-arity-error" "raise-result-arity-error*" "raise-result-error" "raise-result-error*" "raise-syntax-error" "raise-type-error" "raise-user-error" "random" "random-seed" "range" "rational?" "rationalize" "read" "read-accept-bar-quote" "read-accept-box" "read-accept-compiled" "read-accept-dot" "read-accept-graph" "read-accept-infix-dot" "read-accept-lang" "read-accept-quasiquote" "read-accept-reader" "read-byte" "read-byte-or-special" "read-bytes" "read-bytes!" "read-bytes!-evt" "read-bytes-avail!" "read-bytes-avail!*" "read-bytes-avail!-evt" "read-bytes-avail!/enable-break" "read-bytes-evt" "read-bytes-line" "read-bytes-line-evt" "read-case-sensitive" "read-cdot" "read-char" "read-char-or-special" "read-curly-brace-as-paren" "read-curly-brace-with-tag" "read-decimal-as-inexact" "read-eval-print-loop" "read-installation-configuration-table" "read-language" "read-line" "read-line-evt" "read-on-demand-source" "read-single-flonum" "read-square-bracket-as-paren" "read-square-bracket-with-tag" "read-string" "read-string!" "read-string!-evt" "read-string-evt" "read-syntax" "read-syntax-accept-graph" "read-syntax/recursive" "read/recursive" "readtable-mapping" "readtable?" "real->decimal-string" "real->double-flonum" "real->floating-point-bytes" "real->single-flonum" "real-in" "real-part" "real?" "reencode-input-port" "reencode-output-port" "regexp" "regexp-match" "regexp-match*" "regexp-match-evt" "regexp-match-exact?" "regexp-match-peek" "regexp-match-peek-immediate" "regexp-match-peek-positions" "regexp-match-peek-positions*" "regexp-match-peek-positions-immediate" "regexp-match-peek-positions-immediate/end" "regexp-match-peek-positions/end" "regexp-match-positions" "regexp-match-positions*" "regexp-match-positions/end" "regexp-match/end" "regexp-match?" "regexp-max-lookbehind" "regexp-quote" "regexp-replace" "regexp-replace*" "regexp-replace-quote" "regexp-replaces" "regexp-split" "regexp-try-match" "regexp?" "relative-path?" "relocate-input-port" "relocate-output-port" "remainder" "remf" "remf*" "remove" "remove*" "remove-duplicates" "remq" "remq*" "remv" "remv*" "rename-contract" "rename-file-or-directory" "rename-transformer-target" "rename-transformer?" "replace-evt" "reroot-path" "resolve-path" "resolved-module-path-name" "resolved-module-path?" "rest" "reverse" "round" "second" "seconds->date" "security-guard?" "semaphore-peek-evt" "semaphore-peek-evt?" "semaphore-post" "semaphore-try-wait?" "semaphore-wait" "semaphore-wait/enable-break" "semaphore?" "sequence->list" "sequence->stream" "sequence-add-between" "sequence-andmap" "sequence-append" "sequence-count" "sequence-filter" "sequence-fold" "sequence-for-each" "sequence-generate" "sequence-generate*" "sequence-length" "sequence-map" "sequence-ormap" "sequence-ref" "sequence-tail" "sequence/c" "sequence?" "set" "set!-transformer-procedure" "set!-transformer?" "set->list" "set->stream" "set-add" "set-add!" "set-box!" "set-box*!" "set-clear" "set-clear!" "set-copy" "set-copy-clear" "set-count" "set-empty?" "set-eq?" "set-equal?" "set-eqv?" "set-first" "set-for-each" "set-implements/c" "set-implements?" "set-intersect" "set-intersect!" "set-map" "set-mcar!" "set-mcdr!" "set-member?" "set-mutable?" "set-phantom-bytes!" "set-port-next-location!" "set-remove" "set-remove!" "set-rest" "set-subtract" "set-subtract!" "set-symmetric-difference" "set-symmetric-difference!" "set-union" "set-union!" "set-weak?" "set/c" "set=?" "set?" "seteq" "seteqv" "seventh" "sgn" "sha1-bytes" "sha224-bytes" "sha256-bytes" "shared-bytes" "shell-execute" "shrink-path-wrt" "shuffle" "simple-form-path" "simplify-path" "sin" "single-flonum-available?" "single-flonum?" "sinh" "sixth" "skip-projection-wrapper?" "sleep" "some-system-path->string" "sort" "special-comment-value" "special-comment?" "special-filter-input-port" "split-at" "split-at-right" "split-common-prefix" "split-path" "splitf-at" "splitf-at-right" "sqr" "sqrt" "srcloc" "srcloc->string" "srcloc-column" "srcloc-line" "srcloc-position" "srcloc-source" "srcloc-span" "srcloc?" "stop-after" "stop-before" "stream->list" "stream-add-between" "stream-andmap" "stream-append" "stream-count" "stream-empty?" "stream-filter" "stream-first" "stream-fold" "stream-for-each" "stream-force" "stream-length" "stream-map" "stream-ormap" "stream-ref" "stream-rest" "stream-tail" "stream-take" "stream/c" "stream?" "string" "string->bytes/latin-1" "string->bytes/locale" "string->bytes/utf-8" "string->immutable-string" "string->keyword" "string->list" "string->number" "string->path" "string->path-element" "string->some-system-path" "string->symbol" "string->uninterned-symbol" "string->unreadable-symbol" "string-append" "string-append*" "string-append-immutable" "string-ci<=?" "string-ci=?" "string-ci>?" "string-contains?" "string-copy" "string-copy!" "string-downcase" "string-environment-variable-name?" "string-fill!" "string-foldcase" "string-join" "string-len/c" "string-length" "string-locale-ci?" "string-locale-downcase" "string-locale-upcase" "string-locale?" "string-no-nuls?" "string-normalize-nfc" "string-normalize-nfd" "string-normalize-nfkc" "string-normalize-nfkd" "string-normalize-spaces" "string-port?" "string-prefix?" "string-ref" "string-replace" "string-set!" "string-split" "string-suffix?" "string-titlecase" "string-trim" "string-upcase" "string-utf-8-length" "string<=?" "string=?" "string>?" "string?" "struct->vector" "struct-accessor-procedure?" "struct-constructor-procedure?" "struct-info" "struct-mutator-procedure?" "struct-predicate-procedure?" "struct-type-authentic?" "struct-type-info" "struct-type-make-constructor" "struct-type-make-predicate" "struct-type-property-accessor-procedure?" "struct-type-property-predicate-procedure?" "struct-type-property/c" "struct-type-property?" "struct-type-sealed?" "struct-type?" "struct?" "sub1" "subbytes" "subclass?" "subclass?/c" "subprocess" "subprocess-group-enabled" "subprocess-kill" "subprocess-pid" "subprocess-status" "subprocess-wait" "subprocess?" "subset?" "substring" "suggest/c" "symbol->string" "symbol-interned?" "symbol-unreadable?" "symboldatum" "syntax->list" "syntax-arm" "syntax-binding-set" "syntax-binding-set->syntax" "syntax-binding-set-extend" "syntax-binding-set?" "syntax-column" "syntax-debug-info" "syntax-deserialize" "syntax-disarm" "syntax-e" "syntax-line" "syntax-local-apply-transformer" "syntax-local-bind-syntaxes" "syntax-local-certifier" "syntax-local-context" "syntax-local-expand-expression" "syntax-local-get-shadower" "syntax-local-identifier-as-binding" "syntax-local-introduce" "syntax-local-lift-context" "syntax-local-lift-expression" "syntax-local-lift-module" "syntax-local-lift-module-end-declaration" "syntax-local-lift-provide" "syntax-local-lift-require" "syntax-local-lift-values-expression" "syntax-local-make-definition-context" "syntax-local-make-delta-introducer" "syntax-local-module-defined-identifiers" "syntax-local-module-exports" "syntax-local-module-interned-scope-symbols" "syntax-local-module-required-identifiers" "syntax-local-name" "syntax-local-phase-level" "syntax-local-submodules" "syntax-local-transforming-module-provides?" "syntax-local-value" "syntax-local-value/immediate" "syntax-original?" "syntax-position" "syntax-property" "syntax-property-preserved?" "syntax-property-remove" "syntax-property-symbol-keys" "syntax-protect" "syntax-rearm" "syntax-recertify" "syntax-serialize" "syntax-shift-phase-level" "syntax-source" "syntax-source-module" "syntax-span" "syntax-taint" "syntax-tainted?" "syntax-track-origin" "syntax-transforming-module-expression?" "syntax-transforming-with-lifts?" "syntax-transforming?" "syntax/c" "syntax?" "system" "system*" "system*/exit-code" "system-big-endian?" "system-idle-evt" "system-language+country" "system-library-subpath" "system-path-convention-type" "system-type" "system/exit-code" "tail-marks-match?" "take" "take-common-prefix" "take-right" "takef" "takef-right" "tan" "tanh" "tcp-abandon-port" "tcp-accept" "tcp-accept-evt" "tcp-accept-ready?" "tcp-accept/enable-break" "tcp-addresses" "tcp-close" "tcp-connect" "tcp-connect/enable-break" "tcp-listen" "tcp-listener?" "tcp-port?" "tentative-pretty-print-port-cancel" "tentative-pretty-print-port-transfer" "tenth" "terminal-port?" "third" "thread" "thread-cell-ref" "thread-cell-set!" "thread-cell-values?" "thread-cell?" "thread-dead-evt" "thread-dead?" "thread-group?" "thread-receive" "thread-receive-evt" "thread-resume" "thread-resume-evt" "thread-rewind-receive" "thread-running?" "thread-send" "thread-suspend" "thread-suspend-evt" "thread-try-receive" "thread-wait" "thread/suspend-to-kill" "thread?" "time-apply" "touch" "transplant-input-port" "transplant-output-port" "truncate" "udp-addresses" "udp-bind!" "udp-bound?" "udp-close" "udp-connect!" "udp-connected?" "udp-multicast-interface" "udp-multicast-join-group!" "udp-multicast-leave-group!" "udp-multicast-loopback?" "udp-multicast-set-interface!" "udp-multicast-set-loopback!" "udp-multicast-set-ttl!" "udp-multicast-ttl" "udp-open-socket" "udp-receive!" "udp-receive!*" "udp-receive!-evt" "udp-receive!/enable-break" "udp-receive-ready-evt" "udp-send" "udp-send*" "udp-send-evt" "udp-send-ready-evt" "udp-send-to" "udp-send-to*" "udp-send-to-evt" "udp-send-to/enable-break" "udp-send/enable-break" "udp-set-receive-buffer-size!" "udp-set-ttl!" "udp-ttl" "udp?" "unbox" "unbox*" "uncaught-exception-handler" "unit?" "unquoted-printing-string" "unquoted-printing-string-value" "unquoted-printing-string?" "unsupplied-arg?" "use-collection-link-paths" "use-compiled-file-check" "use-compiled-file-paths" "use-user-specific-search-paths" "value-blame" "value-contract" "values" "variable-reference->empty-namespace" "variable-reference->module-base-phase" "variable-reference->module-declaration-inspector" "variable-reference->module-path-index" "variable-reference->module-source" "variable-reference->namespace" "variable-reference->phase" "variable-reference->resolved-module-path" "variable-reference-constant?" "variable-reference-from-unsafe?" "variable-reference?" "vector" "vector*-length" "vector*-ref" "vector*-set!" "vector->immutable-vector" "vector->list" "vector->pseudo-random-generator" "vector->pseudo-random-generator!" "vector->values" "vector-append" "vector-argmax" "vector-argmin" "vector-cas!" "vector-copy" "vector-copy!" "vector-count" "vector-drop" "vector-drop-right" "vector-empty?" "vector-fill!" "vector-filter" "vector-filter-not" "vector-immutable" "vector-immutable/c" "vector-immutableof" "vector-length" "vector-map" "vector-map!" "vector-member" "vector-memq" "vector-memv" "vector-ref" "vector-set!" "vector-set*!" "vector-set-performance-stats!" "vector-sort" "vector-sort!" "vector-split-at" "vector-split-at-right" "vector-take" "vector-take-right" "vector/c" "vector?" "vectorof" "version" "void" "void?" "weak-box-value" "weak-box?" "weak-set" "weak-seteq" "weak-seteqv" "will-execute" "will-executor?" "will-register" "will-try-execute" "with-input-from-bytes" "with-input-from-file" "with-input-from-string" "with-output-to-bytes" "with-output-to-file" "with-output-to-string" "would-be-future" "wrap-evt" "write" "write-byte" "write-bytes" "write-bytes-avail" "write-bytes-avail*" "write-bytes-avail-evt" "write-bytes-avail/enable-break" "write-char" "write-special" "write-special-avail*" "write-special-evt" "write-string" "write-to-file" "writeln" "xor" "zero?" "~.a" "~.s" "~.v" "~a" "~e" "~r" "~s" "~v" + )) + +;; operators ;; + +((symbol) @operator + (#any-of? @operator + "+" "-" "*" "/" "=" "<=" ">=" "<" ">")) + +;; builtin variables ;; + +((symbol) @variable.builtin + (#any-of? @variable.builtin + "always-evt" "block-device-type-bits" "character-device-type-bits" "check-tail-contract" "contract-continuation-mark-key" "contract-random-generate-fail" "directory-type-bits" "empty" "empty-sequence" "empty-stream" "eof" "equal<%>" "error-message-adjuster-key" "externalizable<%>" "failure-result/c" "false" "false/c" "fifo-type-bits" "file-type-bits" "for-clause-syntax-protect" "group-execute-bit" "group-permission-bits" "group-read-bit" "group-write-bit" "impersonator-prop:application-mark" "impersonator-prop:blame" "impersonator-prop:contracted" "legacy-match-expander?" "match-...-nesting" "match-expander?" "mixin-contract" "never-evt" "null" "object%" "other-execute-bit" "other-permission-bits" "other-read-bit" "other-write-bit" "pi" "pi.f" "predicate/c" "printable<%>" "prop:arity-string" "prop:arrow-contract" "prop:authentic" "prop:blame" "prop:chaperone-contract" "prop:checked-procedure" "prop:contract" "prop:contracted" "prop:custom-print-quotable" "prop:custom-write" "prop:dict" "prop:equal+hash" "prop:evt" "prop:exn:missing-module" "prop:exn:srclocs" "prop:expansion-contexts" "prop:flat-contract" "prop:impersonator-of" "prop:input-port" "prop:legacy-match-expander" "prop:liberal-define-context" "prop:match-expander" "prop:object-name" "prop:orc-contract" "prop:output-port" "prop:place-location" "prop:procedure" "prop:recursive-contract" "prop:rename-transformer" "prop:sealed" "prop:sequence" "prop:set!-transformer" "prop:stream" "regular-file-type-bits" "set-group-id-bit" "set-user-id-bit" "socket-type-bits" "sticky-bit" "struct:arity-at-least" "struct:arrow-contract-info" "struct:date" "struct:date*" "struct:exn" "struct:exn:break" "struct:exn:break:hang-up" "struct:exn:break:terminate" "struct:exn:fail" "struct:exn:fail:contract" "struct:exn:fail:contract:arity" "struct:exn:fail:contract:blame" "struct:exn:fail:contract:continuation" "struct:exn:fail:contract:divide-by-zero" "struct:exn:fail:contract:non-fixnum-result" "struct:exn:fail:contract:variable" "struct:exn:fail:filesystem" "struct:exn:fail:filesystem:errno" "struct:exn:fail:filesystem:exists" "struct:exn:fail:filesystem:missing-module" "struct:exn:fail:filesystem:version" "struct:exn:fail:network" "struct:exn:fail:network:errno" "struct:exn:fail:object" "struct:exn:fail:out-of-memory" "struct:exn:fail:read" "struct:exn:fail:read:eof" "struct:exn:fail:read:non-char" "struct:exn:fail:syntax" "struct:exn:fail:syntax:missing-module" "struct:exn:fail:syntax:unbound" "struct:exn:fail:unsupported" "struct:exn:fail:user" "struct:srcloc" "symbolic-link-type-bits" "syntax-local-match-introduce" "syntax-pattern-variable?" "the-unsupplied-arg" "true" "unspecified-dom" "user-execute-bit" "user-permission-bits" "user-read-bit" "user-write-bit" "writable<%>" + )) + +(dot) @variable.builtin + +;;------------------------------------------------------------------;; +;; Special cases ;; +;;------------------------------------------------------------------;; + +(list + "[" + (symbol) @variable + "]") + +(list + . + (symbol) @_p + . + (list + (symbol) @variable) + (#any-of? @_p + "lambda" "λ" "define-values" "define-syntaxes" "define-values-for-export" + "define-values-for-syntax" + )) + +;;------------------------------------------------------------------;; +;; Solve conflicts ;; +;;------------------------------------------------------------------;; + +;; See `:h treesitter`, and search `priority` + +(list + . + (symbol) @include + (#eq? @include "require") + (#set! "priority" 101)) + +(quote + . + (symbol) + (#set! "priority" 105)) @symbol + +((sexp_comment) @comment + (#set! "priority" 110)) diff --git a/tree-sitter/highlights/rasi.scm b/tree-sitter/highlights/rasi.scm new file mode 100644 index 0000000000..e2be63ffbe --- /dev/null +++ b/tree-sitter/highlights/rasi.scm @@ -0,0 +1,82 @@ +(comment) @comment + +"@media" @keyword +"@import" @include +"@theme" @include + +(string_value) @string +[ + (integer_value) + (float_value) + "0" + ] @number +(boolean_value) @boolean + +[ + (feature_name) + (url_image_scale) + (direction) + (text_style_value) + (line_style_value) + (position_value) + (orientation_value) + (cursor_value) + "inherit" + ] @keyword + + +(url_image "url" @function.builtin) +(gradient_image "linear-gradient" @function.builtin) +(distance_calc "calc" @function.builtin) +(rgb_color ["rgb" "rgba"] @function.builtin) +(hsl_color ["hsl" "hsla"] @function.builtin) +(hwb_color ["hwb" "hwba"] @function.builtin) +(cmyk_color "cmyk" @function.builtin) + +[ + "(" + ")" + "{" + "}" + "[" + "]" + ] @punctuation.bracket + +(distance_op) @operator + +[ + ";" + "," + ":" + "." + ] @punctuation.delimiter + +[ + (angle_unit) + (integer_distance_unit) + (float_distance_unit) + ] @type +(percentage) @number +(percentage "%" @type) + +[ + (global_selector) + (id_selector) + ] @namespace + +(id_selector_view [ "normal" "selected" "alternate" ] @attribute) +(id_selector_state [ "normal" "urgent" "active" ] @type.qualifier) + +(hex_color) @number +(hex_color "#" @punctuation.special) +(named_color (identifier) @string.special) +(named_color "/" @operator) +(reference_value "@" @punctuation.special (identifier) @variable) +(reference_value "var" @function.builtin (identifier) @variable) +(list_value (identifier) @variable) +(environ_value "$" @punctuation.special (identifier) @variable) +(environ_value "env" @function.builtin (identifier) @variable) + +(property_name) @variable + +(ERROR) @error diff --git a/tree-sitter/highlights/regex.scm b/tree-sitter/highlights/regex.scm new file mode 100644 index 0000000000..bf0aa59342 --- /dev/null +++ b/tree-sitter/highlights/regex.scm @@ -0,0 +1,34 @@ +;; Forked from tree-sitter-regex +;; The MIT License (MIT) Copyright (c) 2014 Max Brunsfeld +[ + "(" + ")" + "(?" + "(?:" + "(?<" + ">" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +(group_name) @property + +;; These are escaped special characters that lost their special meaning +;; -> no special highlighting +(identity_escape) @string.regex + +(class_character) @constant + +[ + (control_letter_escape) + (character_class_escape) + (control_escape) + (start_assertion) + (end_assertion) + (boundary_assertion) + (non_boundary_assertion) +] @string.escape + +[ "*" "+" "?" "|" "=" "!" ] @operator diff --git a/tree-sitter/highlights/rego.scm b/tree-sitter/highlights/rego.scm new file mode 100644 index 0000000000..31210b6d2c --- /dev/null +++ b/tree-sitter/highlights/rego.scm @@ -0,0 +1,64 @@ +; highlights.scm +[ + (import) + (package) +] @include + +[ + (with) + (as) + (every) + (some) + (in) + (not) + (if) + (contains) + (else) + (default) + "null" +] @keyword + +[ + "true" + "false" +] @boolean + +[ + (assignment_operator) + (bool_operator) + (arith_operator) + (bin_operator) +] @operator + +[ + (string) + (raw_string) +] @string + +(term (ref (var))) @variable + +(comment) @comment + +(number) @number + +(expr_call func_name: (fn_name (var) @function .)) + +(expr_call func_arguments: (fn_args (expr) @parameter)) + +(rule_args (term) @parameter) + +[ + (open_paren) + (close_paren) + (open_bracket) + (close_bracket) + (open_curly) + (close_curly) +] @punctuation.bracket + +(rule (rule_head (var) @method)) + +(rule + (rule_head (term (ref (var) @namespace))) + (rule_body (query (literal (expr (expr_infix (expr (term (ref (var)) @_output)))))) (#eq? @_output @namespace)) +) diff --git a/tree-sitter/highlights/rnoweb.scm b/tree-sitter/highlights/rnoweb.scm new file mode 100644 index 0000000000..4bed5c94e0 --- /dev/null +++ b/tree-sitter/highlights/rnoweb.scm @@ -0,0 +1,2 @@ +;; General syntax +;(ERROR) @error diff --git a/tree-sitter/highlights/robot.scm b/tree-sitter/highlights/robot.scm new file mode 100644 index 0000000000..abff22c428 --- /dev/null +++ b/tree-sitter/highlights/robot.scm @@ -0,0 +1,21 @@ +(argument (dictionary_variable) @string.special) +(argument (list_variable) @string.special) +(argument (scalar_variable) @string.special) +(argument (text_chunk) @string) + +(keyword_invocation (keyword) @function) + +(test_case_definition (name) @property) + +(keyword_definition (body (keyword_setting) @keyword)) +(keyword_definition (name) @function) + +(variable_definition (variable_name) @variable) + +(setting_statement) @keyword + +(extra_text) @comment +(section_header) @keyword + +(ellipses) @punctuation.delimiter +(comment) @comment diff --git a/tree-sitter/highlights/ron.scm b/tree-sitter/highlights/ron.scm new file mode 100644 index 0000000000..8692643056 --- /dev/null +++ b/tree-sitter/highlights/ron.scm @@ -0,0 +1,53 @@ +; Structs +;------------ + +(enum_variant) @constant +(struct_entry (identifier) @property) +(struct_entry (enum_variant (identifier) @constant)) +(struct_name (identifier)) @type + +(unit_struct) @type.builtin + + +; Literals +;------------ + +(string) @string +(boolean) @boolean +(integer) @number +(float) @float +(char) @character + + +; Comments +;------------ + +[ + (line_comment) + (block_comment) +] @comment @spell + + +; Punctuation +;------------ + +["{" "}"] @punctuation.bracket + +["(" ")"] @punctuation.bracket + +["[" "]"] @punctuation.bracket + +[ + "," + ":" +] @punctuation.delimiter + +[ + "-" +] @operator + +; Special +;------------ + +(escape_sequence) @string.escape +(ERROR) @error diff --git a/tree-sitter/highlights/rst.scm b/tree-sitter/highlights/rst.scm new file mode 100644 index 0000000000..7590871309 --- /dev/null +++ b/tree-sitter/highlights/rst.scm @@ -0,0 +1,171 @@ +;; Marks + +[ + ".." + "|" + "--" + "__" + ":" + "::" + "bullet" + "adornment" + (transition) +] @punctuation.special + +;; Resets for injection + +(doctest_block) @none + +;; Directives + +(directive + name: (type) @function) + +(directive + body: (body (arguments) @parameter)) + +((directive + name: (type) @include) + (#eq? @include "include")) + +((directive + name: (type) @function.builtin) + (#any-of? + @function.builtin + ; https://docutils.sourceforge.io/docs/ref/rst/directives.html + "attention" "caution" "danger" "error" "hint" "important" "note" "tip" "warning" "admonition" + "image" "figure" + "topic" "sidebar" "line-block" "parsed-literal" "code" "math" "rubric" "epigraph" "highlights" "pull-quote" "compound" "container" + "table" "csv-table" "list-table" + "contents" "sectnum" "section-numbering" "header" "footer" + "target-notes" + "meta" + "replace" "unicode" "date" + "raw" "class" "role" "default-role" "title" "restructuredtext-test-directive")) + +;; Blocks + +[ + (literal_block) + (line_block) +] @text.literal + +(block_quote + (attribution)? @text.emphasis) @text.literal + +(substitution_definition + name: (substitution) @constant) + +(footnote + name: (label) @constant) + +(citation + name: (label) @constant) + +(target + name: (name)? @constant + link: (_)? @text.literal) + +;; Lists + +; Definition lists +(list_item + (term) @text.strong + (classifier)? @text.emphasis) + +; Field lists +(field (field_name) @constant) + +;; Inline markup + +(emphasis) @text.emphasis + +(strong) @text.strong + +(standalone_hyperlink) @text.uri @nospell + +(role) @function + +((role) @function.builtin + (#any-of? + @function.builtin + ; https://docutils.sourceforge.io/docs/ref/rst/roles.html + ":emphasis:" + ":literal:" + ":code:" + ":math:" + ":pep-reference:" + ":PEP:" + ":rfc-reference:" + ":RFC:" + ":strong:" + ":subscript:" + ":sub:" + ":superscript:" + ":sup:" + ":title-reference:" + ":title:" + ":t:" + ":raw:")) + +[ + "interpreted_text" + (literal) +] @text.literal + +; Prefix role +((interpreted_text + (role) @_role + "interpreted_text" @text.emphasis) + (#eq? @_role ":emphasis:")) + +((interpreted_text + (role) @_role + "interpreted_text" @text.strong) + (#eq? @_role ":strong:")) + +((interpreted_text + (role) @_role + "interpreted_text" @none) + (#eq? @_role ":math:")) + +; Suffix role +((interpreted_text + "interpreted_text" @text.emphasis + (role) @_role) + (#eq? @_role ":emphasis:")) + +((interpreted_text + "interpreted_text" @text.strong + (role) @_role) + (#eq? @_role ":strong:")) + +((interpreted_text + "interpreted_text" @none + (role) @_role) + (#eq? @_role ":math:")) + +[ + (inline_target) + (substitution_reference) + (footnote_reference) + (citation_reference) + (reference) +] @text.reference @nospell + +;; Others + +(title) @text.title + +(comment) @comment @spell +(comment "..") @comment + +(directive + name: (type) @_directive + body: (body + (content) @spell + (#not-any-of? @_directive "code" "code-block" "sourcecode"))) + +(paragraph) @spell + +(ERROR) @error diff --git a/tree-sitter/highlights/ruby.scm b/tree-sitter/highlights/ruby.scm new file mode 100644 index 0000000000..de7f966886 --- /dev/null +++ b/tree-sitter/highlights/ruby.scm @@ -0,0 +1,261 @@ +; Variables + +(identifier) @variable +(global_variable) @variable.global + +; Keywords + +[ + "alias" + "begin" + "class" + "do" + "end" + "ensure" + "module" + "rescue" + "then" + ] @keyword + +[ + "return" + "yield" +] @keyword.return + +[ + "and" + "or" + "in" + "not" +] @keyword.operator + +[ + "def" + "undef" +] @keyword.function + +(method + "end" @keyword.function) + +[ + "case" + "else" + "elsif" + "if" + "unless" + "when" + "then" + ] @conditional + +(if + "end" @conditional) + +[ + "for" + "until" + "while" + "break" + "redo" + "retry" + "next" + ] @repeat + +(constant) @type + +((identifier) @type.qualifier + (#any-of? @type.qualifier "private" "protected" "public")) + +[ + "rescue" + "ensure" + ] @exception + +((identifier) @exception + (#any-of? @exception "fail" "raise")) + +; Function calls + +"defined?" @function + +(call + receiver: (constant)? @type + method: [ + (identifier) + (constant) + ] @function.call + ) + +(program + (call + (identifier) @include) + (#any-of? @include "require" "require_relative" "load")) + +; Function definitions + +(alias (identifier) @function) +(setter (identifier) @function) + +(method name: [ + (identifier) @function + (constant) @type + ]) + +(singleton_method name: [ + (identifier) @function + (constant) @type + ]) + +(class name: (constant) @type) +(module name: (constant) @type) +(superclass (constant) @type) + +; Identifiers +[ + (class_variable) + (instance_variable) + ] @label + +((identifier) @constant.builtin + (#vim-match? @constant.builtin "^__(callee|dir|id|method|send|ENCODING|FILE|LINE)__$")) + +((constant) @type + (#vim-match? @type "^[A-Z\\d_]+$")) + +[ + (self) + (super) + ] @variable.builtin + +(method_parameters (identifier) @parameter) +(lambda_parameters (identifier) @parameter) +(block_parameters (identifier) @parameter) +(splat_parameter (identifier) @parameter) +(hash_splat_parameter (identifier) @parameter) +(optional_parameter (identifier) @parameter) +(destructured_parameter (identifier) @parameter) +(block_parameter (identifier) @parameter) +(keyword_parameter (identifier) @parameter) + +; TODO: Re-enable this once it is supported +; ((identifier) @function +; (#is-not? local)) + +; Literals + +[ + (string) + (bare_string) + (subshell) + (heredoc_body) + ] @string + +[ + (heredoc_beginning) + (heredoc_end) + ] @constant + +[ + (bare_symbol) + (simple_symbol) + (delimited_symbol) + (hash_key_symbol) + ] @symbol + +(pair key: (hash_key_symbol) ":" @constant) +(regex) @string.regex +(escape_sequence) @string.escape +(integer) @number +(float) @float + +[ + (true) + (false) + ] @boolean + +(nil) @constant.builtin + +(comment) @comment @spell + +(program + (comment)+ @comment.documentation + (class)) + +(module + (comment)+ @comment.documentation + (body_statement (class))) + +(class + (comment)+ @comment.documentation + (body_statement (method))) + +(body_statement + (comment)+ @comment.documentation + (method)) + +(string_content) @spell + +; Operators + +[ + "!" + "=" + "==" + "===" + "<=>" + "=>" + "->" + ">>" + "<<" + ">" + "<" + ">=" + "<=" + "**" + "*" + "/" + "%" + "+" + "-" + "&" + "|" + "^" + "&&" + "||" + "||=" + "&&=" + "!=" + "%=" + "+=" + "-=" + "*=" + "/=" + "=~" + "!~" + "?" + ":" + ".." + "..." + ] @operator + +[ + "," + ";" + "." + ] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "%w(" + "%i(" + ] @punctuation.bracket + +(interpolation + "#{" @punctuation.special + "}" @punctuation.special) @none + +(ERROR) @error diff --git a/tree-sitter/highlights/rust.scm b/tree-sitter/highlights/rust.scm new file mode 100644 index 0000000000..82fb966f79 --- /dev/null +++ b/tree-sitter/highlights/rust.scm @@ -0,0 +1,384 @@ +; Forked from https://github.com/tree-sitter/tree-sitter-rust +; Copyright (c) 2017 Maxim Sokolov +; Licensed under the MIT license. + +; Identifier conventions + +(identifier) @variable + +((identifier) @type + (#lua-match? @type "^[A-Z]")) + +(const_item + name: (identifier) @constant) + +; Assume all-caps names are constants + +((identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z%d_]*$")) + +; Other identifiers + +(type_identifier) @type + +(primitive_type) @type.builtin + +(field_identifier) @field + +(shorthand_field_initializer (identifier) @field) + +(mod_item + name: (identifier) @namespace) + +(self) @variable.builtin + +(loop_label ["'" (identifier)] @label) + +; Function definitions + +(function_item (identifier) @function) + +(function_signature_item (identifier) @function) + +(parameter (identifier) @parameter) + +(closure_parameters (_) @parameter) + +; Function calls + +(call_expression + function: (identifier) @function.call) + +(call_expression + function: (scoped_identifier + (identifier) @function.call .)) + +(call_expression + function: (field_expression + field: (field_identifier) @function.call)) + +(generic_function + function: (identifier) @function.call) + +(generic_function + function: (scoped_identifier + name: (identifier) @function.call)) + +(generic_function + function: (field_expression + field: (field_identifier) @function.call)) + +; Assume other uppercase names are enum constructors + +((field_identifier) @constant + (#lua-match? @constant "^[A-Z]")) + +(enum_variant + name: (identifier) @constant) + +; Assume that uppercase names in paths are types + +(scoped_identifier + path: (identifier) @namespace) + +(scoped_identifier + (scoped_identifier + name: (identifier) @namespace)) + +(scoped_type_identifier + path: (identifier) @namespace) + +(scoped_type_identifier + path: (identifier) @type + (#lua-match? @type "^[A-Z]")) + +(scoped_type_identifier + (scoped_identifier + name: (identifier) @namespace)) + +((scoped_identifier + path: (identifier) @type) + (#lua-match? @type "^[A-Z]")) + +((scoped_identifier + name: (identifier) @type) + (#lua-match? @type "^[A-Z]")) + +((scoped_identifier + name: (identifier) @constant) + (#lua-match? @constant "^[A-Z][A-Z%d_]*$")) + +((scoped_identifier + path: (identifier) @type + name: (identifier) @constant) + (#lua-match? @type "^[A-Z]") + (#lua-match? @constant "^[A-Z]")) + +((scoped_type_identifier + path: (identifier) @type + name: (type_identifier) @constant) + (#lua-match? @type "^[A-Z]") + (#lua-match? @constant "^[A-Z]")) + +[ + (crate) + (super) +] @namespace + +(scoped_use_list + path: (identifier) @namespace) + +(scoped_use_list + path: (scoped_identifier + (identifier) @namespace)) + +(use_list (scoped_identifier (identifier) @namespace . (_))) + +(use_list (identifier) @type (#lua-match? @type "^[A-Z]")) + +(use_as_clause alias: (identifier) @type (#lua-match? @type "^[A-Z]")) + +; Correct enum constructors + +(call_expression + function: (scoped_identifier + "::" + name: (identifier) @constant) + (#lua-match? @constant "^[A-Z]")) + +; Assume uppercase names in a match arm are constants. + +((match_arm + pattern: (match_pattern (identifier) @constant)) + (#lua-match? @constant "^[A-Z]")) + +((match_arm + pattern: (match_pattern + (scoped_identifier + name: (identifier) @constant))) + (#lua-match? @constant "^[A-Z]")) + +((identifier) @constant.builtin + (#any-of? @constant.builtin "Some" "None" "Ok" "Err")) + +; Macro definitions + +"$" @function.macro + +(metavariable) @function.macro + +(macro_definition "macro_rules!" @function.macro) + +; Attribute macros + +(attribute_item (attribute (identifier) @function.macro)) + +(attribute (scoped_identifier (identifier) @function.macro .)) + +; Derive macros (assume all arguments are types) +; (attribute +; (identifier) @_name +; arguments: (attribute (attribute (identifier) @type)) +; (#eq? @_name "derive")) + +; Function-like macros +(macro_invocation + macro: (identifier) @function.macro) + +(macro_invocation + macro: (scoped_identifier + (identifier) @function.macro .)) + +; Literals + +[ + (line_comment) + (block_comment) +] @comment @spell + +((line_comment) @comment.documentation + (#lua-match? @comment.documentation "^///[^/]")) + +((line_comment) @comment.documentation + (#lua-match? @comment.documentation "^///$")) + +((line_comment) @comment.documentation + (#lua-match? @comment.documentation "^//!")) + +((block_comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +((block_comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][!]")) + +(boolean_literal) @boolean + +(integer_literal) @number + +(float_literal) @float + +[ + (raw_string_literal) + (string_literal) +] @string + +(escape_sequence) @string.escape + +(char_literal) @character + +; Keywords + +[ + "use" + "mod" +] @include + +(use_as_clause "as" @include) + +[ + "default" + "enum" + "impl" + "let" + "move" + "pub" + "struct" + "trait" + "type" + "union" + "unsafe" + "where" +] @keyword + +[ + "async" + "await" +] @keyword.coroutine + +[ + "ref" + (mutable_specifier) +] @type.qualifier + +[ + "const" + "static" + "dyn" + "extern" +] @storageclass + +(lifetime ["'" (identifier)] @storageclass.lifetime) + +"fn" @keyword.function + +[ + "return" + "yield" +] @keyword.return + +(type_cast_expression "as" @keyword.operator) + +(qualified_type "as" @keyword.operator) + +(use_list (self) @namespace) + +(scoped_use_list (self) @namespace) + +(scoped_identifier [(crate) (super) (self)] @namespace) + +(visibility_modifier [(crate) (super) (self)] @namespace) + +[ + "if" + "else" + "match" +] @conditional + +[ + "break" + "continue" + "in" + "loop" + "while" +] @repeat + +"for" @keyword +(for_expression "for" @repeat) + +; Operators + +[ + "!" + "!=" + "%" + "%=" + "&" + "&&" + "&=" + "*" + "*=" + "+" + "+=" + "-" + "-=" + ".." + "..=" + "/" + "/=" + "<" + "<<" + "<<=" + "<=" + "=" + "==" + ">" + ">=" + ">>" + ">>=" + "?" + "@" + "^" + "^=" + "|" + "|=" + "||" +] @operator + +; Punctuation + +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + +(closure_parameters "|" @punctuation.bracket) + +(type_arguments ["<" ">"] @punctuation.bracket) + +(type_parameters ["<" ">"] @punctuation.bracket) + +(bracketed_type ["<" ">"] @punctuation.bracket) + +(for_lifetimes ["<" ">"] @punctuation.bracket) + +["," "." ":" "::" ";" "->" "=>"] @punctuation.delimiter + +(attribute_item "#" @punctuation.special) + +(inner_attribute_item ["!" "#"] @punctuation.special) + +(macro_invocation "!" @function.macro) + +(empty_type "!" @type.builtin) + +(macro_invocation + macro: (identifier) @exception + "!" @exception + (#eq? @exception "panic")) + +(macro_invocation + macro: (identifier) @exception + "!" @exception + (#contains? @exception "assert")) + +(macro_invocation + macro: (identifier) @debug + "!" @debug + (#eq? @debug "dbg")) diff --git a/tree-sitter/highlights/scala.scm b/tree-sitter/highlights/scala.scm new file mode 100644 index 0000000000..11af6a80de --- /dev/null +++ b/tree-sitter/highlights/scala.scm @@ -0,0 +1,264 @@ +; CREDITS @stumash (stuart.mashaal@gmail.com) + +(class_definition + name: (identifier) @type) + +(enum_definition + name: (identifier) @type) + +(object_definition + name: (identifier) @type) + +(trait_definition + name: (identifier) @type) + +(full_enum_case + name: (identifier) @type) + +(simple_enum_case + name: (identifier) @type) + +;; variables + +(class_parameter + name: (identifier) @parameter) + +(self_type (identifier) @parameter) + +(interpolation (identifier) @none) +(interpolation (block) @none) + +;; types + +(type_definition + name: (type_identifier) @type.definition) + +(type_identifier) @type + +;; val/var definitions/declarations + +(val_definition + pattern: (identifier) @variable) + +(var_definition + pattern: (identifier) @variable) + +(val_declaration + name: (identifier) @variable) + +(var_declaration + name: (identifier) @variable) + +; method definition + +(function_declaration + name: (identifier) @method) + +(function_definition + name: (identifier) @method) + +; imports/exports + +(import_declaration + path: (identifier) @namespace) +((stable_identifier (identifier) @namespace)) + +((import_declaration + path: (identifier) @type) (#lua-match? @type "^[A-Z]")) +((stable_identifier (identifier) @type) (#lua-match? @type "^[A-Z]")) + +(export_declaration + path: (identifier) @namespace) +((stable_identifier (identifier) @namespace)) + +((export_declaration + path: (identifier) @type) (#lua-match? @type "^[A-Z]")) +((stable_identifier (identifier) @type) (#lua-match? @type "^[A-Z]")) + +((namespace_selectors (identifier) @type) (#lua-match? @type "^[A-Z]")) + +; method invocation + +(call_expression + function: (identifier) @function.call) + +(call_expression + function: (operator_identifier) @function.call) + +(call_expression + function: (field_expression + field: (identifier) @method.call)) + +((call_expression + function: (identifier) @constructor) + (#lua-match? @constructor "^[A-Z]")) + +(generic_function + function: (identifier) @function.call) + +(interpolated_string_expression + interpolator: (identifier) @function.call) + +; function definitions + +(function_definition + name: (identifier) @function) + +(parameter + name: (identifier) @parameter) + +(binding + name: (identifier) @parameter) + +; expressions + +(field_expression field: (identifier) @property) +(field_expression value: (identifier) @type + (#lua-match? @type "^[A-Z]")) + +(infix_expression operator: (identifier) @operator) +(infix_expression operator: (operator_identifier) @operator) +(infix_type operator: (operator_identifier) @operator) +(infix_type operator: (operator_identifier) @operator) + +; literals + +(boolean_literal) @boolean +(integer_literal) @number +(floating_point_literal) @float + +[ + (symbol_literal) + (string) + (character_literal) + (interpolated_string_expression) +] @string + +(interpolation "$" @punctuation.special) + +;; keywords + +(opaque_modifier) @type.qualifier +(infix_modifier) @keyword +(transparent_modifier) @type.qualifier +(open_modifier) @type.qualifier + +[ + "case" + "class" + "enum" + "extends" + "derives" + "finally" +;; `forSome` existential types not implemented yet +;; `macro` not implemented yet + "object" + "override" + "package" + "trait" + "type" + "val" + "var" + "with" + "given" + "using" + "end" + "implicit" + "extension" + "with" +] @keyword + +[ + "abstract" + "final" + "lazy" + "sealed" + "private" + "protected" +] @type.qualifier + +(inline_modifier) @storageclass + +(null_literal) @constant.builtin + +(wildcard) @parameter + +(annotation) @attribute + +;; special keywords + +"new" @keyword.operator + +[ + "else" + "if" + "match" + "then" +] @conditional + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "." + "," +] @punctuation.delimiter + +[ + "do" + "for" + "while" + "yield" +] @repeat + +"def" @keyword.function + +[ + "=>" + "<-" + "@" +] @operator + +["import" "export"] @include + +[ + "try" + "catch" + "throw" +] @exception + +"return" @keyword.return + +[ + (comment) + (block_comment) +] @comment @spell + +((block_comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +;; `case` is a conditional keyword in case_block + +(case_block + (case_clause ("case") @conditional)) + +(operator_identifier) @operator + +((identifier) @type (#lua-match? @type "^[A-Z]")) +((identifier) @variable.builtin + (#lua-match? @variable.builtin "^this$")) + +( + (identifier) @function.builtin + (#lua-match? @function.builtin "^super$") +) + +;; Scala CLI using directives +(using_directive_key) @parameter +(using_directive_value) @string diff --git a/tree-sitter/highlights/scfg.scm b/tree-sitter/highlights/scfg.scm new file mode 100644 index 0000000000..240d48a90e --- /dev/null +++ b/tree-sitter/highlights/scfg.scm @@ -0,0 +1,8 @@ +[ + "{" + "}" +] @punctuation.bracket + +(comment) @comment @spell +(directive_name) @type +(directive_params) @parameter diff --git a/tree-sitter/highlights/scheme.scm b/tree-sitter/highlights/scheme.scm new file mode 100644 index 0000000000..28fd1cd1b3 --- /dev/null +++ b/tree-sitter/highlights/scheme.scm @@ -0,0 +1,181 @@ +;; A highlight query can override the highlights queries before it. +;; So the order is important. +;; We should highlight general rules, then highlight special forms. + +(number) @number +(character) @character +(boolean) @boolean +(string) @string @spell +[(comment) + (block_comment)] @comment @spell + +;; highlight for datum comment +;; copied from ../clojure/highlights.scm +([(comment) (directive)] @comment + (#set! "priority" 105)) + +(escape_sequence) @string.escape + +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + +;; variables + +(symbol) @variable +((symbol) @variable.builtin + (#any-of? @variable.builtin "..." ".")) + +;; procedure + +(list + . + (symbol) @function) + +;; special forms + +(list + "[" + (symbol)+ @variable + "]") + +(list + . + (symbol) @_f + . + (list + (symbol) @variable) + (#any-of? @_f "lambda" "λ")) + +(list + . + (symbol) @_f + . + (list + (list + (symbol) @variable)) + (#any-of? @_f + "let" "let*" "let-syntax" "let-values" "let*-values" "letrec" "letrec*" "letrec-syntax")) + +;; operators + +((symbol) @operator + (#any-of? @operator + "+" "-" "*" "/" "=" "<=" ">=" "<" ">")) + +;; keyword + +((symbol) @keyword + (#any-of? @keyword + "define" "lambda" "λ" "begin" "do" "define-syntax" + "and" "or" + "if" "cond" "case" "when" "unless" "else" "=>" + "let" "let*" "let-syntax" "let-values" "let*-values" "letrec" "letrec*" "letrec-syntax" + "set!" + "syntax-rules" "identifier-syntax" + "quote" "unquote" "quote-splicing" "quasiquote" "unquote-splicing" + "delay" + "assert" + "library" "export" "import" "rename" "only" "except" "prefix")) + +((symbol) @conditional + (#any-of? @conditional "if" "cond" "case" "when" "unless")) + +;; quote + +(quote + "'" + (symbol)) @symbol + +(list + . + (symbol) @_f + (#eq? @_f "quote")) @symbol + +;; library + +(list + . + (symbol) @_lib + . + (symbol) @namespace + + (#eq? @_lib "library")) + +;; builtin procedures +;; procedures in R5RS and R6RS but not in R6RS-lib + +((symbol) @function.builtin + (#any-of? @function.builtin + ;; eq + "eqv?" "eq?" "equal?" + ;; number + "number?" "complex?" "real?" "rational?" "integer?" + "exact?" "inexact?" + "zero?" "positive?" "negative?" "odd?" "even?" "finite?" "infinite?" "nan?" + "max" "min" + "abs" "quotient" "remainder" "modulo" + "div" "div0" "mod" "mod0" "div-and-mod" "div0-and-mod0" + "gcd" "lcm" "numerator" "denominator" + "floor" "ceiling" "truncate" "round" + "rationalize" + "exp" "log" "sin" "cos" "tan" "asin" "acos" "atan" + "sqrt" "expt" + "exact-integer-sqrt" + "make-rectangular" "make-polar" "real-part" "imag-part" "magnitude" "angle" + "real-valued" "rational-valued?" "integer-valued?" + "exact" "inexact" "exact->inexact" "inexact->exact" + "number->string" "string->number" + ;; boolean + "boolean?" "not" "boolean=?" + ;; pair + "pair?" "cons" + "car" "cdr" + "caar" "cadr" "cdar" "cddr" + "caaar" "caadr" "cadar" "caddr" "cdaar" "cdadr" "cddar" "cdddr" + "caaaar" "caaadr" "caadar" "caaddr" "cadaar" "cadadr" "caddar" "cadddr" + "cdaaar" "cdaadr" "cdadar" "cdaddr" "cddaar" "cddadr" "cdddar" "cddddr" + "set-car!" "set-cdr!" + ;; list + "null?" "list?" + "list" "length" "append" "reverse" "list-tail" "list-ref" + "map" "for-each" + "memq" "memv" "member" "assq" "assv" "assoc" + ;; symbol + "symbol?" "symbol->string" "string->symbol" "symbol=?" + ;; char + "char?" "char=?" "char?" "char<=?" "char>=?" + "char-ci=?" "char-ci?" "char-ci<=?" "char-ci>=?" + "char-alphabetic?" "char-numeric?" "char-whitespace?" "char-upper-case?" "char-lower-case?" + "char->integer" "integer->char" + "char-upcase" "char-downcase" + ;; string + "string?" "make-string" "string" "string-length" "string-ref" "string-set!" + "string=?" "string-ci=?" "string?" "string<=?" "string>=?" + "string-ci?" "string-ci<=?" "string-ci>=?" + "substring" "string-append" "string->list" "list->string" + "string-for-each" + "string-copy" "string-fill!" + "string-upcase" "string-downcase" + ;; vector + "vector?" "make-vector" "vector" "vector-length" "vector-ref" "vector-set!" + "vector->list" "list->vector" "vector-fill!" "vector-map" "vector-for-each" + ;; bytevector + "bytevector?" "native-endianness" + "make-bytevector" "bytevector-length" "bytevector=?" "bytevector-fill!" + "bytevector-copy!" "bytevector-copy" + ;; error + "error" "assertion-violation" + ;; control + "procedure?" "apply" "force" + "call-with-current-continuation" "call/cc" + "values" "call-with-values" "dynamic-wind" + "eval" "scheme-report-environment" "null-environment" "interaction-environment" + ;; IO + "call-with-input-file" "call-with-output-file" "input-port?" "output-port?" + "current-input-port" "current-output-port" "with-input-from-file" "with-output-to-file" + "open-input-file" "open-output-file" "close-input-port" "close-output-port" + ;; input + "read" "read-char" "peek-char" "eof-object?" "char-ready?" + ;; output + "write" "display" "newline" "write-char" + ;; system + "load" "transcript-on" "transcript-off")) diff --git a/tree-sitter/highlights/scss.scm b/tree-sitter/highlights/scss.scm new file mode 100644 index 0000000000..2e6de820f8 --- /dev/null +++ b/tree-sitter/highlights/scss.scm @@ -0,0 +1,65 @@ +; inherits: css + +[ + "@at-root" + "@debug" + "@error" + "@extend" + "@forward" + "@mixin" + "@use" + "@warn" +] @keyword + +"@function" @keyword.function + +"@return" @keyword.return + +"@include" @include + +[ + "@while" + "@each" + "@for" + "from" + "through" + "in" +] @repeat + +(single_line_comment) @comment +(function_name) @function + + +[ + ">=" + "<=" +] @operator + + +(mixin_statement (name) @function) +(mixin_statement (parameters (parameter) @parameter)) + +(function_statement (name) @function) +(function_statement (parameters (parameter) @parameter)) + +(plain_value) @string +(keyword_query) @function +(identifier) @variable +(variable_name) @variable + +(each_statement (key) @parameter) +(each_statement (value) @parameter) +(each_statement (variable_value) @parameter) + +(for_statement (variable) @parameter) +(for_statement (_ (variable_value) @parameter)) + +(argument) @parameter +(arguments (variable_value) @parameter) + +[ + "[" + "]" +] @punctuation.bracket + +(include_statement (identifier) @function) diff --git a/tree-sitter/highlights/slint.scm b/tree-sitter/highlights/slint.scm new file mode 100644 index 0000000000..8928bc9a52 --- /dev/null +++ b/tree-sitter/highlights/slint.scm @@ -0,0 +1,154 @@ +(identifier) @variable +(type_identifier) @type +(comment) @comment +(int_literal) @number +(float_literal) @float +(string_literal) @string +(function_identifier) @function +[ +(image_macro) +(children_macro) +(radial_grad_macro) +(linear_grad_macro) +] @function.macro +(call_expression + function: (identifier) @function.call) +(call_expression + function: (field_expression + field: (identifier) @function.call)) +(vis) @include +(units) @type +(array_literal + (identifier) @type) +(transition_statement state: (identifier) @field) +(state_expression state: (identifier) @field) +(struct_block_definition + (identifier) @field) + +; (state_identifier) @field + +[ +"in" +"for" +] @repeat + +"@" @keyword + +[ +"import" +"from" +] @include + +[ +"if" +"else" +] @conditional + +[ +"root" +"parent" +"duration" +"easing" +] @variable.builtin + +[ +"true" +"false" +] @boolean + + +[ +"struct" +"property" +"callback" +"in" +"animate" +"states" +"when" +"out" +"transitions" +"global" +] @keyword + +[ +"black" +"transparent" +"blue" +"ease" +"ease_in" +"ease-in" +"ease_in_out" +"ease-in-out" +"ease_out" +"ease-out" +"end" +"green" +"red" +"red" +"start" +"yellow" +"white" +"gray" +] @constant.builtin + + +; Punctuation +[ +"," +"." +";" +":" +] @punctuation.delimiter + +; Brackets +[ +"(" +")" +"[" +"]" +"{" +"}" +] @punctuation.bracket + +(define_property ["<" ">"] @punctuation.bracket) + +[ +"angle" +"bool" +"brush" +"color" +"float" +"image" +"int" +"length" +"percent" +"physical-length" +"physical_length" +"string" +] @type.builtin + +[ + ":=" + "<=>" + "!" + "-" + "+" + "*" + "/" + "&&" + "||" + ">" + "<" + ">=" + "<=" + "=" + ":" + "+=" + "-=" + "*=" + "/=" + "?" + "=>" + ] @operator + +(ternary_expression [":" "?"] @conditional.ternary) diff --git a/tree-sitter/highlights/smali.scm b/tree-sitter/highlights/smali.scm new file mode 100644 index 0000000000..2e2b84b8b2 --- /dev/null +++ b/tree-sitter/highlights/smali.scm @@ -0,0 +1,218 @@ +; Types + +(class_identifier + (identifier) @type) + +(primitive_type) @type.builtin + +((class_identifier + . (identifier) @_first @type.builtin + (identifier) @type.builtin) + (#any-of? @_first "android" "dalvik" "java" "kotlinx")) + +((class_identifier + . (identifier) @_first @type.builtin + . (identifier) @_second @type.builtin + (identifier) @type.builtin) + (#eq? @_first "com") + (#any-of? @_second "android" "google")) + +; Methods + +(method_definition + (method_signature (method_identifier) @method)) + +(expression + (opcode) @_invoke + (body + (full_method_signature + (method_signature (method_identifier) @method.call))) + (#lua-match? @_invoke "^invoke")) + +(method_handle + (full_method_signature + (method_signature (method_identifier) @method.call))) + +(custom_invoke + . (identifier) @method.call + (method_signature (method_identifier) @method.call)) + +(annotation_value + (body + (method_signature (method_identifier) @method.call))) + +(annotation_value + (body + (full_method_signature + (method_signature (method_identifier) @method.call)))) + +(field_definition + (body + (method_signature (method_identifier) @method.call))) + +(field_definition + (body + (full_method_signature + (method_signature (method_identifier) @method.call)))) + +((method_identifier) @constructor + (#any-of? @constructor "" "")) + +"constructor" @constructor + +; Fields + +[ + (field_identifier) + (annotation_key) +] @field + +((field_identifier) @constant + (#lua-match? @constant "^[%u_]*$")) + +; Variables + +(variable) @variable.builtin + +(local_directive + (identifier) @variable) + +; Parameters + +(parameter) @parameter.builtin +(param_identifier) @parameter + +; Labels + +[ + (label) + (jmp_label) +] @label + +; Operators + +(opcode) @keyword.operator + +((opcode) @keyword.return + (#lua-match? @keyword.return "^return")) + +((opcode) @conditional + (#lua-match? @conditional "^if")) + +((opcode) @conditional + (#lua-match? @conditional "^cmp")) + +((opcode) @exception + (#lua-match? @exception "^throw")) + +((opcode) @comment + (#eq? @comment "nop")) ; haha, anyone get it? ;) + +[ + "=" + ".." +] @operator + +; Keywords + +[ + ".class" + ".super" + ".implements" + ".field" + ".end field" + ".annotation" + ".end annotation" + ".subannotation" + ".end subannotation" + ".param" + ".end param" + ".parameter" + ".end parameter" + ".line" + ".locals" + ".local" + ".end local" + ".restart local" + ".registers" + ".packed-switch" + ".end packed-switch" + ".sparse-switch" + ".end sparse-switch" + ".array-data" + ".end array-data" + ".enum" + (prologue_directive) + (epilogue_directive) +] @keyword + +[ + ".source" +] @include + +[ + ".method" + ".end method" +] @keyword.function + +[ + ".catch" + ".catchall" +] @exception + +; Literals + +(string) @string +(source_directive (string "\"" _ @text.uri "\"")) +(escape_sequence) @string.escape + +(character) @character + +"L" @character.special + +(number) @number + +[ + (float) + (NaN) + (Infinity) +] @float + +(boolean) @boolean + +(null) @constant.builtin + +; Misc + +(annotation_visibility) @storageclass + +(access_modifier) @type.qualifier + +(array_type + "[" @punctuation.special) + +["{" "}"] @punctuation.bracket + +["(" ")"] @punctuation.bracket + +[ + "->" + "," + ":" + ";" + "@" + "/" +] @punctuation.delimiter + +(line_directive (number) @text.underline @text.literal) + +; Comments + +(comment) @comment @spell + +(class_definition + (comment) @comment.documentation) + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/smithy.scm b/tree-sitter/highlights/smithy.scm new file mode 100644 index 0000000000..2b709c2a2d --- /dev/null +++ b/tree-sitter/highlights/smithy.scm @@ -0,0 +1,113 @@ +; Preproc + +(control_key) @preproc + +; Namespace + +(namespace) @namespace + +; Includes + +[ + "use" +] @include + +; Builtins + +(primitive) @type.builtin +[ + "enum" + "intEnum" + "list" + "map" + "set" +] @type.builtin + +; Fields (Members) + +; (field) @field + +(key_identifier) @field +(shape_member + (field) @field) +(operation_field) @field +(operation_error_field) @field + +; Constants + +(enum_member + (enum_field) @constant) + +; Types + +(identifier) @type +(structure_resource + (shape_id) @type) + +; Attributes + +(mixins + (shape_id) @attribute) +(trait_statement + (shape_id (#set! "priority" 105)) @attribute) + +; Operators + +[ + "@" + "-" + "=" + ":=" +] @operator + +; Keywords + +[ + "namespace" + "service" + "structure" + "operation" + "union" + "resource" + "metadata" + "apply" + "for" + "with" +] @keyword + +; Literals + +(string) @string +(escape_sequence) @string.escape + +(number) @number + +(float) @float + +(boolean) @boolean + +(null) @constant.builtin + +; Misc + +[ + "$" + "#" +] @punctuation.special + +["{" "}"] @punctuation.bracket + +["(" ")"] @punctuation.bracket + +["[" "]"] @punctuation.bracket + +[ + ":" + "." +] @punctuation.delimiter + +; Comments + +(comment) @comment @spell + +(documentation_comment) @comment.documentation @spell diff --git a/tree-sitter/highlights/solidity.scm b/tree-sitter/highlights/solidity.scm new file mode 100644 index 0000000000..dcdd12f796 --- /dev/null +++ b/tree-sitter/highlights/solidity.scm @@ -0,0 +1,263 @@ +; Pragma + +[ + "pragma" + "solidity" +] @preproc + +(solidity_pragma_token + "||" @symbol) +(solidity_pragma_token + "-" @symbol) + +(solidity_version_comparison_operator) @operator + +(solidity_version) @text.underline @string.special + +; Literals + +[ + (string) + (yul_string_literal) +] @string + +(hex_string_literal + "hex" @symbol + (_) @string) + +(unicode_string_literal + "unicode" @symbol + (_) @string) + +[ + (number_literal) + (yul_decimal_number) + (yul_hex_number) +] @number + +(yul_boolean) @boolean + +; Variables + +[ + (identifier) + (yul_identifier) +] @variable + +; Types + +(type_name (identifier) @type) +(type_name (user_defined_type (identifier) @type)) +(type_name "mapping" @function.builtin) + +[ + (primitive_type) + (number_unit) +] @type.builtin + +(contract_declaration name: (identifier) @type) +(struct_declaration name: (identifier) @type) +(struct_member name: (identifier) @field) +(enum_declaration name: (identifier) @type) +(emit_statement . (identifier) @type) +; Handles ContractA, ContractB in function foo() override(ContractA, contractB) {} +(override_specifier (user_defined_type) @type) + +; Functions and parameters + +(function_definition + name: (identifier) @function) +(modifier_definition + name: (identifier) @function) +(yul_evm_builtin) @function.builtin + +; Use constructor coloring for special functions +(constructor_definition "constructor" @constructor) + +(modifier_invocation (identifier) @function) + +; Handles expressions like structVariable.g(); +(call_expression . (member_expression (identifier) @method.call)) + +; Handles expressions like g(); +(call_expression . (identifier) @function.call) + +; Function parameters +(event_paramater name: (identifier) @parameter) +(parameter name: (identifier) @parameter) + +; Yul functions +(yul_function_call function: (yul_identifier) @function.call) + +; Yul function parameters +(yul_function_definition . (yul_identifier) @function (yul_identifier) @parameter) + +(meta_type_expression "type" @keyword) + +(member_expression property: (identifier) @field) +(call_struct_argument name: (identifier) @field) +(struct_field_assignment name: (identifier) @field) +(enum_value) @constant + +; Keywords + +[ + "contract" + "interface" + "library" + "is" + "struct" + "enum" + "event" + "assembly" + "emit" + "override" + "modifier" + "var" + "let" + "emit" + "fallback" + "receive" + (virtual) +] @keyword + +; FIXME: update grammar +; (block_statement "unchecked" @keyword) + +(event_paramater "indexed" @keyword) + +[ + "public" + "internal" + "private" + "external" + "pure" + "view" + "payable" + (immutable) +] @type.qualifier + +[ + "memory" + "storage" + "calldata" + "constant" +] @storageclass + +[ + "for" + "while" + "do" + "break" + "continue" +] @repeat + +[ + "if" + "else" + "switch" + "case" + "default" +] @conditional + +(ternary_expression + "?" @conditional.ternary + ":" @conditional.ternary) + +[ + "try" + "catch" + "revert" +] @exception + +[ + "return" + "returns" + (yul_leave) +] @keyword.return + +"function" @keyword.function + +[ + "import" + "using" +] @include +(import_directive "as" @include) +(import_directive "from" @include) +((import_directive source: (string) @text.underline) + (#offset! @text.underline 0 1 0 -1)) + +; Punctuation + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +[ + "." + "," + ":" + ; FIXME: update grammar + ; (semicolon) + "->" + "=>" +] @punctuation.delimiter + +; Operators + +[ + "&&" + "||" + ">>" + ">>>" + "<<" + "&" + "^" + "|" + "+" + "-" + "*" + "/" + "%" + "**" + "=" + "<" + "<=" + "==" + "!=" + "!==" + ">=" + ">" + "!" + "~" + "-" + "+" + "++" + "--" + ":=" +] @operator + +[ + "delete" + "new" +] @keyword.operator + +(import_directive "*" @character.special) + +; Comments + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^///[^/]")) +((comment) @comment.documentation + (#lua-match? @comment.documentation "^///$")) + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/sparql.scm b/tree-sitter/highlights/sparql.scm new file mode 100644 index 0000000000..8ba8fef535 --- /dev/null +++ b/tree-sitter/highlights/sparql.scm @@ -0,0 +1,211 @@ +[ + (path_mod) + "||" + "&&" + "=" + "<" + ">" + "<=" + ">=" + "+" + "-" + "*" + "/" + "!" + "|" + "^" +] @operator + +[ + "_:" + (namespace) +] @namespace + +[ + "UNDEF" + "a" +] @variable.builtin + + +[ + "ADD" + "ALL" + "AS" + "ASC" + "ASK" + "BIND" + "BY" + "CLEAR" + "CONSTRUCT" + "COPY" + "CREATE" + "DEFAULT" + "DELETE" + "DELETE DATA" + "DELETE WHERE" + "DESC" + "DESCRIBE" + "DISTINCT" + "DROP" + "EXISTS" + "FILTER" + "FROM" + "GRAPH" + "GROUP" + "HAVING" + "INSERT" + "INSERT DATA" + "INTO" + "LIMIT" + "LOAD" + "MINUS" + "MOVE" + "NAMED" + "NOT" + "OFFSET" + "OPTIONAL" + "ORDER" + "PREFIX" + "REDUCED" + "SELECT" + "SERVICE" + "SILENT" + "UNION" + "USING" + "VALUES" + "WHERE" + "WITH" +] @keyword + +(string) @string +(echar) @string.escape + +(integer) @number +[ + (decimal) + (double) +] @float +(boolean_literal) @boolean + +[ + "BASE" + "PREFIX" +] @keyword + +[ + "ABS" + "AVG" + "BNODE" + "BOUND" + "CEIL" + "CONCAT" + "COALESCE" + "CONTAINS" + "DATATYPE" + "DAY" + "ENCODE_FOR_URI" + "FLOOR" + "HOURS" + "IF" + "IRI" + "LANG" + "LANGMATCHES" + "LCASE" + "MD5" + "MINUTES" + "MONTH" + "NOW" + "RAND" + "REGEX" + "ROUND" + "SECONDS" + "SHA1" + "SHA256" + "SHA384" + "SHA512" + "STR" + "SUM" + "MAX" + "MIN" + "SAMPLE" + "GROUP_CONCAT" + "SEPARATOR" + "COUNT" + "STRAFTER" + "STRBEFORE" + "STRDT" + "STRENDS" + "STRLANG" + "STRLEN" + "STRSTARTS" + "STRUUID" + "TIMEZONE" + "TZ" + "UCASE" + "URI" + "UUID" + "YEAR" + "isBLANK" + "isIRI" + "isLITERAL" + "isNUMERIC" + "isURI" + "sameTerm" +] @function.builtin + +[ + "." + "," + ";" +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" + (nil) + (anon) +] @punctuation.bracket + +[ + "IN" + ("NOT" "IN") +] @keyword.operator + + +(comment) @comment + + +; Could this be summarized? +(select_clause + [ + bound_variable: (var) + "*" + ] @parameter) +(bind bound_variable: (var) @parameter) +(data_block bound_variable: (var) @parameter) +(group_condition bound_variable: (var) @parameter) + +(iri_reference ["<" ">"] @namespace) + +(lang_tag) @type +(rdf_literal + "^^" @type + datatype: (_ ["<" ">" (namespace)] @type) @type) + +(function_call identifier: (_) @function) + +(function_call identifier: (iri_reference ["<" ">"] @function)) +(function_call identifier: (prefixed_name (namespace) @function)) +(base_declaration (iri_reference ["<" ">"] @variable)) +(prefix_declaration (iri_reference ["<" ">"] @variable)) + +[ + (var) + (blank_node_label) + (iri_reference) + (prefixed_name) +] @variable diff --git a/tree-sitter/highlights/sql.scm b/tree-sitter/highlights/sql.scm new file mode 100644 index 0000000000..d45e9a5a6f --- /dev/null +++ b/tree-sitter/highlights/sql.scm @@ -0,0 +1,355 @@ +[ + (keyword_gist) + (keyword_btree) + (keyword_hash) + (keyword_spgist) + (keyword_gin) + (keyword_brin) + (keyword_array) +] @function.call + +(object_reference + name: (identifier) @type) + +(invocation + (object_reference + name: (identifier) @function.call + parameter: [(field)]? @parameter)) + +(relation + alias: (identifier) @variable) + +(field + name: (identifier) @field) + +(term + alias: (identifier) @variable) + +((term + value: (cast + name: (keyword_cast) @function.call + parameter: [(literal)]?))) + +(literal) @string +(comment) @comment @spell +(marginalia) @comment + +((literal) @number + (#lua-match? @number "^%d+$")) + +((literal) @float +(#lua-match? @float "^[-]?%d*\.%d*$")) + +(parameter) @parameter + +[ + (keyword_true) + (keyword_false) +] @boolean + +[ + (keyword_asc) + (keyword_desc) + (keyword_terminated) + (keyword_escaped) + (keyword_unsigned) + (keyword_nulls) + (keyword_last) + (keyword_delimited) + (keyword_replication) + (keyword_auto_increment) + (keyword_default) + (keyword_collate) + (keyword_concurrently) + (keyword_engine) + (keyword_always) + (keyword_generated) + (keyword_preceding) + (keyword_following) + (keyword_first) + (keyword_current_timestamp) + (keyword_immutable) + (keyword_atomic) + (keyword_parallel) + (keyword_leakproof) + (keyword_safe) + (keyword_cost) + (keyword_strict) + (keyword_matched) +] @attribute + +[ + (keyword_materialized) + (keyword_recursive) + (keyword_temp) + (keyword_temporary) + (keyword_unlogged) + (keyword_external) + (keyword_parquet) + (keyword_csv) + (keyword_rcfile) + (keyword_textfile) + (keyword_orc) + (keyword_avro) + (keyword_jsonfile) + (keyword_sequencefile) + (keyword_volatile) +] @storageclass + +[ + (keyword_case) + (keyword_when) + (keyword_then) + (keyword_else) +] @conditional + +[ + (keyword_select) + (keyword_from) + (keyword_where) + (keyword_index) + (keyword_join) + (keyword_primary) + (keyword_delete) + (keyword_create) + (keyword_insert) + (keyword_merge) + (keyword_distinct) + (keyword_replace) + (keyword_update) + (keyword_into) + (keyword_overwrite) + (keyword_values) + (keyword_set) + (keyword_left) + (keyword_right) + (keyword_outer) + (keyword_inner) + (keyword_full) + (keyword_order) + (keyword_partition) + (keyword_group) + (keyword_with) + (keyword_as) + (keyword_having) + (keyword_limit) + (keyword_offset) + (keyword_table) + (keyword_tables) + (keyword_key) + (keyword_references) + (keyword_foreign) + (keyword_constraint) + (keyword_force) + (keyword_use) + (keyword_for) + (keyword_if) + (keyword_exists) + (keyword_max) + (keyword_min) + (keyword_avg) + (keyword_column) + (keyword_columns) + (keyword_cross) + (keyword_lateral) + (keyword_alter) + (keyword_drop) + (keyword_add) + (keyword_view) + (keyword_end) + (keyword_is) + (keyword_using) + (keyword_between) + (keyword_window) + (keyword_no) + (keyword_data) + (keyword_type) + (keyword_value) + (keyword_attribute) + (keyword_rename) + (keyword_to) + (keyword_schema) + (keyword_owner) + (keyword_union) + (keyword_all) + (keyword_any) + (keyword_some) + (keyword_except) + (keyword_intersect) + (keyword_returning) + (keyword_begin) + (keyword_commit) + (keyword_rollback) + (keyword_transaction) + (keyword_only) + (keyword_like) + (keyword_similar) + (keyword_over) + (keyword_change) + (keyword_modify) + (keyword_after) + (keyword_before) + (keyword_range) + (keyword_rows) + (keyword_groups) + (keyword_exclude) + (keyword_current) + (keyword_ties) + (keyword_others) + (keyword_preserve) + (keyword_zerofill) + (keyword_format) + (keyword_fields) + (keyword_row) + (keyword_sort) + (keyword_compute) + (keyword_comment) + (keyword_location) + (keyword_cached) + (keyword_uncached) + (keyword_lines) + (keyword_stored) + (keyword_partitioned) + (keyword_analyze) + (keyword_rewrite) + (keyword_optimize) + (keyword_vacuum) + (keyword_cache) + (keyword_language) + (keyword_sql) + (keyword_called) + (keyword_conflict) + (keyword_declare) + (keyword_filter) + (keyword_function) + (keyword_input) + (keyword_name) + (keyword_oid) + (keyword_options) + (keyword_plpgsql) + (keyword_precision) + (keyword_regclass) + (keyword_regnamespace) + (keyword_regproc) + (keyword_regtype) + (keyword_restricted) + (keyword_return) + (keyword_returns) + (keyword_separator) + (keyword_setof) + (keyword_stable) + (keyword_support) + (keyword_tblproperties) + (keyword_trigger) + (keyword_unsafe) +] @keyword + +[ + (keyword_restrict) + (keyword_unbounded) + (keyword_unique) + (keyword_cascade) + (keyword_delayed) + (keyword_high_priority) + (keyword_low_priority) + (keyword_ignore) + (keyword_nothing) + (keyword_check) + (keyword_option) + (keyword_local) + (keyword_cascaded) + (keyword_wait) + (keyword_nowait) + (keyword_metadata) + (keyword_incremental) + (keyword_bin_pack) + (keyword_noscan) + (keyword_stats) + (keyword_statistics) +] @type.qualifier + +[ + (keyword_int) + (keyword_null) + (keyword_boolean) + (keyword_binary) + (keyword_bit) + (keyword_inet) + (keyword_character) + (keyword_smallserial) + (keyword_serial) + (keyword_bigserial) + (keyword_smallint) + (keyword_mediumint) + (keyword_bigint) + (keyword_tinyint) + (keyword_decimal) + (keyword_float) + (keyword_double) + (keyword_numeric) + (keyword_real) + (double) + (keyword_money) + (keyword_char) + (keyword_varchar) + (keyword_varying) + (keyword_text) + (keyword_string) + (keyword_uuid) + (keyword_json) + (keyword_jsonb) + (keyword_xml) + (keyword_bytea) + (keyword_enum) + (keyword_date) + (keyword_datetime) + (keyword_timestamp) + (keyword_timestamptz) + (keyword_geometry) + (keyword_geography) + (keyword_box2d) + (keyword_box3d) + (keyword_interval) +] @type.builtin + +[ + (keyword_in) + (keyword_and) + (keyword_or) + (keyword_not) + (keyword_by) + (keyword_on) + (keyword_do) +] @keyword.operator + +[ + "+" + "-" + "*" + "/" + "%" + "^" + ":=" + "=" + "<" + "<=" + "!=" + ">=" + ">" + "<>" + "->" + "->>" + "#>" + "#>>" +] @operator + +[ + "(" + ")" +] @punctuation.bracket + +[ + ";" + "," + "." +] @punctuation.delimiter diff --git a/tree-sitter/highlights/squirrel.scm b/tree-sitter/highlights/squirrel.scm new file mode 100644 index 0000000000..593f0ffb7d --- /dev/null +++ b/tree-sitter/highlights/squirrel.scm @@ -0,0 +1,307 @@ +; Keywords + +[ + "class" + "clone" + "delete" + "enum" + "extends" + "rawcall" + "resume" + "var" +] @keyword + +[ + "function" +] @keyword.function + +[ + "in" + "instanceof" + "typeof" +] @keyword.operator + +[ + "return" + "yield" +] @keyword.return + +((global_variable + "::" + (_) @keyword.coroutine) + (#any-of? @keyword.coroutine "suspend" "newthread")) + +; Conditionals + +[ + "if" + "else" + "switch" + "case" + "default" + "break" +] @conditional + +; Repeats + +[ + "for" + "foreach" + "do" + "while" + "continue" +] @repeat + +; Exceptions + +[ + "try" + "catch" + "throw" +] @exception + +; Storageclasses + +[ + "local" +] @storageclass + +; Qualifiers + +[ + "static" + "const" +] @type.qualifier + +; Variables + +(identifier) @variable + +(local_declaration + (identifier) @variable.local + . "=") + + +(global_variable) @variable.global + +((identifier) @variable.builtin + (#any-of? @variable.builtin "base" "this" "vargv")) + +; Parameters + +(parameter + . (identifier) @parameter) + +; Properties (Slots) + +(deref_expression + "." + . (identifier) @property) + +(member_declaration + (identifier) @property + . "=") + +((table_slot + . (identifier) @property + . ["=" ":"]) + (#set! "priority" 105)) + +; Types + +((identifier) @type + (#lua-match? @type "^[A-Z]")) + +(class_declaration + (identifier) @type + "extends"? . (identifier)? @type) + +(enum_declaration + (identifier) @type) + +; Attributes + +(attribute_declaration + left: (identifier) @attribute) + +; Functions & Methods + +(member_declaration + (function_declaration + "::"? (_) @method . "(" (_)? ")")) + +((function_declaration + "::"? (_) @function . "(" (_)? ")") + (#not-has-ancestor? @function member_declaration)) + +(call_expression + function: (identifier) @function.call) + +(call_expression + function: (deref_expression + "." . (identifier) @function.call)) + +(call_expression + (global_variable + "::" + (_) @function.call)) + +(_ + (identifier) @function + "=" + (lambda_expression + "@" @symbol)) + +(call_expression + [ + function: (identifier) @function.builtin + function: (global_variable "::" (_) @function.builtin) + function: (deref_expression "." (_) @function.builtin) + ] + (#any-of? @function.builtin + ; General Methods + "assert" "array" "callee" "collectgarbage" "compilestring" + "enabledebughook" "enabledebuginfo" "error" "getconsttable" + "getroottable" "print" "resurrectunreachable" "setconsttable" + "setdebughook" "seterrorhandler" "setroottable" "type" + + ; Hidden Methods + "_charsize_" "_intsize_" "_floatsize_" "_version_" "_versionnumber_" + + ; Number Methods + "tofloat" "tostring" "tointeger" "tochar" + + ; String Methods + "len" "slice" "find" "tolower" "toupper" + + ; Table Methods + "rawget" "rawset" "rawdelete" "rawin" "clear" + "setdelegate" "getdelegate" "filter" "keys" "values" + + ; Array Methods + "append" "push" "extend" "pop" "top" "insert" "remove" "resize" "sort" + "reverse" "map" "apply" "reduce" + + ; Function Methods + "call" "pcall" "acall" "pacall" "setroot" "getroot" "bindenv" "getinfos" + + ; Class Methods + "instance" "getattributes" "setattributes" "newmember" "rawnewmember" + + ; Class Instance Methods + "getclass" + + ; Generator Methods + "getstatus" + + ; Thread Methods + "call" "wakeup" "wakeupthrow" "getstackinfos" + + ; Weak Reference Methods + "ref" "weakref" +)) + +(member_declaration + "constructor" @constructor) + +; Constants + +(const_declaration + "const" + . (identifier) @constant) + +(enum_declaration + "{" + . (identifier) @constant) + +((identifier) @constant + (#lua-match? @constant "^_*[A-Z][A-Z%d_]*$")) + +; Operators + +[ + "+" + "-" + "*" + "/" + "%" + "||" + "&&" + "|" + "^" + "&" + "==" + "!=" + "<=>" + ">" + ">=" + "<=" + "<" + "<<" + ">>" + ">>>" + "=" + "<-" + "+=" + "-=" + "*=" + "/=" + "%=" + "~" + "!" + "++" + "--" +] @operator + +; Punctuation + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +[ "" ] @punctuation.bracket + +[ + "." + "," + ";" + ":" +] @punctuation.delimiter + +[ + "::" + "..." +] @punctuation.special + +; Ternaries + +(ternary_expression + "?" @conditional.ternary + ":" @conditional.ternary) + +; Literals + +(string) @string + +(verbatim_string) @string.special + +(char) @character + +(escape_sequence) @string.escape + +(integer) @number + +(float) @float + +(bool) @boolean + +(null) @constant.builtin + +; Comments + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) diff --git a/tree-sitter/highlights/starlark.scm b/tree-sitter/highlights/starlark.scm new file mode 100644 index 0000000000..cfa920d1fd --- /dev/null +++ b/tree-sitter/highlights/starlark.scm @@ -0,0 +1,300 @@ +;; From tree-sitter-python licensed under MIT License +; Copyright (c) 2016 Max Brunsfeld + +; Variables +(identifier) @variable + +; Reset highlighting in f-string interpolations +(interpolation) @none + +;; Identifier naming conventions +((identifier) @type + (#lua-match? @type "^[A-Z].*[a-z]")) +((identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) + +((identifier) @constant.builtin + (#lua-match? @constant.builtin "^__[a-zA-Z0-9_]*__$")) + +((identifier) @constant.builtin + (#any-of? @constant.builtin + ;; https://docs.python.org/3/library/constants.html + "NotImplemented" + "Ellipsis" + "quit" + "exit" + "copyright" + "credits" + "license")) + +((attribute + attribute: (identifier) @field) + (#lua-match? @field "^[%l_].*$")) + +((assignment + left: (identifier) @type.definition + (type (identifier) @_annotation)) + (#eq? @_annotation "TypeAlias")) + +((assignment + left: (identifier) @type.definition + right: (call + function: (identifier) @_func)) + (#any-of? @_func "TypeVar" "NewType")) + +;; Decorators +((decorator "@" @attribute) + (#set! "priority" 101)) + +(decorator + (identifier) @attribute) +(decorator + (attribute + attribute: (identifier) @attribute)) +(decorator + (call (identifier) @attribute)) +(decorator + (call (attribute + attribute: (identifier) @attribute))) + +((decorator + (identifier) @attribute.builtin) + (#any-of? @attribute.builtin "classmethod" "property")) + +;; Builtin functions +((call + function: (identifier) @function.builtin) + (#any-of? @function.builtin + "abs" "all" "any" "ascii" "bin" "bool" "breakpoint" "bytearray" "bytes" "callable" "chr" "classmethod" + "compile" "complex" "delattr" "dict" "dir" "divmod" "enumerate" "eval" "exec" "fail" "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" + "struct" "sum" "super" "tuple" "type" "vars" "zip" "__import__")) + +;; Function definitions +(function_definition + name: (identifier) @function) + +(type (identifier) @type) +(type + (subscript + (identifier) @type)) ; type subscript: Tuple[int] + +((call + function: (identifier) @_isinstance + arguments: (argument_list + (_) + (identifier) @type)) + (#eq? @_isinstance "isinstance")) + +((identifier) @type.builtin + (#any-of? @type.builtin + ;; https://docs.python.org/3/library/exceptions.html + "ArithmeticError" "BufferError" "LookupError" "AssertionError" "AttributeError" + "EOFError" "FloatingPointError" "ModuleNotFoundError" "IndexError" "KeyError" + "KeyboardInterrupt" "MemoryError" "NameError" "NotImplementedError" "OSError" "OverflowError" "RecursionError" + "ReferenceError" "RuntimeError" "StopIteration" "StopAsyncIteration" "SyntaxError" "IndentationError" "TabError" + "SystemError" "SystemExit" "TypeError" "UnboundLocalError" "UnicodeError" "UnicodeEncodeError" "UnicodeDecodeError" + "UnicodeTranslateError" "ValueError" "ZeroDivisionError" "EnvironmentError" "IOError" "WindowsError" + "BlockingIOError" "ChildProcessError" "ConnectionError" "BrokenPipeError" "ConnectionAbortedError" + "ConnectionRefusedError" "ConnectionResetError" "FileExistsError" "FileNotFoundError" "InterruptedError" + "IsADirectoryError" "NotADirectoryError" "PermissionError" "ProcessLookupError" "TimeoutError" "Warning" + "UserWarning" "DeprecationWarning" "PendingDeprecationWarning" "SyntaxWarning" "RuntimeWarning" + "FutureWarning" "UnicodeWarning" "BytesWarning" "ResourceWarning" + ;; https://docs.python.org/3/library/stdtypes.html + "bool" "int" "float" "complex" "list" "tuple" "range" "str" + "bytes" "bytearray" "memoryview" "set" "frozenset" "dict" "type")) + +;; Normal parameters +(parameters + (identifier) @parameter) +;; Lambda parameters +(lambda_parameters + (identifier) @parameter) +(lambda_parameters + (tuple_pattern + (identifier) @parameter)) +; Default parameters +(keyword_argument + name: (identifier) @parameter) +; Naming parameters on call-site +(default_parameter + name: (identifier) @parameter) +(typed_parameter + (identifier) @parameter) +(typed_default_parameter + (identifier) @parameter) +; Variadic parameters *args, **kwargs +(parameters + (list_splat_pattern ; *args + (identifier) @parameter)) +(parameters + (dictionary_splat_pattern ; **kwargs + (identifier) @parameter)) + + +;; Literals +(none) @constant.builtin +[(true) (false)] @boolean +((identifier) @variable.builtin + (#eq? @variable.builtin "self")) +((identifier) @variable.builtin + (#eq? @variable.builtin "cls")) + +(integer) @number +(float) @float + +(comment) @comment @spell + +((module . (comment) @preproc) + (#lua-match? @preproc "^#!/")) + +(string) @string +[ + (escape_sequence) + "{{" + "}}" +] @string.escape + +; doc-strings + +(module . (expression_statement (string) @string.documentation @spell)) + +(function_definition + body: + (block + . (expression_statement (string) @string.documentation @spell))) + +; Tokens + +[ + "-" + "-=" + ":=" + "!=" + "*" + "**" + "**=" + "*=" + "/" + "//" + "//=" + "/=" + "&" + "&=" + "%" + "%=" + "^" + "^=" + "+" + "+=" + "<" + "<<" + "<<=" + "<=" + "<>" + "=" + "==" + ">" + ">=" + ">>" + ">>=" + "@" + "@=" + "|" + "|=" + "~" + "->" +] @operator + +; Keywords +[ + "and" + "in" + "not" + "or" + + "del" +] @keyword.operator + +[ + "def" + "lambda" +] @keyword.function + +[ + "async" + "await" + "exec" + "nonlocal" + "pass" + "print" + "with" + "as" +] @keyword + +[ + "async" + "await" +] @keyword.coroutine + +[ + "return" +] @keyword.return + +((call + function: (identifier) @include + arguments: (argument_list + (string) @conceal)) + (#eq? @include "load")) + +["if" "elif" "else" "match" "case"] @conditional + +["for" "while" "break" "continue"] @repeat + +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + +(interpolation + "{" @punctuation.special + "}" @punctuation.special) + +(type_conversion) @function.macro + +["," "." ":" ";" (ellipsis)] @punctuation.delimiter + +;; Error +(ERROR) @error + +;; Starlark-specific + +;; Assertion calls +(assert_keyword) @keyword + +(assert_builtin) @function.builtin + +;; Struct definitions +((call + function: (identifier) @_func + arguments: (argument_list + (keyword_argument + name: (identifier) @field))) + (#eq? @_func "struct")) + +;; Function calls + +(call + function: (identifier) @function.call) + +(call + function: (attribute + attribute: (identifier) @method.call)) + +((call + function: (identifier) @constructor) + (#lua-match? @constructor "^[A-Z]")) + +((call + function: (attribute + attribute: (identifier) @constructor)) + (#lua-match? @constructor "^[A-Z]")) diff --git a/tree-sitter/highlights/supercollider.scm b/tree-sitter/highlights/supercollider.scm new file mode 100644 index 0000000000..f9b6d6f1f4 --- /dev/null +++ b/tree-sitter/highlights/supercollider.scm @@ -0,0 +1,97 @@ +; highlights.scm +; See this for full list: https://github.com/nvim-treesitter/nvim-treesitter/blob/master/CONTRIBUTING.md + +; comments +(line_comment) @comment +(block_comment) @comment + +; Argument definition +(argument name: (identifier) @parameter) + +; Variables +(local_var name: (identifier) @variable) +(environment_var name:(identifier) @variable.builtin) +(builtin_var) @constant.builtin + +; (variable) @variable + +; Functions +(function_definition + name: (variable) @function) + +; For function calls +(named_argument + name: (identifier) @property) + +; Methods +(method_call + name: (method_name) @method) + +; Classes +(class) @type + +; Literals +(number) @number +(float) @float + +(string) @string +(symbol) @string.special + +; Operators +[ +"&&" +"||" +"&" +"|" +"^" +"==" +"!=" +"<" +"<=" +">" +">=" +"<<" +">>" +"+" +"-" +"*" +"/" +"%" +"=" +] @operator + +; Keywords +[ +"arg" +"classvar" +"const" +; "super" +; "this" +"var" +] @keyword + +; Brackets +[ + "(" + ")" + "[" + "]" + "{" + "}" + "|" +] @punctuation.bracket + +; Delimiters +[ + ";" + "." + "," +] @punctuation.delimiter + +; control structure +(control_structure) @conditional + +(escape_sequence) @string.escape + +; SinOsc.ar()!2 +(duplicated_statement) @repeat diff --git a/tree-sitter/highlights/surface.scm b/tree-sitter/highlights/surface.scm new file mode 100644 index 0000000000..785cfa68c0 --- /dev/null +++ b/tree-sitter/highlights/surface.scm @@ -0,0 +1,44 @@ +; Surface text is highlighted as such +(text) @text + +; Surface has two types of comments, both are highlighted as such +(comment) @comment + +; Surface attributes are highlighted as HTML attributes +(attribute_name) @tag.attribute + +; Attributes are highlighted as strings +(quoted_attribute_value) @string + +; Surface blocks are highlighted as keywords +[ + (start_block) + (end_block) + (subblock) +] @keyword + +; Surface supports HTML tags and are highlighted as such +[ + "<" + ">" + "" + "{" + "}" + "" + "{!--" + "--}" +] @tag.delimiter + +; Surface tags are highlighted as HTML +(tag_name) @tag + +; Surface components are highlighted as types (Elixir modules) +(component_name) @type + +; Surface directives are highlighted as keywords +(directive_name) @keyword + +; Surface operators +["="] @operator diff --git a/tree-sitter/highlights/svelte.scm b/tree-sitter/highlights/svelte.scm new file mode 100644 index 0000000000..9539f2b511 --- /dev/null +++ b/tree-sitter/highlights/svelte.scm @@ -0,0 +1,30 @@ +; inherits: html_tags + +(raw_text_expr) @none + +[ + (special_block_keyword) + (then) + (as) +] @keyword + +((special_block_keyword) @keyword.coroutine + (#eq? @keyword.coroutine "await")) + +((special_block_keyword) @exception + (#eq? @exception "catch")) + +((special_block_keyword) @conditional + (#any-of? @conditional "if" "else")) + +[ + "{" + "}" +] @punctuation.bracket + +[ + "#" + ":" + "/" + "@" +] @tag.delimiter diff --git a/tree-sitter/highlights/swift.scm b/tree-sitter/highlights/swift.scm new file mode 100644 index 0000000000..4cdd882a80 --- /dev/null +++ b/tree-sitter/highlights/swift.scm @@ -0,0 +1,181 @@ +[ "." ";" ":" "," ] @punctuation.delimiter +; TODO: "\\(" ")" in interpolations should be @punctuation.special +[ "\\(" "(" ")" "[" "]" "{" "}"] @punctuation.bracket + +; Identifiers +(attribute) @variable +(type_identifier) @type +(self_expression) @variable.builtin + +; Declarations +"func" @keyword.function + +[ + (visibility_modifier) + (member_modifier) + (function_modifier) + (property_modifier) + (parameter_modifier) + (inheritance_modifier) +] @type.qualifier + +(function_declaration (simple_identifier) @method) +(function_declaration ["init" @constructor]) +(throws) @keyword +(where_keyword) @keyword +(parameter external_name: (simple_identifier) @parameter) +(parameter name: (simple_identifier) @parameter) +(type_parameter (type_identifier) @parameter) +(inheritance_constraint (identifier (simple_identifier) @parameter)) +(equality_constraint (identifier (simple_identifier) @parameter)) +(pattern bound_identifier: (simple_identifier)) @variable + +[ + "typealias" + "struct" + "class" + "actor" + "enum" + "protocol" + "extension" + "indirect" + "nonisolated" + "override" + "convenience" + "required" + "some" +] @keyword + +[ + "async" + "await" +] @keyword.coroutine + +[ + (getter_specifier) + (setter_specifier) + (modify_specifier) +] @keyword + +(class_body (property_declaration (pattern (simple_identifier) @property))) +(protocol_property_declaration (pattern (simple_identifier) @property)) + +(import_declaration ["import" @include]) + +(enum_entry ["case" @keyword]) + +; Function calls +(call_expression (simple_identifier) @function.call) ; foo() +(call_expression ; foo.bar.baz(): highlight the baz() + (navigation_expression + (navigation_suffix (simple_identifier) @function.call))) +((navigation_expression + (simple_identifier) @type) ; SomeType.method(): highlight SomeType as a type + (#lua-match? @type "^[A-Z]")) + +(directive) @function.macro +(diagnostic) @function.macro + +; Statements +(for_statement ["for" @repeat]) +(for_statement ["in" @repeat]) +(for_statement (pattern) @variable) +(else) @keyword +(as_operator) @keyword + +["while" "repeat" "continue" "break"] @repeat + +["let" "var"] @keyword + +(guard_statement ["guard" @conditional]) +(if_statement ["if" @conditional]) +(switch_statement ["switch" @conditional]) +(switch_entry ["case" @keyword]) +(switch_entry ["fallthrough" @keyword]) +(switch_entry (default_keyword) @keyword) +"return" @keyword.return +(ternary_expression + ["?" ":"] @conditional) + +["do" (throw_keyword) (catch_keyword)] @keyword + +(statement_label) @label + +; Comments +[ + (comment) + (multiline_comment) +] @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^///[^/]")) + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^///$")) + +((multiline_comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +; String literals +(line_str_text) @string +(str_escaped_char) @string +(multi_line_str_text) @string +(raw_str_part) @string +(raw_str_end_part) @string +(raw_str_interpolation_start) @punctuation.special +["\"" "\"\"\""] @string + +; Lambda literals +(lambda_literal ["in" @keyword.operator]) + +; Basic literals +[ + (integer_literal) + (hex_literal) + (oct_literal) + (bin_literal) +] @number +(real_literal) @float +(boolean_literal) @boolean +"nil" @constant.builtin + +; Regex literals +(regex_literal) @string.regex + +; Operators +(custom_operator) @operator +[ + "try" + "try?" + "try!" + "!" + "+" + "-" + "*" + "/" + "%" + "=" + "+=" + "-=" + "*=" + "/=" + "<" + ">" + "<=" + ">=" + "++" + "--" + "&" + "~" + "%=" + "!=" + "!==" + "==" + "===" + "??" + + "->" + + "..<" + "..." +] @operator diff --git a/tree-sitter/highlights/sxhkdrc.scm b/tree-sitter/highlights/sxhkdrc.scm new file mode 100644 index 0000000000..03b70221e5 --- /dev/null +++ b/tree-sitter/highlights/sxhkdrc.scm @@ -0,0 +1,10 @@ +(modifier) @keyword +(operator) @operator +(attribute) @attribute +(command_sync_prefix) @type +(punctuation) @punctuation.bracket +(delimiter) @punctuation.delimiter +(keysym) @variable +(comment) @comment +(range) @number +"\\\n" @punctuation.special diff --git a/tree-sitter/highlights/systemtap.scm b/tree-sitter/highlights/systemtap.scm new file mode 100644 index 0000000000..54a27b8978 --- /dev/null +++ b/tree-sitter/highlights/systemtap.scm @@ -0,0 +1,153 @@ +(identifier) @variable + +(preprocessor_macro_definition + name: (identifier) @function.macro) + +(preprocessor_macro_expansion) @function.macro + +(preprocessor_constant) @constant.macro + +(number) @number +(string) @string +(escape_sequence) @string.escape + +[ + (script_argument_string) + (script_argument_number) +] @constant + +(probe_point_component) @function + +(function_definition + name: (identifier) @function) + +(parameter + name: (identifier) @parameter) + +(type) @type.builtin + +(aggregation_operator) @attribute + +(member_expression + member: (identifier) @field) + +(call_expression + function: (identifier) @function.call) + +((call_expression + function: (identifier) @function.builtin) + (#any-of? @function.builtin + "print" "printd" "printdln" "printf" "println" + "sprint" "sprintd" "sprintdln" "sprintf" "sprintln")) + +((identifier) @variable.builtin + (#lua-match? @variable.builtin "^\$+[0-9A-Z_a-z]+\$*$")) + +(shebang_line) @preproc + +(comment) @comment @spell + +[ + "!" + "!=" + "!~" + "$" + "$$" + "%" + "%=" + "&" + "&&" + "&=" + "*" + "*=" + "+" + "++" + "+=" + "-" + "--" + "-=" + "->" + "." + ".=" + "/" + "/=" + ":" + "<" + "<<" + "<<<" + "<<=" + "<=" + "=" + "==" + "=~" + ">" + ">=" + ">>" + ">>=" + "?" + "^" + "^=" + "|" + "|=" + "||" + "~" +] @operator + +[ + "," + (null_statement) +] @punctuation.delimiter + +[ + "%{" + "%}" + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "delete" + "limit" + "next" + "probe" +] @keyword + +"function" @keyword.function +"in" @keyword.operator +"return" @keyword.return + +[ + "if" + "else" +] @conditional + +[ + "break" + "continue" + "for" + "foreach" + "while" +] @repeat + +[ + "try" + "catch" +] @exception + +[ + "%(" + "%)" + "%:" + "%?" + (preprocessor_tokens) + (embedded_code) +] @preproc + +"@define" @define + +"private" @type.qualifier +"global" @storageclass diff --git a/tree-sitter/highlights/t32.scm b/tree-sitter/highlights/t32.scm new file mode 100644 index 0000000000..d1543efd02 --- /dev/null +++ b/tree-sitter/highlights/t32.scm @@ -0,0 +1,232 @@ +; Keywords, punctuation and operators +[ + "=" + "^^" + "||" + "&&" + "+" + "-" + "*" + "/" + "%" + "|" + "^" + "==" + "!=" + ">" + ">=" + "<=" + "<" + "<<" + ">>" + ".." + "--" + "++" + "+" + "-" + "~" + "!" + "&" + "->" + "*" + "-=" + "+=" + "*=" + "/=" + "%=" + "|=" + "&=" + "^=" + ">>=" + "<<=" + "--" + "++" +] @operator + +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + +[ + "," + "." +] @punctuation.delimiter + +[ + "enum" + "struct" + "union" +] @keyword + +"sizeof" @keyword.operator + +[ + "const" + "volatile" +] @type.qualifier + + +; Operators in comma and conditional HLL expressions +(hll_comma_expression + "," @operator) + +(hll_conditional_expression + [ + "?" + ":" +] @conditional.ternary) + + +; Strings and others literal types +(access_class) @constant.builtin + +[ + (address) + (bitmask) + (file_handle) + (integer) + (hll_number_literal) +] @number + +[ + (float) + (frequency) + (percentage) + (time) +] @float + +[ + (string) + (hll_string_literal) +] @string + +(hll_escape_sequence) @string.escape + +(path) @string.special +(symbol) @symbol + +[ + (character) + (hll_char_literal) +] @character + + +; Types in HLL expressions +[ + (hll_type_identifier) + (hll_type_descriptor) +] @type + +(hll_type_qualifier) @type.qualifier + +(hll_primitive_type) @type.builtin + + +; HLL expressions +(hll_call_expression + function: (identifier) @function.call) + +(hll_call_expression + function: (hll_field_expression + field: (hll_field_identifier) @function.call)) + + +; HLL variables +(identifier) @variable +(hll_field_identifier) @field + + +; Commands +(command_expression + command: (identifier) @keyword) + +(macro_definition + command: (identifier) @keyword) + +(call_expression + function: (identifier) @function.builtin) + + +; Returns +( + (command_expression + command: (identifier) @keyword.return) + (#match? @keyword.return "^[eE][nN][dD]([dD][oO])?$") +) +( + (command_expression + command: (identifier) @keyword.return) + (#lua-match? @keyword.return "^[rR][eE][tT][uU][rR][nN]$") +) + + +; Subroutine calls +(subroutine_call_expression + command: (identifier) @keyword + subroutine: (identifier) @function.call) + + +; Variables, constants and labels +(macro) @variable.builtin +(trace32_hll_variable) @variable.builtin + +(argument_list + (identifier) @constant.builtin) + +( + (argument_list (identifier) @constant.builtin) + (#lua-match? @constant.builtin "^[%%/][%l%u][%l%u%d.]*$") +) + +( + (command_expression + command: (identifier) @keyword + arguments: (argument_list . (identifier) @label)) + (#lua-match? @keyword "^[gG][oO][tT][oO]$") +) + +(labeled_expression + label: (identifier) @label) + +(option_expression + (identifier) @constant.builtin) + +(format_expression + (identifier) @constant.builtin) + + +; Subroutine blocks +(subroutine_block + command: (identifier) @keyword.function + subroutine: (identifier) @function) + +(labeled_expression + label: (identifier) @function + (block)) + + +; Parameter declarations +(parameter_declaration + command: (identifier) @keyword + (identifier)? @constant.builtin + macro: (macro) @parameter) + + +; Control flow +(if_block + command: (identifier) @conditional) +(else_block + command: (identifier) @conditional) + +(while_block + command: (identifier) @repeat) +(repeat_block + command: (identifier) @repeat) + + +(comment) @comment @spell diff --git a/tree-sitter/highlights/tablegen.scm b/tree-sitter/highlights/tablegen.scm new file mode 100644 index 0000000000..9b2782d191 --- /dev/null +++ b/tree-sitter/highlights/tablegen.scm @@ -0,0 +1,156 @@ +; Preprocs + +(preprocessor_directive) @preproc + +; Includes + +"include" @include + +; Keywords + +[ + "assert" + "class" + "multiclass" + "field" + "let" + "def" + "defm" + "defset" + "defvar" +] @keyword + +[ + "in" +] @keyword.operator + +; Conditionals + +[ + "if" + "else" + "then" +] @conditional + +; Repeats + +[ + "foreach" +] @repeat + +; Variables + +(identifier) @variable + +(var) @variable.builtin + +; Parameters + +(template_arg (identifier) @parameter) + + +; Types + +(type) @type + +[ + "bit" + "int" + "string" + "dag" + "bits" + "list" + "code" +] @type.builtin + +(class name: (identifier) @type) + +(multiclass name: (identifier) @type) + +(def name: (value (_) @type)) + +(defm name: (value (_) @type)) + +(defset name: (identifier) @type) + +(parent_class_list (identifier) @type (value (_) @type)?) + +(anonymous_record (identifier) @type) + +(anonymous_record (value (_) @type)) + +((identifier) @type + (#lua-match? @type "^_*[A-Z][A-Z0-9_]+$")) + +; Fields + +(instruction + (identifier) @field) + +(let_instruction + (identifier) @field) + +; Functions + +([ + (bang_operator) + (cond_operator) +] @function + (#set! "priority" 105)) + +; Operators + +[ + "=" + "#" + "-" + ":" + "..." +] @operator + +; Literals + +(string) @string + +(code) @string.special + +(integer) @number + +(boolean) @boolean + +(uninitialized_value) @constant.builtin + +; Punctuation + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ "(" ")" ] @punctuation.bracket + +[ "<" ">" ] @punctuation.bracket + +[ + "." + "," + ";" +] @punctuation.delimiter + +[ + "!" +] @punctuation.special + +; Comments + +[ + (comment) + (multiline_comment) +] @comment @spell + + +((comment) @preproc + (#lua-match? @preproc "^.*RUN")) + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/teal.scm b/tree-sitter/highlights/teal.scm new file mode 100644 index 0000000000..4af3e212b1 --- /dev/null +++ b/tree-sitter/highlights/teal.scm @@ -0,0 +1,135 @@ + +;; Primitives +(boolean) @boolean +(comment) @comment @spell +((comment) @comment.documentation + (#lua-match? @comment.documentation "^[-][-][-]")) +((comment) @comment.documentation + (#lua-match? @comment.documentation "^[-][-](%s?)@")) +(shebang_comment) @preproc +(identifier) @variable +((identifier) @variable.builtin + (#eq? @variable.builtin "self")) +(nil) @constant.builtin +(number) @number +(string) @string +(table_constructor ["{" "}"] @constructor) +(varargs "..." @constant.builtin) +[ "," "." ":" ";" ] @punctuation.delimiter + +(escape_sequence) @string.escape +(format_specifier) @string.escape + +;; Basic statements/Keywords +[ "if" "then" "elseif" "else" ] @conditional +[ "for" "while" "repeat" "until" ] @repeat +"return" @keyword.return +[ "in" "local" (break) (goto) "do" "end" ] @keyword +(label) @label + +;; Global isn't a real keyword, but it gets special treatment in these places +(var_declaration "global" @keyword) +(type_declaration "global" @keyword) +(function_statement "global" @keyword) +(record_declaration "global" @keyword) +(enum_declaration "global" @keyword) + +;; Ops +(bin_op (op) @operator) +(unary_op (op) @operator) +[ "=" "as" ] @operator + +;; Functions +(function_statement + "function" @keyword.function + . name: (_) @function) +(anon_function + "function" @keyword.function) +(function_body "end" @keyword.function) + +(arg name: (identifier) @parameter) + +(function_signature + (arguments + . (arg name: (identifier) @variable.builtin)) + (#eq? @variable.builtin "self")) + +(typeargs + "<" @punctuation.bracket + . (_) @parameter + . ("," . (_) @parameter)* + . ">" @punctuation.bracket) + +(function_call + (identifier) @function . (arguments)) +(function_call + (index (_) key: (identifier) @function) . (arguments)) +(function_call + (method_index (_) key: (identifier) @function) . (arguments)) + +;; Types +(record_declaration + . "record" @keyword + name: (identifier) @type) +(anon_record . "record" @keyword) +(record_body + (record_declaration + . [ "record" ] @keyword + . name: (identifier) @type)) +(record_body + (enum_declaration + . [ "enum" ] @keyword + . name: (identifier) @type)) +(record_body + (typedef + . "type" @keyword + . name: (identifier) @type . "=")) +(record_body + (metamethod "metamethod" @keyword)) +(record_body + (userdata) @keyword) + +(enum_declaration + "enum" @keyword + name: (identifier) @type) + +(type_declaration "type" @keyword) +(type_declaration (identifier) @type) +(simple_type name: (identifier) @type) +(type_index (identifier) @type) +(type_union "|" @operator) +(function_type "function" @type) + +;; The rest of it +(var_declaration + declarators: (var_declarators + (var name: (identifier) @variable))) +(var_declaration + declarators: (var_declarators + (var + "<" @punctuation.bracket + . attribute: (attribute) @attribute + . ">" @punctuation.bracket))) +[ "(" ")" "[" "]" "{" "}" ] @punctuation.bracket + +;; Only highlight format specifiers in calls to string.format +;; string.format('...') +;(function_call +; called_object: (index +; (identifier) @base +; key: (identifier) @entry) +; arguments: (arguments . +; (string (format_specifier) @string.escape)) +; +; (#eq? @base "string") +; (#eq? @entry "format")) + +;; ('...'):format() +;(function_call +; called_object: (method_index +; (string (format_specifier) @string.escape) +; key: (identifier) @func-name) +; (#eq? @func-name "format")) + + +(ERROR) @error diff --git a/tree-sitter/highlights/terraform.scm b/tree-sitter/highlights/terraform.scm new file mode 100644 index 0000000000..95d0c0451b --- /dev/null +++ b/tree-sitter/highlights/terraform.scm @@ -0,0 +1,21 @@ +; inherits: hcl + +; Terraform specific references +; +; +; local/module/data/var/output +(expression (variable_expr (identifier) @variable.builtin (#any-of? @variable.builtin "data" "var" "local" "module" "output")) (get_attr (identifier) @field)) + +; path.root/cwd/module +(expression (variable_expr (identifier) @type.builtin (#eq? @type.builtin "path")) (get_attr (identifier) @variable.builtin (#any-of? @variable.builtin "root" "cwd" "module"))) + +; terraform.workspace +(expression (variable_expr (identifier) @type.builtin (#eq? @type.builtin "terraform")) (get_attr (identifier) @variable.builtin (#any-of? @variable.builtin "workspace"))) + +; Terraform specific keywords + +; FIXME: ideally only for identifiers under a `variable` block to minimize false positives +((identifier) @type.builtin (#any-of? @type.builtin "bool" "string" "number" "object" "tuple" "list" "map" "set" "any")) +(object_elem val: (expression + (variable_expr + (identifier) @type.builtin (#any-of? @type.builtin "bool" "string" "number" "object" "tuple" "list" "map" "set" "any")))) diff --git a/tree-sitter/highlights/thrift.scm b/tree-sitter/highlights/thrift.scm new file mode 100644 index 0000000000..2c963f5bcf --- /dev/null +++ b/tree-sitter/highlights/thrift.scm @@ -0,0 +1,230 @@ +; Variables + +((identifier) @variable + (#set! "priority" 95)) + +; Includes + +[ + "include" + "cpp_include" +] @include + +; Function + +(function_definition + (identifier) @function) + +; Fields + +(field (identifier) @field) + +; Parameters + +(function_definition + (parameters + (parameter (identifier) @parameter))) + +(throws + (parameters + (parameter (identifier) @parameter))) + +; Types + +(typedef_identifier) @type +(struct_definition + "struct" (identifier) @type) + +(union_definition + "union" (identifier) @type) + +(exception_definition + "exception" (identifier) @type) + +(service_definition + "service" (identifier) @type) + +(interaction_definition + "interaction" (identifier) @type) + +(type + type: (identifier) @type) + +(definition_type + type: (identifier) @type) + +((identifier) @type + (#lua-match? @type "^[_]*[A-Z]")) + +; Constants + +(const_definition (identifier) @constant) +((identifier) @constant + (#lua-match? @constant "^[_A-Z][A-Z0-9_]*$")) +(enum_definition "enum" + . (identifier) @type + "{" (identifier) @constant "}") + +; Builtin Types + +(primitive) @type.builtin + +[ + "list" + "map" + "set" + "sink" + "stream" + "void" +] @type.builtin + +; Namespace + +(namespace_declaration + (namespace_scope) @tag + [(namespace) @namespace (_ (identifier) @namespace)]) + +; Attributes + +(annotation_definition + (annotation_identifier (identifier) @attribute)) +(fb_annotation_definition + "@" @attribute (annotation_identifier (identifier) @attribute) + (identifier)? @attribute) +(namespace_uri (string) @attribute) + +; Operators + +[ + "=" + "&" +] @operator + +; Exceptions + +[ + "throws" +] @exception + +; Keywords + +[ + "enum" + "exception" + "extends" + "interaction" + "namespace" + "senum" + "service" + "struct" + "typedef" + "union" + "uri" +] @keyword + +; Deprecated Keywords + +[ + "cocoa_prefix" + "cpp_namespace" + "csharp_namespace" + "delphi_namespace" + "java_package" + "perl_package" + "php_namespace" + "py_module" + "ruby_namespace" + "smalltalk_category" + "smalltalk_prefix" + "xsd_all" + "xsd_attrs" + "xsd_namespace" + "xsd_nillable" + "xsd_optional" +] @keyword + +; Extended Keywords +[ + "package" + "performs" +] @keyword + +[ + "async" + "oneway" +] @keyword.coroutine + +; Qualifiers + +[ + "client" + "const" + "idempotent" + "optional" + "permanent" + "readonly" + "required" + "safe" + "server" + "stateful" + "transient" +] @type.qualifier + +; Literals + +(string) @string + +(escape_sequence) @string.escape + +(namespace_uri + (string) @text.uri @string.special) + +(number) @number + +(double) @float + +(boolean) @boolean + +; Typedefs + +(typedef_identifier) @type.definition + +; Punctuation + +[ + "*" +] @punctuation.special + +["{" "}"] @punctuation.bracket + +["(" ")"] @punctuation.bracket + +["[" "]"] @punctuation.bracket + +["<" ">"] @punctuation.bracket + +[ + "." + "," + ";" + ":" +] @punctuation.delimiter + +; Comments + +(comment) @comment @spell + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) + +((comment) @comment.documentation + (#lua-match? @comment.documentation "^///[^/]")) +((comment) @comment.documentation + (#lua-match? @comment.documentation "^///$")) + +((comment) @preproc + (#lua-match? @preproc "#!.*")) + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/tiger.scm b/tree-sitter/highlights/tiger.scm new file mode 100644 index 0000000000..c0dd7dadf7 --- /dev/null +++ b/tree-sitter/highlights/tiger.scm @@ -0,0 +1,121 @@ +; Built-ins {{{ +((function_call + function: (identifier) @function.builtin) + (#any-of? @function.builtin "chr" "concat" "exit" "flush" "getchar" "not" "ord" "print" "print_err" "print_int" "size" "strcmp" "streq" "substring") + ; FIXME: not supported by neovim + ; (#is-not? local) + ) + +((type_identifier) @type.builtin + (#any-of? @type.builtin "int" "string" "Object") + ; FIXME: not supported by neovim + ; (#is-not? local) + ) + +((identifier) @variable.builtin + (#eq? @variable.builtin "self") + ; FIXME: not supported by neovim + ; (#is-not? local) + ) +; }}} + +; Keywords {{{ +[ + "function" + "primitive" + "method" +] @keyword.function + +[ + "do" + "for" + "to" + "while" +] @repeat + +"new" @keyword.operator + +"import" @include + +[ + "array" + (break_expression) + "else" + "end" + "if" + "in" + "let" + "of" + "then" + "type" + "var" + + "class" + "extends" + + "_cast" + "_chunks" + "_exp" + "_lvalue" + "_namety" +] @keyword +; }}} + +; Operators {{{ +(operator) @operator + +[ + "," + ";" + ":" + "." +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket +; }}} + +; Functions and methods {{{ +(function_call + function: (identifier) @function) +(function_declaration + name: (identifier) @function) +(primitive_declaration + name: (identifier) @function) + +(method_call + method: (identifier) @method) +(method_declaration + name: (identifier) @method) + +(parameters + name: (identifier) @parameter) +; }}} + +; Declarations {{{ +(import_declaration + file: (string_literal) @string.special) +; }}} + +; Literals {{{ +(nil_literal) @constant.builtin +(integer_literal) @number +(string_literal) @string +(escape_sequence) @string.escape +; }}} + +; Misc {{{ +(comment) @comment + +(type_identifier) @type +(field_identifier) @property +(identifier) @variable +; }}} + +; vim: sw=2 foldmethod=marker diff --git a/tree-sitter/highlights/tlaplus.scm b/tree-sitter/highlights/tlaplus.scm new file mode 100644 index 0000000000..5c2289b8c9 --- /dev/null +++ b/tree-sitter/highlights/tlaplus.scm @@ -0,0 +1,270 @@ +; ; Intended for consumption by nvim-treesitter +; ; Default capture names for nvim-treesitter found here: +; ; https://github.com/nvim-treesitter/nvim-treesitter/blob/e473630fe0872cb0ed97cd7085e724aa58bc1c84/lua/nvim-treesitter/highlight.lua#L14-L104 +; ; In this file, captures defined later take precedence over captures defined earlier + +; Keywords +[ + "ACTION" + "ASSUME" + "ASSUMPTION" + "AXIOM" + "BY" + "CASE" + "CHOOSE" + "CONSTANT" + "CONSTANTS" + "COROLLARY" + "DEF" + "DEFINE" + "DEFS" + "DOMAIN" + "ELSE" + "ENABLED" + "EXCEPT" + "EXTENDS" + "HAVE" + "HIDE" + "IF" + "IN" + "INSTANCE" + "LAMBDA" + "LEMMA" + "LET" + "LOCAL" + "MODULE" + "NEW" + "OBVIOUS" + "OMITTED" + "ONLY" + "OTHER" + "PICK" + "PROOF" + "PROPOSITION" + "PROVE" + "QED" + "RECURSIVE" + "SF_" + "STATE" + "SUBSET" + "SUFFICES" + "TAKE" + "TEMPORAL" + "THEN" + "THEOREM" + "UNCHANGED" + "UNION" + "USE" + "VARIABLE" + "VARIABLES" + "WF_" + "WITH" + "WITNESS" + (address) + (all_map_to) + (assign) + (case_arrow) + (case_box) + (def_eq) + (exists) + (forall) + (gets) + (label_as) + (maps_to) + (set_in) + (temporal_exists) + (temporal_forall) +] @keyword + +; Pluscal keywords +[ + (pcal_algorithm_start) + "algorithm" + "assert" + "begin" + "call" + "define" + "end" + "fair" + "goto" + "macro" + "or" + "procedure" + "process" + "skip" + "variable" + "variables" + "when" + "with" +] @keyword +[ + "await" +] @keyword.coroutine +(pcal_with ("=") @keyword) +(pcal_process ("=") @keyword) +[ + "if" + "then" + "else" + "elsif" + (pcal_end_if) + "either" + (pcal_end_either) +] @conditional +[ + "while" + "do" + (pcal_end_while) + "with" + (pcal_end_with) +] @repeat +("return") @keyword.return +("print") @function.macro + + +; Literals +(binary_number (format) @keyword) +(binary_number (value) @number) +(boolean) @boolean +(boolean_set) @type +(hex_number (format) @keyword) +(hex_number (value) @number) +(int_number_set) @type +(nat_number) @number +(nat_number_set) @type +(octal_number (format) @keyword) +(octal_number (value) @number) +(real_number) @number +(real_number_set) @type +(string) @string +(escape_char) @string.escape +(string_set) @type + +; Namespaces +(extends (identifier_ref) @namespace) +(instance (identifier_ref) @namespace) +(module name: (identifier) @namespace) +(pcal_algorithm name: (identifier) @namespace) + +; Operators, functions, and macros +(bound_infix_op symbol: (_) @operator) +(bound_nonfix_op symbol: (_) @operator) +(bound_postfix_op symbol: (_) @operator) +(bound_prefix_op symbol: (_) @operator) +((prefix_op_symbol) @operator) +((infix_op_symbol) @operator) +((postfix_op_symbol) @operator) +(function_definition name: (identifier) @function) +(module_definition name: (_) @include) +(operator_definition name: (_) @function.macro) +(pcal_macro_decl name: (identifier) @function.macro) +(pcal_macro_call name: (identifier) @function.macro) +(pcal_proc_decl name: (identifier) @function.macro) +(pcal_process name: (identifier) @function) +(recursive_declaration (identifier) @function.macro) +(recursive_declaration (operator_declaration name: (_) @function.macro)) + +; Constants and variables +(constant_declaration (identifier) @constant) +(constant_declaration (operator_declaration name: (_) @constant)) +(pcal_var_decl (identifier) @variable) +(pcal_with (identifier) @parameter) +((".") . (identifier) @attribute) +(record_literal (identifier) @attribute) +(set_of_records (identifier) @attribute) +(variable_declaration (identifier) @variable) + +; Parameters +(choose (identifier) @parameter) +(choose (tuple_of_identifiers (identifier) @parameter)) +(lambda (identifier) @parameter) +(module_definition (operator_declaration name: (_) @parameter)) +(module_definition parameter: (identifier) @parameter) +(operator_definition (operator_declaration name: (_) @parameter)) +(operator_definition parameter: (identifier) @parameter) +(pcal_macro_decl parameter: (identifier) @parameter) +(pcal_proc_var_decl (identifier) @parameter) +(quantifier_bound (identifier) @parameter) +(quantifier_bound (tuple_of_identifiers (identifier) @parameter)) +(unbounded_quantification (identifier) @parameter) + +; Delimiters +[ + (langle_bracket) + (rangle_bracket) + (rangle_bracket_sub) + "{" + "}" + "[" + "]" + "]_" + "(" + ")" +] @punctuation.bracket +[ + "," + ":" + "." + "!" + ";" + (bullet_conj) + (bullet_disj) + (prev_func_val) + (placeholder) +] @punctuation.delimiter + +; Proofs +(assume_prove (new (identifier) @parameter)) +(assume_prove (new (operator_declaration name: (_) @parameter))) +(assumption name: (identifier) @constant) +(pick_proof_step (identifier) @parameter) +(proof_step_id "<" @punctuation.bracket) +(proof_step_id (level) @label) +(proof_step_id (name) @label) +(proof_step_id ">" @punctuation.bracket) +(proof_step_ref "<" @punctuation.bracket) +(proof_step_ref (level) @label) +(proof_step_ref (name) @label) +(proof_step_ref ">" @punctuation.bracket) +(take_proof_step (identifier) @parameter) +(theorem name: (identifier) @constant) + +; Comments and tags +(block_comment "(*" @comment) +(block_comment "*)" @comment) +(block_comment_text) @comment +(comment) @comment +(single_line) @comment +(_ label: (identifier) @label) +(label name: (_) @label) +(pcal_goto statement: (identifier) @label) + +; Reference highlighting with the same color as declarations. +; `constant`, `operator`, and others are custom captures defined in locals.scm +((identifier_ref) @constant (#is? @constant constant)) +((identifier_ref) @function (#is? @function function)) +((identifier_ref) @function.macro (#is? @function.macro macro)) +((identifier_ref) @include (#is? @include import)) +((identifier_ref) @parameter (#is? @parameter parameter)) +((identifier_ref) @variable (#is? @variable var)) +((prefix_op_symbol) @constant (#is? @constant constant)) +((prefix_op_symbol) @function.macro (#is? @function.macro macro)) +((prefix_op_symbol) @parameter (#is? @parameter parameter)) +((infix_op_symbol) @constant (#is? @constant constant)) +((infix_op_symbol) @function.macro (#is? @function.macro macro)) +((infix_op_symbol) @parameter (#is? @parameter parameter)) +((postfix_op_symbol) @constant (#is? @constant constant)) +((postfix_op_symbol) @function.macro (#is? @function.macro macro)) +((postfix_op_symbol) @parameter (#is? @parameter parameter)) +(bound_prefix_op symbol: (_) @constant (#is? @constant constant)) +(bound_prefix_op symbol: (_) @function.macro (#is? @function.macro macro)) +(bound_prefix_op symbol: (_) @parameter (#is? @parameter parameter)) +(bound_infix_op symbol: (_) @constant (#is? @constant constant)) +(bound_infix_op symbol: (_) @function.macro (#is? @function.macro macro)) +(bound_infix_op symbol: (_) @parameter (#is? @parameter parameter)) +(bound_postfix_op symbol: (_) @constant (#is? @constant constant)) +(bound_postfix_op symbol: (_) @function.macro (#is? @function.macro macro)) +(bound_postfix_op symbol: (_) @parameter (#is? @parameter parameter)) +(bound_nonfix_op symbol: (_) @constant (#is? @constant constant)) +(bound_nonfix_op symbol: (_) @function.macro (#is? @function.macro macro)) +(bound_nonfix_op symbol: (_) @parameter (#is? @parameter parameter)) diff --git a/tree-sitter/highlights/todotxt.scm b/tree-sitter/highlights/todotxt.scm new file mode 100644 index 0000000000..37f91b8a74 --- /dev/null +++ b/tree-sitter/highlights/todotxt.scm @@ -0,0 +1,6 @@ +(done_task) @comment +(task (priority) @keyword) +(task (date) @comment) +(task (kv) @comment) +(task (project) @string) +(task (context) @type) diff --git a/tree-sitter/highlights/toml.scm b/tree-sitter/highlights/toml.scm new file mode 100644 index 0000000000..5255a20ab0 --- /dev/null +++ b/tree-sitter/highlights/toml.scm @@ -0,0 +1,36 @@ +; Properties +;----------- + +(bare_key) @type +(quoted_key) @string +(pair (bare_key)) @property + +; Literals +;--------- + +(boolean) @boolean +(comment) @comment @spell +(string) @string +(integer) @number +(float) @float +(offset_date_time) @string.special +(local_date_time) @string.special +(local_date) @string.special +(local_time) @string.special + +; Punctuation +;------------ + +"." @punctuation.delimiter +"," @punctuation.delimiter + +"=" @operator + +"[" @punctuation.bracket +"]" @punctuation.bracket +"[[" @punctuation.bracket +"]]" @punctuation.bracket +"{" @punctuation.bracket +"}" @punctuation.bracket + +(ERROR) @error diff --git a/tree-sitter/highlights/tsx.scm b/tree-sitter/highlights/tsx.scm new file mode 100644 index 0000000000..07391231c6 --- /dev/null +++ b/tree-sitter/highlights/tsx.scm @@ -0,0 +1 @@ +; inherits: typescript,jsx diff --git a/tree-sitter/highlights/turtle.scm b/tree-sitter/highlights/turtle.scm new file mode 100644 index 0000000000..98d4e0e946 --- /dev/null +++ b/tree-sitter/highlights/turtle.scm @@ -0,0 +1,58 @@ +(string) @string + +(lang_tag) @type + +[ + "_:" + "<" + ">" + (namespace) +] @namespace + +[ + (iri_reference) + (prefixed_name) +] @variable + +(blank_node_label) @variable + +"a" @variable.builtin + +(integer) @number + +[ + (decimal) + (double) +] @float + +(boolean_literal) @boolean + +[ + "BASE" + "PREFIX" + "@prefix" + "@base" +] @keyword + +[ + "." + "," + ";" +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + (anon) +] @punctuation.bracket + +(comment) @comment + +(echar) @string.escape + + +(rdf_literal + "^^" @type + datatype: (_ ["<" ">" (namespace)] @type) @type) diff --git a/tree-sitter/highlights/twig.scm b/tree-sitter/highlights/twig.scm new file mode 100644 index 0000000000..4e7bfe4f25 --- /dev/null +++ b/tree-sitter/highlights/twig.scm @@ -0,0 +1,55 @@ +(comment) @comment @spell + +(filter_identifier) @function.call +(function_identifier) @function.call +(test) @function.builtin +(variable) @variable +(string) @string +(interpolated_string) @string +(operator) @operator +(number) @number +(boolean) @boolean +(null) @constant.builtin +(keyword) @keyword +(attribute) @attribute +(tag) @tag +(conditional) @conditional +(repeat) @repeat +(method) @method +(parameter) @parameter + +[ + "{{" + "}}" + "{{-" + "-}}" + "{{~" + "~}}" + "{%" + "%}" + "{%-" + "-%}" + "{%~" + "~%}" +] @tag.delimiter + +[ + "," + "." +] @punctuation.delimiter + +[ + "?" + ":" + "=" + "|" +] @operator + +(interpolated_string ["#{" "}"] @punctuation.special) + +[ + "(" ")" + "[" "]" +] @punctuation.bracket + +(hash ["{" "}"] @punctuation.bracket) diff --git a/tree-sitter/highlights/typescript.scm b/tree-sitter/highlights/typescript.scm new file mode 100644 index 0000000000..798ff94718 --- /dev/null +++ b/tree-sitter/highlights/typescript.scm @@ -0,0 +1,157 @@ +; inherits: ecma + +"require" @include + +(import_require_clause source: (string) @text.uri) + +[ + "declare" + "enum" + "export" + "implements" + "interface" + "type" + "namespace" + "override" + "module" + "asserts" + "infer" + "is" +] @keyword + +[ + "keyof" + "satisfies" +] @keyword.operator + +(as_expression "as" @keyword.operator) +(export_statement "as" @keyword.operator) +(mapped_type_clause "as" @keyword.operator) + +[ + "abstract" + "private" + "protected" + "public" + "readonly" +] @type.qualifier + +; types + +(type_identifier) @type +(predefined_type) @type.builtin + +(import_statement "type" + (import_clause + (named_imports + ((import_specifier + name: (identifier) @type))))) + +(template_literal_type) @string + +(non_null_expression "!" @operator) + +;; punctuation + +(type_arguments + ["<" ">"] @punctuation.bracket) + +(type_parameters + ["<" ">"] @punctuation.bracket) + +(object_type + ["{|" "|}"] @punctuation.bracket) + +(union_type + "|" @punctuation.delimiter) + +(intersection_type + "&" @punctuation.delimiter) + +(type_annotation + ":" @punctuation.delimiter) + +(type_predicate_annotation + ":" @punctuation.delimiter) + +(index_signature + ":" @punctuation.delimiter) + +(omitting_type_annotation + "-?:" @punctuation.delimiter) + +(opting_type_annotation + "?:" @punctuation.delimiter) + +"?." @punctuation.delimiter + +(abstract_method_signature "?" @punctuation.special) +(method_signature "?" @punctuation.special) +(method_definition "?" @punctuation.special) +(property_signature "?" @punctuation.special) +(optional_parameter "?" @punctuation.special) +(optional_type "?" @punctuation.special) +(public_field_definition [ "?" "!" ] @punctuation.special) +(flow_maybe_type "?" @punctuation.special) + +(template_type ["${" "}"] @punctuation.special) + +(conditional_type ["?" ":"] @conditional.ternary) + +; Variables + +(undefined) @variable.builtin + +;;; Parameters +(required_parameter (identifier) @parameter) +(optional_parameter (identifier) @parameter) + +(required_parameter + (rest_pattern + (identifier) @parameter)) + +;; ({ a }) => null +(required_parameter + (object_pattern + (shorthand_property_identifier_pattern) @parameter)) + +;; ({ a = b }) => null +(required_parameter + (object_pattern + (object_assignment_pattern + (shorthand_property_identifier_pattern) @parameter))) + +;; ({ a: b }) => null +(required_parameter + (object_pattern + (pair_pattern + value: (identifier) @parameter))) + +;; ([ a ]) => null +(required_parameter + (array_pattern + (identifier) @parameter)) + +;; a => null +(arrow_function + parameter: (identifier) @parameter) + +;; global declaration +(ambient_declaration "global" @namespace) + +;; function signatures +(ambient_declaration + (function_signature + name: (identifier) @function)) + +;; method signatures +(method_signature name: (_) @method) + +;; property signatures +(property_signature + name: (property_identifier) @method + type: (type_annotation + [ + (union_type (parenthesized_type (function_type))) + (function_type) + ])) diff --git a/tree-sitter/highlights/ungrammar.scm b/tree-sitter/highlights/ungrammar.scm new file mode 100644 index 0000000000..027c6e0080 --- /dev/null +++ b/tree-sitter/highlights/ungrammar.scm @@ -0,0 +1,30 @@ +(comment) @comment + +(definition) @keyword + +(identifier) @variable + +(label_name) @label + +(token) @string + +[ + "=" + "|" +] @operator + +[ + "*" + "?" +] @repeat + +[ + ":" +] @punctuation.delimiter + +[ + "(" + ")" +] @punctuation.bracket + +(ERROR) @error diff --git a/tree-sitter/highlights/usd.scm b/tree-sitter/highlights/usd.scm new file mode 100644 index 0000000000..73968a647b --- /dev/null +++ b/tree-sitter/highlights/usd.scm @@ -0,0 +1,181 @@ +(None) @constant.builtin +(asset_path) @text.uri +(attribute_property) @property +(bool) @boolean +(comment) @comment @spell +(custom) @function.builtin +(float) @float +(integer) @number +(orderer) @function.call +(prim_path) @string.special +(relationship_type) @type +(string) @string +(uniform) @function.builtin +(variant_set_definition) @keyword + +;; Prefer namespace highlighting, if any. +;; +;; e.g. `rel fizz` - `fizz` uses `@identifier` +;; e.g. `rel foo:bar:fizz` - `foo` and `bar` use `@namespace` and `fizz` uses `@identifier` +;; +(identifier) @variable +(namespace_identifier) @namespace +(namespace_identifier + (identifier) @namespace +) + +[ + "class" + "def" + "over" +] @keyword.function + +["(" ")" "[" "]" "{" "}"] @punctuation.bracket +[":" ";" "."] @punctuation.delimiter + +[ + "=" +] @operator + +(attribute_type) @type +( + ;; Reference: https://openusd.org/release/api/sdf_page_front.html + (attribute_type) @type.builtin + (#any-of? @type.builtin + ;; Scalar types + "asset" "asset[]" + "bool" "bool[]" + "double" "double[]" + "float" "float[]" + "half" "half[]" + "int" "int[]" + "int64" "int64[]" + "string" "string[]" + "timecode" "timecode[]" + "token" "token[]" + "uchar" "uchar[]" + "uint" "uint[]" + "uint64" "uint64[]" + + ;; Dimensioned Types + "double2" "double2[]" + "double3" "double3[]" + "double4" "double4[]" + "float2" "float2[]" + "float3" "float3[]" + "float4" "float4[]" + "half2" "half2[]" + "half3" "half3[]" + "half4" "half4[]" + "int2" "int2[]" + "int3" "int3[]" + "int4" "int4[]" + "matrix2d" "matrix2d[]" + "matrix3d" "matrix3d[]" + "matrix4d" "matrix4d[]" + "quatd" "quatd[]" + "quatf" "quatf[]" + "quath" "quath[]" + + ;; Extra Types + "color3f" "color3f[]" + "normal3f" "normal3f[]" + "point3f" "point3f[]" + "texCoord2f" "texCoord2f[]" + "vector3d" "vector3d[]" + "vector3f" "vector3f[]" + "vector3h" "vector3h[]" + + "dictionary" + + ;; Deprecated Types + "EdgeIndex" "EdgeIndex[]" + "FaceIndex" "FaceIndex[]" + "Matrix4d" "Matrix4d[]" + "PointIndex" "PointIndex[]" + "PointFloat" "PointFloat[]" + "Transform" "Transform[]" + "Vec3f" "Vec3f[]" + ) +) + +( + (identifier) @keyword + (#any-of? @keyword + + ;; Reference: https://openusd.org/release/api/sdf_page_front.html + ;; LIVRPS names + "inherits" + "payload" + "references" + "specializes" + "variantSets" + "variants" + + ; assetInfo names + "assetInfo" + "identifier" + "name" + "payloadAssetDependencies" + "version" + + ;; clips names + "clips" + + "active" + "assetPaths" + "manifestAssetPath" + "primPath" + "templateAssetPath" + "templateEndTime" + "templateStartTime" + "templateStride" + "times" + + ;; customData names + "customData" + + "apiSchemaAutoApplyTo" + "apiSchemaOverridePropertyNames" + "className" + "extraPlugInfo" + "isUsdShadeContainer" + "libraryName" + "providesUsdShadeConnectableAPIBehavior" + "requiresUsdShadeEncapsulation" + "skipCodeGeneration" + + ;; Layer metadata names + "colorConfiguration" + "colorManagementSystem" + "customLayerData" + "defaultPrim" + "doc" + "endTimeCode" + "framesPerSecond" + "owner" + "startTimeCode" + "subLayers" + + ;; Prim metadata + "instanceable" + ) +) + +;; Common attribute metadata +( + (layer_offset + (identifier) @keyword + (#any-of? @keyword + + "offset" + "scale" + ) + ) +) + +;; Docstrings in USD +(metadata + (comment)* + (string) @comment.documentation +) diff --git a/tree-sitter/highlights/uxntal.scm b/tree-sitter/highlights/uxntal.scm new file mode 100644 index 0000000000..795c73d096 --- /dev/null +++ b/tree-sitter/highlights/uxntal.scm @@ -0,0 +1,84 @@ +; Includes + +(include + "~" @include + _ @text.uri @string.special) + +; Variables + +(identifier) @variable + +; Macros + +(macro + "%" + (identifier) @function.macro) + +((identifier) @function.macro + (#lua-match? @function.macro "^[a-z]?[0-9]*[A-Z-_]+$")) + +(rune + . rune_start: (rune_char ",") + . (identifier) @function.call) + +(rune + . rune_start: (rune_char ";") + . (identifier) @function.call) + +((identifier) @function.call + (#lua-match? @function.call "^:")) + +; Keywords + +(opcode) @keyword + +; Labels + +(label + "@" @symbol + (identifier) @function) + +(sublabel_reference + (identifier) @namespace + "/" @punctuation.delimiter + (identifier) @label) + +; Repeats + +((identifier) @repeat + (#eq? @repeat "while")) + +; Literals + +(raw_ascii) @string + +(hex_literal + "#" @symbol + (hex_lit_value) @string.special) + +(number) @number + +; Punctuation + +[ "{" "}" ] @punctuation.bracket + +[ "[" "]" ] @punctuation.bracket + +[ + "%" + "|" + "$" + "," + "_" + "." + "-" + ";" + "=" + "!" + "?" + "&" +] @punctuation.special + +; Comments + +(comment) @comment @spell diff --git a/tree-sitter/highlights/v.scm b/tree-sitter/highlights/v.scm new file mode 100644 index 0000000000..0fb3074faf --- /dev/null +++ b/tree-sitter/highlights/v.scm @@ -0,0 +1,467 @@ +; Includes + +[ + "import" + "module" +] @include + +; Keywords + +[ + "asm" + "assert" + "const" + "defer" + "enum" + "goto" + "interface" + "struct" + "sql" + "type" + "union" + "unsafe" +] @keyword + +[ + "as" + "in" + "!in" + "or" + "is" + "!is" +] @keyword.operator + +[ + "match" + "if" + "$if" + "else" + "$else" + "select" +] @conditional + +[ + "for" + "$for" + "continue" + "break" +] @repeat + +"fn" @keyword.function + +"return" @keyword.return + +[ + "__global" + "shared" + "static" + "const" +] @storageclass + +[ + "pub" + "mut" +] @type.qualifier + +[ + "go" + "spawn" + "lock" + "rlock" +] @keyword.coroutine + +; Variables + +(identifier) @variable + +; Namespace + +(module_clause + (identifier) @namespace) + +(import_path + (import_name) @namespace) + +(import_alias + (import_name) @namespace) + +; Literals + +[ (true) (false) ] @boolean + +(interpreted_string_literal) @string + +(string_interpolation) @none + +; Types + +(struct_declaration + name: (identifier) @type) + +(enum_declaration + name: (identifier) @type) + +(interface_declaration + name: (identifier) @type) + +(type_declaration + name: (identifier) @type) + +(type_reference_expression (identifier) @type) + +; Labels + +(label_reference) @label + +; Fields + +(selector_expression field: (reference_expression (identifier) @field)) + +(field_name) @field + +(struct_field_declaration + name: (identifier) @field) + +; Parameters + +(parameter_declaration + name: (identifier) @parameter) + +(receiver + name: (identifier) @parameter) + +; Constants + +((identifier) @constant + (#has-ancestor? @constant compile_time_if_expression)) + +(enum_fetch + (reference_expression) @constant) + +(enum_field_definition + (identifier) @constant) + +(const_definition + name: (identifier) @constant) + +((identifier) @variable.builtin + (#any-of? @variable.builtin "err" "macos" "linux" "windows")) + +; Attributes + +(attribute) @attribute + +; Functions + +(function_declaration + name: (identifier) @function) + +(function_declaration + receiver: (receiver) + name: (identifier) @method) + +(call_expression + name: (selector_expression + field: (reference_expression) @method.call)) + +(call_expression + name: (reference_expression) @function.call) + +((identifier) @function.builtin + (#any-of? @function.builtin + "eprint" + "eprintln" + "error" + "exit" + "panic" + "print" + "println" + "after" + "after_char" + "all" + "all_after" + "all_after_last" + "all_before" + "all_before_last" + "any" + "ascii_str" + "before" + "bool" + "byte" + "byterune" + "bytes" + "bytestr" + "c_error_number_str" + "capitalize" + "clear" + "clone" + "clone_to_depth" + "close" + "code" + "compare" + "compare_strings" + "contains" + "contains_any" + "contains_any_substr" + "copy" + "count" + "cstring_to_vstring" + "delete" + "delete_last" + "delete_many" + "ends_with" + "eprint" + "eprintln" + "eq_epsilon" + "error" + "error_with_code" + "exit" + "f32" + "f32_abs" + "f32_max" + "f32_min" + "f64" + "f64_max" + "fields" + "filter" + "find_between" + "first" + "flush_stderr" + "flush_stdout" + "free" + "gc_check_leaks" + "get_str_intp_u32_format" + "get_str_intp_u64_format" + "grow_cap" + "grow_len" + "hash" + "hex" + "hex2" + "hex_full" + "i16" + "i64" + "i8" + "index" + "index_after" + "index_any" + "index_byte" + "insert" + "int" + "is_alnum" + "is_bin_digit" + "is_capital" + "is_digit" + "is_hex_digit" + "is_letter" + "is_lower" + "is_oct_digit" + "is_space" + "is_title" + "is_upper" + "isnil" + "join" + "join_lines" + "keys" + "last" + "last_index" + "last_index_byte" + "length_in_bytes" + "limit" + "malloc" + "malloc_noscan" + "map" + "match_glob" + "memdup" + "memdup_noscan" + "move" + "msg" + "panic" + "panic_error_number" + "panic_lasterr" + "panic_optional_not_set" + "parse_int" + "parse_uint" + "pointers" + "pop" + "prepend" + "print" + "print_backtrace" + "println" + "proc_pidpath" + "ptr_str" + "push_many" + "realloc_data" + "reduce" + "repeat" + "repeat_to_depth" + "replace" + "replace_each" + "replace_once" + "reverse" + "reverse_in_place" + "runes" + "sort" + "sort_by_len" + "sort_ignore_case" + "sort_with_compare" + "split" + "split_any" + "split_into_lines" + "split_nth" + "starts_with" + "starts_with_capital" + "str" + "str_escaped" + "str_intp" + "str_intp_g32" + "str_intp_g64" + "str_intp_rune" + "str_intp_sq" + "str_intp_sub" + "strg" + "string_from_wide" + "string_from_wide2" + "strip_margin" + "strip_margin_custom" + "strlong" + "strsci" + "substr" + "substr_ni" + "substr_with_check" + "title" + "to_lower" + "to_upper" + "to_wide" + "tos" + "tos2" + "tos3" + "tos4" + "tos5" + "tos_clone" + "trim" + "trim_left" + "trim_pr" + "try_pop" + "try_push" + "utf32_decode_to_buffer" + "utf32_to_str" + "utf32_to_str_no_malloc" + "utf8_char_len" + "utf8_getchar" + "utf8_str_len" + "utf8_str_visible_length" + "utf8_to_utf32" + "v_realloc" + "vbytes" + "vcalloc" + "vcalloc_noscan" + "vmemcmp" + "vmemcpy" + "vmemmove" + "vmemset" + "vstring" + "vstring_literal" + "vstring_literal_with_len" + "vstring_with_len" + "vstrlen" + "vstrlen_char" + "winapi_lasterr_str")) + +; Operators + +[ + "++" + "--" + + "+" + "-" + "*" + "/" + "%" + + "~" + "&" + "|" + "^" + + "!" + "&&" + "||" + "!=" + + "<<" + ">>" + + "<" + ">" + "<=" + ">=" + + "+=" + "-=" + "*=" + "/=" + "&=" + "|=" + "^=" + "<<=" + ">>=" + + "=" + ":=" + "==" + + "?" + "<-" + "$" + ".." + "..." +] @operator + +; Punctuation + +[ "." "," ":" ";" ] @punctuation.delimiter + +[ "(" ")" "{" "}" "[" "]" ] @punctuation.bracket + +; Literals + +(int_literal) @number + +(float_literal) @float + +[ + (c_string_literal) + (raw_string_literal) + (interpreted_string_literal) + (string_interpolation) + (rune_literal) +] @string + +(string_interpolation + (braced_interpolation_opening) @punctuation.bracket + (interpolated_expression) @none + (braced_interpolation_closing) @punctuation.bracket) + +(escape_sequence) @string.escape + +[ + (true) + (false) +] @boolean + +(nil) @constant.builtin + +(none) @variable.builtin + +; Comments + +(comment) @comment @spell + +(_ + (comment)+ @comment.documentation + [(function_declaration) (type_declaration) (enum_declaration)]) + +; Errors + +(ERROR) @error diff --git a/tree-sitter/highlights/vala.scm b/tree-sitter/highlights/vala.scm new file mode 100644 index 0000000000..875bad539d --- /dev/null +++ b/tree-sitter/highlights/vala.scm @@ -0,0 +1,250 @@ +; highlights.scm + +; highlight comments and symbols +(comment) @comment @spell +((comment) @comment.documentation + (#lua-match? @comment.documentation "^/[*][*][^*].*[*]/$")) +(symbol) @symbol +(member_access_expression (_) (identifier) @symbol) + +; highlight constants +( + (member_access_expression (identifier) @constant) + (#lua-match? @constant "^[%u][%u%d_]*$") +) + +( + (member_access_expression (member_access_expression) @include (identifier) @constant) + (#lua-match? @constant "^[%u][%u%d_]*$") +) + +; highlight types and probable types +(type (symbol (_)? @namespace (identifier) @type)) +( + (member_access_expression . (identifier) @type) + (#match? @type "^[A-Z][A-Za-z_0-9]{2,}$") +) + +; highlight creation methods in object creation expressions +( + (object_creation_expression (type (symbol (symbol (symbol)? @include (identifier) @type) (identifier) @constructor))) + (#lua-match? @constructor "^[%l][%l%d_]*$") +) + +(unqualified_type (symbol . (identifier) @type)) +(unqualified_type (symbol (symbol) @namespace (identifier) @type)) + +(attribute) @attribute +(namespace_declaration (symbol) @namespace) +(method_declaration (symbol (symbol) @type (identifier) @function)) +(method_declaration (symbol (identifier) @function)) +(local_declaration (assignment (identifier) @variable)) +(local_function_declaration (identifier) @function) +(destructor_declaration (identifier) @function) +(creation_method_declaration (symbol (symbol) @type (identifier) @constructor)) +(creation_method_declaration (symbol (identifier) @constructor)) +(constructor_declaration (_)? "construct" @keyword.function) +(enum_declaration (symbol) @type) +(enum_value (identifier) @constant) +(errordomain_declaration (symbol) @type) +(errorcode (identifier) @constant) +(constant_declaration (identifier) @constant) +(method_call_expression (member_access_expression (identifier) @function)) +; highlight macros +( + (method_call_expression (member_access_expression (identifier) @function.macro)) + (#match? @function.macro "^assert[A-Za-z_0-9]*|error|info|debug|print|warning|warning_once$") +) +(lambda_expression (identifier) @parameter) +(parameter (identifier) @parameter) +(property_declaration (symbol (identifier) @property)) +(field_declaration (identifier) @field) +[ + (this_access) + (base_access) + (value_access) +] @constant.builtin +(boolean) @boolean +(character) @character +(escape_sequence) @string.escape +(integer) @number +(null) @constant.builtin +(real) @float +(regex) @string.regex +(string) @string +(string_formatter) @string.special +(template_string) @string +(template_string_expression) @string.special +(verbatim_string) @string +[ + "var" + "void" +] @type.builtin + +(if_directive + expression: (_) @preproc +) @keyword +(elif_directive + expression: (_) @preproc +) @keyword +(else_directive) @keyword +(endif_directive) @keyword + +[ + "abstract" + "class" + "construct" + "continue" + "default" + "delegate" + "enum" + "errordomain" + "get" + "inline" + "interface" + "namespace" + "new" + "out" + "override" + "partial" + "ref" + "set" + "signal" + "struct" + "virtual" + "with" +] @keyword + +[ + "async" + "yield" +] @keyword.coroutine + +[ + "const" + "dynamic" + "owned" + "weak" + "unowned" +] @type.qualifier + +[ + "case" + "else" + "if" + "switch" +] @conditional + +; specially highlight break statements in switch sections +(switch_section (break_statement "break" @conditional)) + +[ + "extern" + "internal" + "private" + "protected" + "public" + "static" +] @storageclass + +[ + "and" + "as" + "delete" + "in" + "is" + "lock" + "not" + "or" + "sizeof" + "typeof" +] @keyword.operator + +"using" @include +(using_directive (symbol) @namespace) + +(symbol "global::" @namespace) + +(array_creation_expression "new" @keyword.operator) +(object_creation_expression "new" @keyword.operator) +(argument "out" @keyword.operator) +(argument "ref" @keyword.operator) + +[ + "break" + "continue" + "do" + "for" + "foreach" + "while" +] @repeat + +[ + "catch" + "finally" + "throw" + "throws" + "try" +] @exception + +[ + "return" +] @keyword.return + +[ + "=" + "==" + "+" + "+=" + "-" + "-=" + "++" + "--" + "|" + "|=" + "&" + "&=" + "^" + "^=" + "/" + "/=" + "*" + "*=" + "%" + "%=" + "<<" + "<<=" + ">>" + ">>=" + "." + "?." + "->" + "!" + "!=" + "~" + "??" + "?" + ":" + "<" + "<=" + ">" + ">=" + "||" + "&&" + "=>" +] @operator + +[ + "," + ";" +] @punctuation.delimiter + +[ + "$(" + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket diff --git a/tree-sitter/highlights/verilog.scm b/tree-sitter/highlights/verilog.scm new file mode 100644 index 0000000000..373f912184 --- /dev/null +++ b/tree-sitter/highlights/verilog.scm @@ -0,0 +1,315 @@ +; Keywords + +[ + ; block delimiters + (module_keyword) + "endmodule" + "program" + "endprogram" + "class" + "endclass" + "interface" + "endinterface" + "package" + "endpackage" + "checker" + "endchecker" + "config" + "endconfig" + + "pure" + "virtual" + "extends" + "implements" + "super" + (class_item_qualifier) + + "parameter" + "localparam" + "defparam" + "assign" + "typedef" + "modport" + "fork" + "join" + "join_none" + "join_any" + "default" + "break" + "assert" + "tagged" + "extern" + (unique_priority) +] @keyword + +[ + "function" + "endfunction" + + "task" + "endtask" +] @keyword.function + +"return" @keyword.return + +[ + "begin" + "end" +] @label + +[ + (always_keyword) + "generate" + "for" + "foreach" + "repeat" + "forever" + "initial" + "while" +] @repeat + +[ + "if" + "else" + (case_keyword) + "endcase" +] @conditional + +(comment) @comment @spell + +(include_compiler_directive) @constant.macro +(package_import_declaration + "import" @include) + +(package_import_declaration + (package_import_item + (package_identifier + (simple_identifier) @constant))) + +(text_macro_identifier + (simple_identifier) @constant.macro) + +(package_scope + (package_identifier + (simple_identifier) @constant)) + +(package_declaration + (package_identifier + (simple_identifier) @constant)) + +(parameter_port_list + "#" @constructor) + +[ + "=" + "-" + "+" + "/" + "*" + "^" + "&" + "|" + "&&" + "||" + ":" + "{" + "}" + "'{" + "<=" + "@" + "==" + "!=" + "===" + "!==" + "-:" + "<" + ">" + ">=" + "%" + ">>" + "<<" + "|=" + (unary_operator) + (inc_or_dec_operator) +] @operator + +[ + "or" + "and" +] @keyword.operator + +(cast + ["'" "(" ")"] @operator) + +(edge_identifier) @attribute + +(port_direction) @label +(port_identifier + (simple_identifier) @variable) + +[ + (net_type) + (integer_vector_type) + (integer_atom_type) +] @type.builtin + +[ + "signed" + "unsigned" +] @type.qualifier + +(data_type + (simple_identifier) @type) + +(method_call_body + (method_identifier) @field) + +(interface_identifier + (simple_identifier) @type) + +(modport_identifier + (modport_identifier + (simple_identifier) @field)) + +(net_port_type1 + (simple_identifier) @type) + +[ + (double_quoted_string) + (string_literal) +] @string @spell + +[ + (default_nettype_compiler_directive) + (timescale_compiler_directive) +] @preproc + +(include_compiler_directive) @include + +; begin/end label +(seq_block + (simple_identifier) @comment) + +[ + ";" + "::" + "," + "." +] @punctuation.delimiter + + +(default_nettype_compiler_directive + (default_nettype_value) @string) + +(text_macro_identifier + (simple_identifier) @constant) + +(module_declaration + (module_header + (simple_identifier) @constructor)) + +(class_constructor_declaration + "new" @constructor) + +(parameter_identifier + (simple_identifier) @parameter) + +[ + (integral_number) + (unsigned_number) + (unbased_unsized_literal) +] @number + +(time_unit) @attribute + +(checker_instantiation + (checker_identifier + (simple_identifier) @constructor)) + +(module_instantiation + (simple_identifier) @constructor) + +(name_of_instance + (instance_identifier + (simple_identifier) @variable)) + +(interface_port_declaration + (interface_identifier + (simple_identifier) @type)) + +(net_declaration + (simple_identifier) @type) + +(lifetime) @label + +(function_identifier + (function_identifier + (simple_identifier) @function)) + +(function_subroutine_call + (subroutine_call + (tf_call + (simple_identifier) @function))) + +(function_subroutine_call + (subroutine_call + (system_tf_call + (system_tf_identifier) @function.builtin))) + +(task_identifier + (task_identifier + (simple_identifier) @method)) + +;;TODO: fixme +;(assignment_pattern_expression + ;(assignment_pattern + ;(parameter_identifier) @field)) + +(type_declaration + (data_type ["packed"] @type.qualifier)) + +(struct_union) @type + +[ + "enum" +] @type + +(enum_name_declaration + (enum_identifier + (simple_identifier) @constant)) + +(type_declaration + (simple_identifier) @type) + +[ + (integer_atom_type) + (non_integer_type) + "genvar" +] @type.builtin + +(struct_union_member + (list_of_variable_decl_assignments + (variable_decl_assignment + (simple_identifier) @field))) + +(member_identifier + (simple_identifier) @field) + +(struct_union_member + (data_type_or_void + (data_type + (simple_identifier) @type))) + +(type_declaration + (simple_identifier) @type) + +(generate_block_identifier) @comment + +[ + "[" + "]" + "(" + ")" +] @punctuation.bracket + +(ERROR) @error diff --git a/tree-sitter/highlights/vhs.scm b/tree-sitter/highlights/vhs.scm new file mode 100644 index 0000000000..67fa3cf8ab --- /dev/null +++ b/tree-sitter/highlights/vhs.scm @@ -0,0 +1,38 @@ +[ + "Output" + "Backspace" + "Down" + "Enter" + "Escape" + "Left" + "Right" + "Space" + "Tab" + "Up" + "Set" + "Type" + "Sleep" + "Hide" + "Show" ] @keyword + +[ "Shell" + "FontFamily" + "FontSize" + "Framerate" + "PlaybackSpeed" + "Height" + "LetterSpacing" + "TypingSpeed" + "LineHeight" + "Padding" + "Theme" + "LoopOffset" + "Width" ] @type + +[ "@" ] @operator +(control) @function.macro +(float) @float +(integer) @number +(comment) @comment @spell +[(path) (string) (json)] @string +(time) @symbol diff --git a/tree-sitter/highlights/vim.scm b/tree-sitter/highlights/vim.scm new file mode 100644 index 0000000000..09188ddb68 --- /dev/null +++ b/tree-sitter/highlights/vim.scm @@ -0,0 +1,290 @@ +(identifier) @variable +((identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) + +;; Keywords + +[ + "if" + "else" + "elseif" + "endif" +] @conditional + +[ + "try" + "catch" + "finally" + "endtry" + "throw" +] @exception + +[ + "for" + "endfor" + "in" + "while" + "endwhile" + "break" + "continue" +] @repeat + +[ + "function" + "endfunction" +] @keyword.function + +;; Function related +(function_declaration name: (_) @function) +(call_expression function: (identifier) @function.call) +(call_expression function: (scoped_identifier (identifier) @function.call)) +(parameters (identifier) @parameter) +(default_parameter (identifier) @parameter) + +[ (bang) (spread) ] @punctuation.special + +[ (no_option) (inv_option) (default_option) (option_name) ] @variable.builtin +[ + (scope) + "a:" + "$" +] @namespace + +;; Commands and user defined commands + +[ + "let" + "unlet" + "const" + "call" + "execute" + "normal" + "set" + "setfiletype" + "setlocal" + "silent" + "echo" + "echon" + "echohl" + "echomsg" + "echoerr" + "autocmd" + "augroup" + "return" + "syntax" + "filetype" + "source" + "lua" + "ruby" + "perl" + "python" + "highlight" + "command" + "delcommand" + "comclear" + "colorscheme" + "startinsert" + "stopinsert" + "global" + "runtime" + "wincmd" + "cnext" + "cprevious" + "cNext" + "vertical" + "leftabove" + "aboveleft" + "rightbelow" + "belowright" + "topleft" + "botright" + (unknown_command_name) + "edit" + "enew" + "find" + "ex" + "visual" + "view" + "eval" +] @keyword +(map_statement cmd: _ @keyword) +(command_name) @function.macro + +;; Filetype command + +(filetype_statement [ + "detect" + "plugin" + "indent" + "on" + "off" +] @keyword) + +;; Syntax command + +(syntax_statement (keyword) @string) +(syntax_statement [ + "enable" + "on" + "off" + "reset" + "case" + "spell" + "foldlevel" + "iskeyword" + "keyword" + "match" + "cluster" + "region" + "clear" + "include" +] @keyword) + +(syntax_argument name: _ @keyword) + +[ + "" + "" + "" + " + + + +""" + +JSON = """\ +{ + "name": "John Doe", + "age": 30, + "isStudent": false, + "address": { + "street": "123 Main St", + "city": "Anytown", + "state": "CA", + "zip": "12345" + }, + "phoneNumbers": [ + { + "type": "home", + "number": "555-555-1234" + }, + { + "type": "work", + "number": "555-555-5678" + } + ], + "hobbies": ["reading", "hiking", "swimming"], + "pets": [ + { + "type": "dog", + "name": "Fido" + }, + ], + "graduationYear": null +} + +""" + +REGEX = """\ +^abc # Matches any string that starts with "abc" +abc$ # Matches any string that ends with "abc" +^abc$ # Matches the string "abc" and nothing else +a.b # Matches any string containing "a", any character, then "b" +a[.]b # Matches the string "a.b" +a|b # Matches either "a" or "b" +a{2} # Matches "aa" +a{2,} # Matches two or more consecutive "a" characters +a{2,5} # Matches between 2 and 5 consecutive "a" characters +a? # Matches "a" or nothing (0 or 1 occurrence of "a") +a* # Matches zero or more consecutive "a" characters +a+ # Matches one or more consecutive "a" characters +\d # Matches any digit (equivalent to [0-9]) +\D # Matches any non-digit +\w # Matches any word character (equivalent to [a-zA-Z0-9_]) +\W # Matches any non-word character +\s # Matches any whitespace character (spaces, tabs, line breaks) +\S # Matches any non-whitespace character +(?i)abc # Case-insensitive match for "abc" +(?:a|b) # Non-capturing group for either "a" or "b" +(?<=a)b # Positive lookbehind: matches "b" that is preceded by "a" +(? None: + # super().__init__() + # self.language = language + # self.content = content + + def compose(self) -> ComposeResult: + yield TextArea() + + +app = TextAreaSnapshot() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 817eb64cf1..7aa5a82b3b 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -2,6 +2,10 @@ import pytest +from tests.snapshot_tests.language_snippets import SNIPPETS +from textual._languages import VALID_LANGUAGES +from textual.widgets import TextArea + # These paths should be relative to THIS directory. WIDGET_EXAMPLES_DIR = Path("../../docs/examples/widgets") LAYOUT_EXAMPLES_DIR = Path("../../docs/examples/guide/layout") @@ -610,3 +614,23 @@ def test_text_log_blank_write(snap_compare) -> None: def test_nested_fr(snap_compare) -> None: # https://github.com/Textualize/textual/pull/3059 assert snap_compare(SNAPSHOT_APPS_DIR / "nested_fr.py") + + +@pytest.mark.parametrize("language", VALID_LANGUAGES) +def test_text_area_language_rendering(language, snap_compare): + # This test will fail if we're missing a snapshot test for a valid + # language. We should have a snapshot test for each language we support + # as the syntax highlighting will be completely different for each of them. + + snippet = SNIPPETS.get(language) + + def setup_language(pilot) -> None: + text_area = pilot.app.query_one(TextArea) + text_area.load_text(snippet) + text_area.language = language + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area_languages.py", + run_before=setup_language, + terminal_size=(80, snippet.count("\n") + 2), + ) From d26ed876b7149080767556c6297a1ee4f576c813 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 8 Aug 2023 16:28:18 +0100 Subject: [PATCH 152/366] Updating snapshots for TextArea since we now highlight more nodes --- src/textual/_syntax_theme.py | 6 +- src/textual/widgets/_text_area.py | 3 +- .../__snapshots__/test_snapshots.ambr | 350 +++++++++--------- 3 files changed, 181 insertions(+), 178 deletions(-) diff --git a/src/textual/_syntax_theme.py b/src/textual/_syntax_theme.py index 0eb5f039a3..cd59cf3a55 100644 --- a/src/textual/_syntax_theme.py +++ b/src/textual/_syntax_theme.py @@ -6,9 +6,11 @@ _MONOKAI = { "string": Style(color="#E6DB74"), - "string.documentation": Style(color="yellow"), + "string.documentation": Style(color="#E6DB74"), "comment": Style(color="#75715E"), "keyword": Style(color="#F92672"), + "repeat": Style(color="#F92672"), + "exception": Style(color="#F92672"), "include": Style(color="#F92672"), "keyword.function": Style(color="#F92672"), "keyword.return": Style(color="#F92672"), @@ -23,7 +25,7 @@ "variable": Style(color="white"), "parameter": Style(color="cyan"), # "type": Style(color="cyan"), - "escape": Style(bgcolor="magenta"), + # "escape": Style(bgcolor="magenta"), "heading": Style(color="#F92672", bold=True), } diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index cf711b2614..0194afeb61 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: from tree_sitter import Language -from textual import events +from textual import events, log from textual._cells import cell_len from textual._fix_direction import _fix_direction from textual._syntax_theme import DEFAULT_SYNTAX_THEME, SyntaxTheme @@ -296,6 +296,7 @@ def _reload_document(self) -> None: except ImportError: # SyntaxAwareDocument isn't available on Python 3.7. # Fall back to the standard document. + log.warning("Syntax highlighting isn't available on Python 3.7.") self._document = Document(text) def load_text(self, text: str) -> None: diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 8526334f47..1336cb443d 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -28028,363 +28028,363 @@ font-weight: 700; } - .terminal-2831883283-matrix { + .terminal-1571808136-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2831883283-title { + .terminal-1571808136-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2831883283-r1 { fill: #e4e5e6 } - .terminal-2831883283-r2 { fill: #1b1b1b } - .terminal-2831883283-r3 { fill: #f92672 } - .terminal-2831883283-r4 { fill: #c5c8c6 } - .terminal-2831883283-r5 { fill: #7b7e82 } - .terminal-2831883283-r6 { fill: #75715e } - .terminal-2831883283-r7 { fill: #e6db74 } - .terminal-2831883283-r8 { fill: #ae81ff } - .terminal-2831883283-r9 { fill: #a6e22e } - .terminal-2831883283-r10 { fill: #68a0b3 } + .terminal-1571808136-r1 { fill: #e4e5e6 } + .terminal-1571808136-r2 { fill: #1b1b1b } + .terminal-1571808136-r3 { fill: #f92672 } + .terminal-1571808136-r4 { fill: #c5c8c6 } + .terminal-1571808136-r5 { fill: #7b7e82 } + .terminal-1571808136-r6 { fill: #75715e } + .terminal-1571808136-r7 { fill: #e6db74 } + .terminal-1571808136-r8 { fill: #ae81ff } + .terminal-1571808136-r9 { fill: #a6e22e } + .terminal-1571808136-r10 { fill: #68a0b3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  importmath -  2  fromosimportpath -  3   -  4  # I'm a comment :) -  5   -  6  string_var = "Hello, world!" -  7  int_var = 42 -  8  float_var = 3.14                                                             -  9  complex_var = 1 + 2j - 10   - 11  list_var = [12345]                                                   - 12  tuple_var = (12345)                                                  - 13  set_var = {12345}                                                    - 14  dict_var = {"a"1"b"2"c"3}                                          - 15   - 16  deffunction_no_args():                                                      - 17  return"No arguments" - 18   - 19  deffunction_with_args(ab):                                                - 20  returna + b - 21   - 22  deffunction_with_default_args(a=0b=0):                                    - 23  returna * b - 24   - 25  lambda_func = lambdaxx**2 - 26   - 27  ifint_var == 42:                                                            - 28  print("It's the answer!")                                                - 29  elifint_var < 42:                                                           - 30  print("Less than the answer.")                                           - 31  else:                                                                        - 32  print("Greater than the answer.")                                        - 33   - 34  for indexvalue in enumerate(list_var):                                     - 35  print(f"Index: {index}, Value: {value}")                                 - 36   - 37  counter = 0 - 38  while counter < 5:                                                           - 39  print(f"Counter value: {counter}")                                       - 40  counter += 1 - 41   - 42  squared_numbers = [x**2 for x in range(10ifx % 2 == 0]                    - 43   - 44  try:                                                                         - 45  result = 10 / 0 - 46  except ZeroDivisionError:                                                    - 47  print("Cannot divide by zero!")                                          - 48  finally:                                                                     - 49  print("End of try-except block.")                                        - 50   - 51  classAnimal:                                                                - 52  def__init__(selfname):                                                - 53  self.name = name - 54   - 55  defspeak(self):                                                         - 56          raise NotImplementedError("Subclasses must implement this method." - 57   - 58  classDog(Animal):                                                           - 59  defspeak(self):                                                         - 60  returnf"{self.name} says Woof!" - 61   - 62  deffibonacci(n):                                                            - 63  ab = 01 - 64      for _ in range(n):                                                       - 65  yielda - 66  ab = ba + b - 67   - 68  for num in fibonacci(5):                                                     - 69  print(num)                                                               - 70   - 71  withopen('test.txt''w'asf:                                             - 72  f.write("Testing with statement.")                                       - 73   - 74  @my_decorator - 75  defsay_hello():                                                             - 76  print("Hello!")                                                          - 77   - 78  say_hello()                                                                  - 79   + + + +  1  importmath +  2  fromosimportpath +  3   +  4  # I'm a comment :) +  5   +  6  string_var = "Hello, world!" +  7  int_var = 42 +  8  float_var = 3.14                                                             +  9  complex_var = 1 + 2j + 10   + 11  list_var = [12345]                                                   + 12  tuple_var = (12345)                                                  + 13  set_var = {12345}                                                    + 14  dict_var = {"a"1"b"2"c"3}                                          + 15   + 16  deffunction_no_args():                                                      + 17  return"No arguments" + 18   + 19  deffunction_with_args(ab):                                                + 20  returna + b + 21   + 22  deffunction_with_default_args(a=0b=0):                                    + 23  returna * b + 24   + 25  lambda_func = lambdaxx**2 + 26   + 27  ifint_var == 42:                                                            + 28  print("It's the answer!")                                                + 29  elifint_var < 42:                                                           + 30  print("Less than the answer.")                                           + 31  else:                                                                        + 32  print("Greater than the answer.")                                        + 33   + 34  forindexvalue in enumerate(list_var):                                     + 35  print(f"Index: {index}, Value: {value}")                                 + 36   + 37  counter = 0 + 38  whilecounter < 5:                                                           + 39  print(f"Counter value: {counter}")                                       + 40  counter += 1 + 41   + 42  squared_numbers = [x**2forx in range(10ifx % 2 == 0]                    + 43   + 44  try:                                                                         + 45  result = 10 / 0 + 46  exceptZeroDivisionError:                                                    + 47  print("Cannot divide by zero!")                                          + 48  finally:                                                                     + 49  print("End of try-except block.")                                        + 50   + 51  classAnimal:                                                                + 52  def__init__(selfname):                                                + 53  self.name = name + 54   + 55  defspeak(self):                                                         + 56  raiseNotImplementedError("Subclasses must implement this method." + 57   + 58  classDog(Animal):                                                           + 59  defspeak(self):                                                         + 60  returnf"{self.name} says Woof!" + 61   + 62  deffibonacci(n):                                                            + 63  ab = 01 + 64  for_ in range(n):                                                       + 65  yielda + 66  ab = ba + b + 67   + 68  fornum in fibonacci(5):                                                     + 69  print(num)                                                               + 70   + 71  withopen('test.txt''w'asf:                                             + 72  f.write("Testing with statement.")                                       + 73   + 74  @my_decorator + 75  defsay_hello():                                                             + 76  print("Hello!")                                                          + 77   + 78  say_hello()                                                                  + 79   From 99808047cc2c26c85d0c166465a48fd8754dee53 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 8 Aug 2023 16:49:37 +0100 Subject: [PATCH 153/366] Typing fixes --- .../document/_syntax_aware_document.py | 19 +++++++++---------- src/textual/widgets/_text_area.py | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 014bf2fe9b..e69ef0db79 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -52,6 +52,8 @@ def __init__( if TREE_SITTER: if isinstance(language, str): + if self._language.name not in VALID_LANGUAGES: + raise RuntimeError(f"Invalid language {language!r}") self._language = get_language(language) self._parser = get_parser(language) else: @@ -59,18 +61,15 @@ def __init__( self._parser = Parser() self._parser.set_language(language) - if self._language.name in VALID_LANGUAGES: - highlight_query_path = ( - Path(HIGHLIGHTS_PATH.resolve()) / f"{language}.scm" - ) - if isinstance(syntax_theme, SyntaxTheme): - self._syntax_theme = syntax_theme - else: - self._syntax_theme = SyntaxTheme.get_theme(syntax_theme) + highlight_query_path = ( + Path(HIGHLIGHTS_PATH.resolve()) / f"{self._language.name}.scm" + ) + if isinstance(syntax_theme, SyntaxTheme): + self._syntax_theme = syntax_theme + else: + self._syntax_theme = SyntaxTheme.get_theme(syntax_theme) self._syntax_theme.highlight_query = highlight_query_path.read_text() - else: - raise RuntimeError(f"Invalid language {language!r}") self._syntax_tree = self._build_ast(self._parser) self._prepare_highlights() diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 0194afeb61..0f2b8fd138 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -483,7 +483,7 @@ def edit(self, edit: Edit) -> Any: # action.undo(self) # --- Lower level event/key handling - def _on_key(self, event: events.Key) -> None: + async def _on_key(self, event: events.Key) -> None: """Handle key presses which correspond to document inserts.""" key = event.key insert_values = { From ca34b5503e9fac294ae1bdfd0c96edeb7f50617d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 8 Aug 2023 17:10:32 +0100 Subject: [PATCH 154/366] Testing --- .../snapshot_apps/text_area_languages.py | 5 -- tests/text_area/test_selection.py | 70 +++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 tests/text_area/test_selection.py diff --git a/tests/snapshot_tests/snapshot_apps/text_area_languages.py b/tests/snapshot_tests/snapshot_apps/text_area_languages.py index 38f77f1f49..3e6f05ab8d 100644 --- a/tests/snapshot_tests/snapshot_apps/text_area_languages.py +++ b/tests/snapshot_tests/snapshot_apps/text_area_languages.py @@ -4,11 +4,6 @@ class TextAreaSnapshot(App): - # def __init__(self, language: str, content: str) -> None: - # super().__init__() - # self.language = language - # self.content = content - def compose(self) -> ComposeResult: yield TextArea() diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py new file mode 100644 index 0000000000..6231de7e40 --- /dev/null +++ b/tests/text_area/test_selection.py @@ -0,0 +1,70 @@ +import pytest + +from textual.app import App, ComposeResult +from textual.document._document import Selection +from textual.widgets import TextArea + +TEXT = """I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.load_text(TEXT) + yield text_area + + +def test_default_selection(): + text_area = TextArea() + assert text_area.selection == Selection.cursor((0, 0)) + + +# def test_selection_modified(): +# app = TextAreaApp() +# async with app.run_test(): +# text_area = app.query_one(TextArea) + + +async def test_selected_text_forward(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((0, 0), (2, 0)) + assert ( + text_area.selected_text + == """\ +I must not fear. +Fear is the mind-killer. +""" + ) + + +async def test_selected_text_backward(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((2, 0), (0, 0)) + assert ( + text_area.selected_text + == """\ +I must not fear. +Fear is the mind-killer. +""" + ) + + +async def test_selection_clamp(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((99, 99), (100, 100)) + assert text_area.selection == Selection(start=(4, 0), end=(4, 0)) + + +async def test_mouse_selection(): + # TODO: Wednesday. + pass From d209d852af883171955a42690c323e3496f58f7f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 9 Aug 2023 10:27:09 +0100 Subject: [PATCH 155/366] Adding tests --- tests/text_area/test_selection.py | 85 +++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 11 deletions(-) diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 6231de7e40..75f5a405da 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -1,7 +1,6 @@ -import pytest - from textual.app import App, ComposeResult from textual.document._document import Selection +from textual.geometry import Offset from textual.widgets import TextArea TEXT = """I must not fear. @@ -19,17 +18,13 @@ def compose(self) -> ComposeResult: def test_default_selection(): + """The cursor starts at (0, 0) in the document.""" text_area = TextArea() assert text_area.selection == Selection.cursor((0, 0)) -# def test_selection_modified(): -# app = TextAreaApp() -# async with app.run_test(): -# text_area = app.query_one(TextArea) - - async def test_selected_text_forward(): + """Selecting text from top to bottom results in the correct selected_text.""" app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) @@ -44,6 +39,7 @@ async def test_selected_text_forward(): async def test_selected_text_backward(): + """Selecting text from bottom to top results in the correct selected_text.""" app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) @@ -58,6 +54,7 @@ async def test_selected_text_backward(): async def test_selection_clamp(): + """When you set the selection reactive, it's clamped to within the document bounds.""" app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) @@ -65,6 +62,72 @@ async def test_selection_clamp(): assert text_area.selection == Selection(start=(4, 0), end=(4, 0)) -async def test_mouse_selection(): - # TODO: Wednesday. - pass +async def test_mouse_click(): + """When you click the TextArea, the cursor moves to the expected location.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(TextArea, Offset(x=5, y=2)) + assert text_area.selection == Selection.cursor((2, 2)) + + +async def test_mouse_click_clamp_from_right(): + """When you click to the right of the document bounds, the cursor is clamped + to within the document bounds.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(TextArea, Offset(x=8, y=20)) + assert text_area.selection == Selection.cursor((4, 0)) + + +async def test_mouse_click_gutter_clamp(): + """When you click the gutter, it selects the start of the line.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(TextArea, Offset(x=0, y=3)) + assert text_area.selection == Selection.cursor((3, 0)) + + +async def test_cursor_selection_right(): + """When you press shift+right the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.press(*["shift+right"] * 3) + assert text_area.selection == Selection((0, 0), (0, 3)) + + +async def test_cursor_selection_right(): + """When you press shift+right resulting in the cursor moving to the next line, + the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.press(*["shift+right"] * 3) + assert text_area.selection == Selection((0, 0), (0, 3)) + + +async def test_cursor_selection_left(): + """When you press shift+left the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 5)) + await pilot.press(*["shift+left"] * 3) + assert text_area.selection == Selection((2, 5), (2, 2)) + + +async def test_cursor_selection_left_to_previous_line(): + """When you press shift+left resulting in the cursor moving back to the previous line, + the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 2)) + await pilot.press(*["shift+left"] * 3) + + # The cursor jumps up to the end of the line above. + end_of_previous_line = len(TEXT.splitlines()[1]) + assert text_area.selection == Selection((2, 2), (1, end_of_previous_line)) From 19628dc0337841f8cf6ec9cceaf93890e30a244a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 9 Aug 2023 11:08:16 +0100 Subject: [PATCH 156/366] Fixing language selection --- src/textual/document/_syntax_aware_document.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index e69ef0db79..46ee498c5a 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -52,7 +52,7 @@ def __init__( if TREE_SITTER: if isinstance(language, str): - if self._language.name not in VALID_LANGUAGES: + if language not in VALID_LANGUAGES: raise RuntimeError(f"Invalid language {language!r}") self._language = get_language(language) self._parser = get_parser(language) @@ -69,8 +69,7 @@ def __init__( else: self._syntax_theme = SyntaxTheme.get_theme(syntax_theme) - self._syntax_theme.highlight_query = highlight_query_path.read_text() - + self._syntax_theme.highlight_query = highlight_query_path.read_text() self._syntax_tree = self._build_ast(self._parser) self._prepare_highlights() From 3d960bc7ee646dc2836a5d7f42c6cac16c405b70 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 9 Aug 2023 11:11:45 +0100 Subject: [PATCH 157/366] Refresh size on indent width change --- src/textual/widgets/_text_area.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 0f2b8fd138..47a56683f8 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -281,6 +281,10 @@ def _watch_show_line_numbers(self) -> None: """The line number gutter contributes to virtual size, so recalculate.""" self._refresh_size() + def _watch_indent_width(self) -> None: + """Changing width of tabs will change document display width.""" + self._refresh_size() + def _reload_document(self) -> None: """Recreate the document based on the language and theme currently set.""" language = self.language From 2345be89a8393a469d045eafcb773c27159a5748 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 9 Aug 2023 16:51:11 +0100 Subject: [PATCH 158/366] Testing, renaming, fixing display of selection --- examples/code_browser.css | 19 +++++++------- src/textual/_syntax_theme.py | 2 ++ src/textual/widgets/_text_area.py | 42 +++++++++++++++++++------------ tests/text_area/test_selection.py | 7 +++--- tree-sitter/highlights/regex.scm | 4 +-- 5 files changed, 44 insertions(+), 30 deletions(-) diff --git a/examples/code_browser.css b/examples/code_browser.css index 9a2c295c95..2cdf6ca533 100644 --- a/examples/code_browser.css +++ b/examples/code_browser.css @@ -3,24 +3,25 @@ Screen { } #tree-view { - display: none; + color: red; + display: none; scrollbar-gutter: stable; - overflow: auto; - width: auto; - height: 100%; - dock: left; + overflow: auto; + width: auto; + height: 100%; + dock: left; } CodeBrowser.-show-tree #tree-view { - display: block; + display: block; max-width: 50%; } #code-view { - overflow: auto scroll; - min-width: 100%; + overflow: auto scroll; + min-width: 100%; } #code { - width: auto; + width: auto; } diff --git a/src/textual/_syntax_theme.py b/src/textual/_syntax_theme.py index cd59cf3a55..8f7746b852 100644 --- a/src/textual/_syntax_theme.py +++ b/src/textual/_syntax_theme.py @@ -27,6 +27,8 @@ # "type": Style(color="cyan"), # "escape": Style(bgcolor="magenta"), "heading": Style(color="#F92672", bold=True), + "regex.punctuation.bracket": Style(color="#F92672"), + "regex.operator": Style(color="#F92672"), } BUILTIN_THEMES = { diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 47a56683f8..70fde1b283 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -4,6 +4,8 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, ClassVar +from rich.segment import Segment +from rich.style import Style from rich.text import Text if TYPE_CHECKING: @@ -190,7 +192,7 @@ class TextArea(ScrollView, can_focus=True): Binding( "ctrl+w", "delete_word_left", "delete left to start of word", show=False ), - Binding("ctrl+d", "delete_right", "delete right", show=False), + Binding("delete,ctrl+d", "delete_right", "delete right", show=False), Binding( "ctrl+f", "delete_word_right", "delete right to start of word", show=False ), @@ -341,28 +343,35 @@ def render_line(self, widget_y: int) -> Strip: selection_top_row, selection_top_column = selection_top selection_bottom_row, selection_bottom_column = selection_bottom + # Selection styling if start != end and selection_top_row <= line_index <= selection_bottom_row: # If this row intersects with the selection range selection_style = self.get_component_rich_style("text-area--selection") - if line_index == selection_top_row == selection_bottom_row: - # Selection within a single line - line.stylize_before( - selection_style, - start=selection_top_column, - end=selection_bottom_column, - ) + if line_character_count == 0 and line_index != end: + # A simple highlight to show empty lines are included in the selection + line = Text("▌", end="", style=Style(color=selection_style.bgcolor)) else: - # Selection spanning multiple lines - if line_index == selection_top_row: + if line_index == selection_top_row == selection_bottom_row: + # Selection within a single line line.stylize_before( selection_style, start=selection_top_column, - end=line_character_count, + end=selection_bottom_column, ) - elif line_index == selection_bottom_row: - line.stylize_before(selection_style, end=selection_bottom_column) else: - line.stylize_before(selection_style, end=line_character_count) + # Selection spanning multiple lines + if line_index == selection_top_row: + line.stylize_before( + selection_style, + start=selection_top_column, + end=line_character_count, + ) + elif line_index == selection_bottom_row: + line.stylize_before( + selection_style, end=selection_bottom_column + ) + else: + line.stylize_before(selection_style, end=line_character_count) virtual_width, virtual_height = self.virtual_size @@ -400,7 +409,8 @@ def render_line(self, widget_y: int) -> Strip: # Render the gutter and the text of this line gutter_segments = self.app.console.render(gutter) text_segments = self.app.console.render( - line, self.app.console.options.update_width(virtual_width) + line, + self.app.console.options.update_width(virtual_width), ) # Crop the line to show only the visible part (some may be scrolled out of view) @@ -418,7 +428,7 @@ def render_line(self, widget_y: int) -> Strip: # Join and return the gutter and the visible portion of this line strip = Strip.join([gutter_strip, text_strip]).simplify() - return strip + return strip.apply_style(self.rich_style) @property def text(self) -> str: diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 75f5a405da..84fb577b69 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -99,14 +99,15 @@ async def test_cursor_selection_right(): assert text_area.selection == Selection((0, 0), (0, 3)) -async def test_cursor_selection_right(): +async def test_cursor_selection_right_to_previous_line(): """When you press shift+right resulting in the cursor moving to the next line, the selection is updated correctly.""" app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) - await pilot.press(*["shift+right"] * 3) - assert text_area.selection == Selection((0, 0), (0, 3)) + text_area.selection = Selection.cursor((0, 15)) + await pilot.press(*["shift+right"] * 4) + assert text_area.selection == Selection((0, 15), (1, 2)) async def test_cursor_selection_left(): diff --git a/tree-sitter/highlights/regex.scm b/tree-sitter/highlights/regex.scm index bf0aa59342..7c671c2c04 100644 --- a/tree-sitter/highlights/regex.scm +++ b/tree-sitter/highlights/regex.scm @@ -11,7 +11,7 @@ "]" "{" "}" -] @punctuation.bracket +] @regex.punctuation.bracket (group_name) @property @@ -31,4 +31,4 @@ (non_boundary_assertion) ] @string.escape -[ "*" "+" "?" "|" "=" "!" ] @operator +[ "*" "+" "?" "|" "=" "!" ] @regex.operator From 6ae2c7e4a7cc645d9c88cda0176c37e036a42d78 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 10 Aug 2023 10:06:39 +0100 Subject: [PATCH 159/366] Fix multibyte highlight glitch --- src/textual/document/_syntax_aware_document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 46ee498c5a..c6cc17551a 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -273,6 +273,6 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No if row_out_of_bounds or column_out_of_bounds: return_value = None else: - return_value = row_text[column:].encode("utf8") + return_value = row_text[column].encode("utf8") return return_value From a9c4733d6034bc200aae197ebe945cd94085f06a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 10 Aug 2023 12:37:58 +0100 Subject: [PATCH 160/366] Fix deleting right with selection at end of document in TextArea --- .../document/_syntax_aware_document.py | 44 ++++++++++--------- src/textual/widgets/_text_area.py | 4 +- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index c6cc17551a..4b056a4690 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -135,17 +135,18 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: if TREE_SITTER: deleted_text_byte_length = len(deleted_text.encode("utf-8")) - self._syntax_tree.edit( - start_byte=start_byte, - old_end_byte=old_end_byte, - new_end_byte=old_end_byte - deleted_text_byte_length, - start_point=top, - old_end_point=bottom, - new_end_point=top, - ) - self._syntax_tree = self._parser.parse( - self._read_callable, self._syntax_tree - ) + edit_args = { + "start_byte": start_byte, + "old_end_byte": old_end_byte, + "new_end_byte": old_end_byte - deleted_text_byte_length, + "start_point": top, + "old_end_point": bottom, + "new_end_point": top, + } + print(f"edit = {edit_args!r}") + self._syntax_tree.edit(**edit_args) + new_tree = self._parser.parse(self._read_callable, self._syntax_tree) + self._syntax_tree = new_tree self._prepare_highlights() return deleted_text @@ -162,6 +163,10 @@ def get_line_text(self, line_index: int) -> Text: line = Text(self[line_index], end="") if self._highlights: highlights = self._highlights[line_index] + print(line_index) + print(line) + print(highlights) + print("---") for start, end, highlight_name in highlights: node_style = self._syntax_theme.get_highlight(highlight_name) line.stylize(node_style, start, end) @@ -186,7 +191,8 @@ def _tree_sitter_byte_offset(self, location: tuple[int, int]) -> int: bytes_on_left = len(lines[row][:column].encode("utf-8")) else: bytes_on_left = 0 - return bytes_lines_above + bytes_on_left + byte_offset = bytes_lines_above + bytes_on_left + return byte_offset def _prepare_highlights( self, @@ -258,7 +264,9 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No lines = self._lines row_out_of_bounds = row >= len(lines) - if not row_out_of_bounds: + if row_out_of_bounds: + return None + else: row_text = lines[row] # If it's not the last row, add a newline. # We could optimise this by not concatenating and just returning @@ -266,13 +274,9 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No # hot function. if row < len(lines) - 1: row_text += self.newline - else: - row_text = "" column_out_of_bounds = column >= len(row_text) - if row_out_of_bounds or column_out_of_bounds: - return_value = None + if column_out_of_bounds: + return None else: - return_value = row_text[column].encode("utf8") - - return return_value + return row_text[column].encode("utf-8") diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 70fde1b283..3c94140bfc 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -934,14 +934,14 @@ def action_delete_right(self) -> None: """Deletes the character to the right of the cursor and keeps the cursor at the same location. If there's a selection, then the selected range is deleted.""" - if self.cursor_at_end_of_document: - return selection = self.selection start, end = selection end_row, end_column = end if selection.is_empty: + if self.cursor_at_end_of_document: + return if self.cursor_at_end_of_row: end = (end_row + 1, 0) else: From 2bd279b7d9c7ea66b309ac22c341b6512209f350 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 10 Aug 2023 14:28:35 +0100 Subject: [PATCH 161/366] Fixing utf-8 multibyte character issues --- .../document/_syntax_aware_document.py | 54 ++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 4b056a4690..b68ff48afb 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections import defaultdict +from functools import lru_cache from pathlib import Path from rich.text import Text @@ -160,16 +161,18 @@ def get_line_text(self, line_index: int) -> Text: Returns: The syntax highlighted Text of the line. """ + line_bytes = self[line_index].encode("utf-8") + byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) line = Text(self[line_index], end="") if self._highlights: highlights = self._highlights[line_index] - print(line_index) - print(line) - print(highlights) - print("---") for start, end, highlight_name in highlights: node_style = self._syntax_theme.get_highlight(highlight_name) - line.stylize(node_style, start, end) + line.stylize( + node_style, + byte_to_codepoint.get(start, 0), + byte_to_codepoint.get(end), + ) return line @@ -275,8 +278,45 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No if row < len(lines) - 1: row_text += self.newline - column_out_of_bounds = column >= len(row_text) + encoded_row = row_text.encode("utf-8") + column_out_of_bounds = column >= len(encoded_row) if column_out_of_bounds: return None else: - return row_text[column].encode("utf-8") + return encoded_row[column:] + + +@lru_cache(maxsize=128) +def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]: + byte_to_codepoint = {} + current_byte_offset = 0 + code_point_offset = 0 + + while current_byte_offset < len(data): + # Save the mapping before incrementing the byte offset + byte_to_codepoint[current_byte_offset] = code_point_offset + + first_byte = data[current_byte_offset] + + # Single-byte character + if (first_byte & 0b10000000) == 0: + current_byte_offset += 1 + # 2-byte character + elif (first_byte & 0b11100000) == 0b11000000: + current_byte_offset += 2 + # 3-byte character + elif (first_byte & 0b11110000) == 0b11100000: + current_byte_offset += 3 + # 4-byte character + elif (first_byte & 0b11111000) == 0b11110000: + current_byte_offset += 4 + else: + raise ValueError(f"Invalid UTF-8 byte: {first_byte}") + + # Increment the code-point counter + code_point_offset += 1 + + # Mapping for the end of the string + byte_to_codepoint[current_byte_offset] = code_point_offset + + return byte_to_codepoint From 8212f8ab0aecaf2138d471fdc54249b1d8316243 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 10 Aug 2023 14:48:10 +0100 Subject: [PATCH 162/366] Default location of text insertion is cursor position, add cursor_location properties --- src/textual/widgets/_text_area.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 3c94140bfc..33b5caad98 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -612,6 +612,21 @@ def scroll_cursor_visible(self) -> Offset: ) return scroll_offset + @property + def cursor_location(self) -> Location: + """The current location of the cursor in the document.""" + return self.selection.end + + @cursor_location.setter + def cursor_location(self, new_location: Location) -> Location: + """Set the cursor_location to a new location. + + If a selection is in progress, the anchor point will remain. + """ + start, end = self.selection + self.selection = Selection(start, new_location) + return self.selection.end + @property def cursor_at_first_row(self) -> bool: return self.selection.end[0] == 0 @@ -877,9 +892,11 @@ def record_cursor_offset(self) -> None: def insert_text( self, text: str, - location: Location, + location: Location | None = None, cursor_destination: Location | None = None, ) -> None: + if location is None: + location = self.selection.end self.edit(Insert(text, location, location, cursor_destination)) def insert_text_range( From 4b74797205e0de806430739ee158ef74c41ea971 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 10 Aug 2023 14:57:28 +0100 Subject: [PATCH 163/366] Removing some debugging code --- src/textual/document/_syntax_aware_document.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index b68ff48afb..531c8d8848 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -136,16 +136,14 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: if TREE_SITTER: deleted_text_byte_length = len(deleted_text.encode("utf-8")) - edit_args = { - "start_byte": start_byte, - "old_end_byte": old_end_byte, - "new_end_byte": old_end_byte - deleted_text_byte_length, - "start_point": top, - "old_end_point": bottom, - "new_end_point": top, - } - print(f"edit = {edit_args!r}") - self._syntax_tree.edit(**edit_args) + self._syntax_tree.edit( + start_byte=start_byte, + old_end_byte=old_end_byte, + new_end_byte=old_end_byte - deleted_text_byte_length, + start_point=top, + old_end_point=bottom, + new_end_point=top, + ) new_tree = self._parser.parse(self._read_callable, self._syntax_tree) self._syntax_tree = new_tree self._prepare_highlights() From b7952f8ebbf0f1a87f3659fda6ed2b9d84daee0d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 10 Aug 2023 15:32:26 +0100 Subject: [PATCH 164/366] Cursor location tests --- tests/text_area/test_selection.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 84fb577b69..1b469e4c0c 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -23,6 +23,23 @@ def test_default_selection(): assert text_area.selection == Selection.cursor((0, 0)) +async def test_cursor_location_get(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((1, 1), (2, 2)) + assert text_area.cursor_location == (2, 2) + + +async def test_cursor_location_set(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((1, 1), (2, 2)) + text_area.cursor_location = (2, 3) + assert text_area.selection == Selection((1, 1), (2, 3)) + + async def test_selected_text_forward(): """Selecting text from top to bottom results in the correct selected_text.""" app = TextAreaApp() From e5850f35090cff89f20252d0b62422fc40ce439d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 10 Aug 2023 15:36:24 +0100 Subject: [PATCH 165/366] Updating snapshots --- .../__snapshots__/test_snapshots.ambr | 1997 +++++++++-------- 1 file changed, 1002 insertions(+), 995 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 1336cb443d..d8b15930c0 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -26861,318 +26861,319 @@ font-weight: 700; } - .terminal-3436158021-matrix { + .terminal-1142773784-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3436158021-title { + .terminal-1142773784-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3436158021-r1 { fill: #e4e5e6 } - .terminal-3436158021-r2 { fill: #1b1b1b } - .terminal-3436158021-r3 { fill: #75715e } - .terminal-3436158021-r4 { fill: #c5c8c6 } - .terminal-3436158021-r5 { fill: #7b7e82 } - .terminal-3436158021-r6 { fill: #e6db74 } - .terminal-3436158021-r7 { fill: #ae81ff } - .terminal-3436158021-r8 { fill: #f92672 } - .terminal-3436158021-r9 { fill: #a6e22e } + .terminal-1142773784-r1 { fill: #e4e5e6 } + .terminal-1142773784-r2 { fill: #1b1b1b } + .terminal-1142773784-r3 { fill: #75715e } + .terminal-1142773784-r4 { fill: #c5c8c6 } + .terminal-1142773784-r5 { fill: #7b7e82 } + .terminal-1142773784-r6 { fill: #e2e3e3 } + .terminal-1142773784-r7 { fill: #e6db74 } + .terminal-1142773784-r8 { fill: #ae81ff } + .terminal-1142773784-r9 { fill: #f92672 } + .terminal-1142773784-r10 { fill: #a6e22e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  /* This is a comment in CSS */ -  2   -  3  /* Basic selectors and properties */ -  4  body {                                 -  5      font-family: Arial, sans-serif;    -  6      background-color: #f4f4f4;         -  7      margin: 0;                         -  8      padding: 0;                        -  9  }                                      - 10   - 11  /* Class and ID selectors */ - 12  .header {                              - 13      background-color: #333;            - 14      color: #fff;                       - 15      padding: 10px0;                   - 16      text-align: center;                - 17  }                                      - 18   - 19  #logo {                                - 20      font-size: 24px;                   - 21      font-weight: bold;                 - 22  }                                      - 23   - 24  /* Descendant and child selectors */ - 25  .nav ul {                              - 26      list-style-type: none;             - 27      padding: 0;                        - 28  }                                      - 29   - 30  .nav > li {                            - 31      display: inline-block;             - 32      margin-right: 10px;                - 33  }                                      - 34   - 35  /* Pseudo-classes */ - 36  a:hover {                              - 37      text-decoration: underline;        - 38  }                                      - 39   - 40  input:focus {                          - 41      border-color: #007BFF;             - 42  }                                      - 43   - 44  /* Media query */ - 45  @media (max-width: 768px) {            - 46      body {                             - 47          font-size: 16px;               - 48      }                                  - 49   - 50      .header {                          - 51          padding: 5px0;                - 52      }                                  - 53  }                                      - 54   - 55  /* Keyframes animation */ - 56  @keyframes slideIn {                   - 57  from {                             - 58          transform: translateX(-100%);  - 59      }                                  - 60  to {                               - 61          transform: translateX(0);      - 62      }                                  - 63  }                                      - 64   - 65  .slide-in-element {                    - 66      animation: slideIn 0.5s forwards;  - 67  }                                      - 68   + +  1  /* This is a comment in CSS */ +  2   +  3  /* Basic selectors and properties */ +  4  body {                                 +  5      font-family: Arial, sans-serif;    +  6      background-color: #f4f4f4;         +  7      margin: 0;                         +  8      padding: 0;                        +  9  }                                      + 10   + 11  /* Class and ID selectors */ + 12  .header {                              + 13      background-color: #333;            + 14      color: #fff;                       + 15      padding: 10px0;                   + 16      text-align: center;                + 17  }                                      + 18   + 19  #logo {                                + 20      font-size: 24px;                   + 21      font-weight: bold;                 + 22  }                                      + 23   + 24  /* Descendant and child selectors */ + 25  .nav ul {                              + 26      list-style-type: none;             + 27      padding: 0;                        + 28  }                                      + 29   + 30  .nav > li {                            + 31      display: inline-block;             + 32      margin-right: 10px;                + 33  }                                      + 34   + 35  /* Pseudo-classes */ + 36  a:hover {                              + 37      text-decoration: underline;        + 38  }                                      + 39   + 40  input:focus {                          + 41      border-color: #007BFF;             + 42  }                                      + 43   + 44  /* Media query */ + 45  @media (max-width: 768px) {            + 46      body {                             + 47          font-size: 16px;               + 48      }                                  + 49   + 50      .header {                          + 51          padding: 5px0;                + 52      }                                  + 53  }                                      + 54   + 55  /* Keyframes animation */ + 56  @keyframes slideIn {                   + 57  from {                             + 58          transform: translateX(-100%);  + 59      }                                  + 60  to {                               + 61          transform: translateX(0);      + 62      }                                  + 63  }                                      + 64   + 65  .slide-in-element {                    + 66      animation: slideIn 0.5s forwards;  + 67  }                                      + 68   @@ -27203,269 +27204,270 @@ font-weight: 700; } - .terminal-3422989764-matrix { + .terminal-1143762621-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3422989764-title { + .terminal-1143762621-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3422989764-r1 { fill: #e4e5e6 } - .terminal-3422989764-r2 { fill: #1b1b1b } - .terminal-3422989764-r3 { fill: #c5c8c6 } - .terminal-3422989764-r4 { fill: #7b7e82 } + .terminal-1143762621-r1 { fill: #e4e5e6 } + .terminal-1143762621-r2 { fill: #1b1b1b } + .terminal-1143762621-r3 { fill: #c5c8c6 } + .terminal-1143762621-r4 { fill: #7b7e82 } + .terminal-1143762621-r5 { fill: #e2e3e3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  <!DOCTYPE html>                                                              -  2  <html lang="en">                                                            -  3   -  4  <head>                                                                      -  5      <!-- Meta tags -->                                                      -  6      <meta charset="UTF-8">                                                  -  7      <meta name="viewport" content="width=device-width, initial-scale=1.0">  -  8      <!-- Title -->                                                          -  9      <title>HTML Test Page</title>                                           - 10      <!-- Link to CSS -->                                                    - 11      <link rel="stylesheet" href="styles.css">                               - 12  </head>                                                                     - 13   - 14  <body>                                                                      - 15      <!-- Header section -->                                                 - 16      <header class="header">                                                 - 17          <h1 id="logo">HTML Test Page</h1>                                   - 18      </header>                                                               - 19   - 20      <!-- Navigation -->                                                     - 21      <nav class="nav">                                                       - 22          <ul>                                                                - 23              <li><a href="#">Home</a></li>                                   - 24              <li><a href="#">About</a></li>                                  - 25              <li><a href="#">Contact</a></li>                                - 26          </ul>                                                               - 27      </nav>                                                                  - 28   - 29      <!-- Main content area -->                                              - 30      <main>                                                                  - 31          <article>                                                           - 32              <h2>Welcome to the Test Page</h2>                               - 33              <p>This is a paragraph to test the HTML structure.</p>          - 34              <img src="test-image.jpg" alt="Test Image" width="300">         - 35          </article>                                                          - 36      </main>                                                                 - 37   - 38      <!-- Form -->                                                           - 39      <section>                                                               - 40          <form action="/submit" method="post">                               - 41              <label for="name">Name:</label>                                 - 42              <input type="text" id="name" name="name">                       - 43              <input type="submit" value="Submit">                            - 44          </form>                                                             - 45      </section>                                                              - 46   - 47      <!-- Footer -->                                                         - 48      <footer>                                                                - 49          <p>&copy; 2023 HTML Test Page</p>                                   - 50      </footer>                                                               - 51   - 52      <!-- Script tag -->                                                     - 53      <script src="scripts.js"></script>                                      - 54  </body>                                                                     - 55   - 56  </html>                                                                     - 57   + +  1  <!DOCTYPE html>                                                              +  2  <html lang="en">                                                            +  3   +  4  <head>                                                                      +  5      <!-- Meta tags -->                                                      +  6      <meta charset="UTF-8">                                                  +  7      <meta name="viewport" content="width=device-width, initial-scale=1.0">  +  8      <!-- Title -->                                                          +  9      <title>HTML Test Page</title>                                           + 10      <!-- Link to CSS -->                                                    + 11      <link rel="stylesheet" href="styles.css">                               + 12  </head>                                                                     + 13   + 14  <body>                                                                      + 15      <!-- Header section -->                                                 + 16      <header class="header">                                                 + 17          <h1 id="logo">HTML Test Page</h1>                                   + 18      </header>                                                               + 19   + 20      <!-- Navigation -->                                                     + 21      <nav class="nav">                                                       + 22          <ul>                                                                + 23              <li><a href="#">Home</a></li>                                   + 24              <li><a href="#">About</a></li>                                  + 25              <li><a href="#">Contact</a></li>                                + 26          </ul>                                                               + 27      </nav>                                                                  + 28   + 29      <!-- Main content area -->                                              + 30      <main>                                                                  + 31          <article>                                                           + 32              <h2>Welcome to the Test Page</h2>                               + 33              <p>This is a paragraph to test the HTML structure.</p>          + 34              <img src="test-image.jpg" alt="Test Image" width="300">         + 35          </article>                                                          + 36      </main>                                                                 + 37   + 38      <!-- Form -->                                                           + 39      <section>                                                               + 40          <form action="/submit" method="post">                               + 41              <label for="name">Name:</label>                                 + 42              <input type="text" id="name" name="name">                       + 43              <input type="submit" value="Submit">                            + 44          </form>                                                             + 45      </section>                                                              + 46   + 47      <!-- Footer -->                                                         + 48      <footer>                                                                + 49          <p>&copy; 2023 HTML Test Page</p>                                   + 50      </footer>                                                               + 51   + 52      <!-- Script tag -->                                                     + 53      <script src="scripts.js"></script>                                      + 54  </body>                                                                     + 55   + 56  </html>                                                                     + 57   @@ -27496,167 +27498,168 @@ font-weight: 700; } - .terminal-1655668963-matrix { + .terminal-3215715892-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1655668963-title { + .terminal-3215715892-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1655668963-r1 { fill: #e4e5e6 } - .terminal-1655668963-r2 { fill: #1b1b1b } - .terminal-1655668963-r3 { fill: #c5c8c6 } - .terminal-1655668963-r4 { fill: #7b7e82 } - .terminal-1655668963-r5 { fill: #e6db74 } - .terminal-1655668963-r6 { fill: #ae81ff } + .terminal-3215715892-r1 { fill: #e4e5e6 } + .terminal-3215715892-r2 { fill: #1b1b1b } + .terminal-3215715892-r3 { fill: #c5c8c6 } + .terminal-3215715892-r4 { fill: #7b7e82 } + .terminal-3215715892-r5 { fill: #e2e3e3 } + .terminal-3215715892-r6 { fill: #e6db74 } + .terminal-3215715892-r7 { fill: #ae81ff } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  { -  2      "name": "John Doe",                            -  3      "age": 30,                                     -  4      "isStudent": false,                            -  5      "address": {                                   -  6          "street": "123 Main St",                   -  7          "city": "Anytown",                         -  8          "state": "CA",                             -  9          "zip": "12345" - 10      },                                             - 11      "phoneNumbers": [                              - 12          {                                          - 13              "type": "home",                        - 14              "number": "555-555-1234" - 15          },                                         - 16          {                                          - 17              "type": "work",                        - 18              "number": "555-555-5678" - 19          }                                          - 20      ],                                             - 21      "hobbies": ["reading""hiking""swimming"],  - 22      "pets": [                                      - 23          {                                          - 24              "type": "dog",                         - 25              "name": "Fido" - 26          },                                         - 27      ],                                             - 28      "graduationYear": null                         - 29  }                                                  - 30   - 31   + +  1  { +  2      "name": "John Doe",                            +  3      "age": 30,                                     +  4      "isStudent": false,                            +  5      "address": {                                   +  6          "street": "123 Main St",                   +  7          "city": "Anytown",                         +  8          "state": "CA",                             +  9          "zip": "12345" + 10      },                                             + 11      "phoneNumbers": [                              + 12          {                                          + 13              "type": "home",                        + 14              "number": "555-555-1234" + 15          },                                         + 16          {                                          + 17              "type": "work",                        + 18              "number": "555-555-5678" + 19          }                                          + 20      ],                                             + 21      "hobbies": ["reading""hiking""swimming"],  + 22      "pets": [                                      + 23          {                                          + 24              "type": "dog",                         + 25              "name": "Fido" + 26          },                                         + 27      ],                                             + 28      "graduationYear": null                         + 29  }                                                  + 30   + 31   @@ -27687,318 +27690,318 @@ font-weight: 700; } - .terminal-3761540624-matrix { + .terminal-2526664541-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3761540624-title { + .terminal-2526664541-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3761540624-r1 { fill: #e4e5e6 } - .terminal-3761540624-r2 { fill: #1b1b1b;font-weight: bold } - .terminal-3761540624-r3 { fill: #f92672;font-weight: bold } - .terminal-3761540624-r4 { fill: #c5c8c6 } - .terminal-3761540624-r5 { fill: #7b7e82 } - .terminal-3761540624-r6 { fill: #75715e } - .terminal-3761540624-r7 { fill: #e2e3e3 } - .terminal-3761540624-r8 { fill: #23568b } + .terminal-2526664541-r1 { fill: #e4e5e6 } + .terminal-2526664541-r2 { fill: #1b1b1b;font-weight: bold } + .terminal-2526664541-r3 { fill: #f92672;font-weight: bold } + .terminal-2526664541-r4 { fill: #c5c8c6 } + .terminal-2526664541-r5 { fill: #7b7e82 } + .terminal-2526664541-r6 { fill: #e2e3e3 } + .terminal-2526664541-r7 { fill: #75715e } + .terminal-2526664541-r8 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  Heading -  2  =======                                                                      -  3   -  4  Sub-heading -  5  -----------                                                                  -  6   -  7  ### Heading -  8   -  9  #### H4 Heading - 10   - 11  ##### H5 Heading - 12   - 13  ###### H6 Heading - 14   - 15   - 16  Paragraphs are separated                                                     - 17  by a blank line.                                                             - 18   - 19  Two spaces at the end of a line                                              - 20  produces a line break.                                                       - 21   - 22  Text attributes _italic_,                                                    - 23  **bold**, `monospace`.                                                       - 24   - 25  Horizontal rule:                                                             - 26   - 27  ---                                                                          - 28   - 29  Bullet list:                                                                 - 30   - 31  * apples                                                                   - 32  * oranges                                                                  - 33  * pears                                                                    - 34   - 35  Numbered list:                                                               - 36   - 37  1. lather                                                                  - 38  2. rinse                                                                   - 39  3. repeat                                                                  - 40   - 41  An [example](http://example.com).                                            - 42   - 43  > Markdown uses email-style > characters for blockquoting.                   - 44  >                                                                            - 45  > Lorem ipsum                                                                - 46   - 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) - 48   - 49   - 50  ```                                                                          - 51  a=1                                                                          - 52  ```                                                                          - 53   - 54  ```python                                                                    - 55  import this                                                                  - 56  ```                                                                          - 57   - 58  ```somelang                                                                  - 59  foobar                                                                       - 60  ```                                                                          - 61   - 62      import this                                                              - 63   - 64   - 65  1. List item                                                                 - 66   - 67         Code block                                                            - 68   - + +  1  Heading +  2  =======                                                                      +  3   +  4  Sub-heading +  5  -----------                                                                  +  6   +  7  ### Heading +  8   +  9  #### H4 Heading + 10   + 11  ##### H5 Heading + 12   + 13  ###### H6 Heading + 14   + 15   + 16  Paragraphs are separated                                                     + 17  by a blank line.                                                             + 18   + 19  Two spaces at the end of a line                                              + 20  produces a line break.                                                       + 21   + 22  Text attributes _italic_,                                                    + 23  **bold**, `monospace`.                                                       + 24   + 25  Horizontal rule:                                                             + 26   + 27  ---                                                                          + 28   + 29  Bullet list:                                                                 + 30   + 31  * apples                                                                   + 32  * oranges                                                                  + 33  * pears                                                                    + 34   + 35  Numbered list:                                                               + 36   + 37  1. lather                                                                  + 38  2. rinse                                                                   + 39  3. repeat                                                                  + 40   + 41  An [example](http://example.com).                                            + 42   + 43  > Markdown uses email-style > characters for blockquoting.                   + 44  >                                                                            + 45  > Lorem ipsum                                                                + 46   + 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) + 48   + 49   + 50  ```                                                                          + 51  a=1                                                                          + 52  ```                                                                          + 53   + 54  ```python                                                                    + 55  import this                                                                  + 56  ```                                                                          + 57   + 58  ```somelang                                                                  + 59  foobar                                                                       + 60  ```                                                                          + 61   + 62      import this                                                              + 63   + 64   + 65  1. List item                                                                 + 66   + 67         Code block                                                            + 68   + @@ -28028,363 +28031,364 @@ font-weight: 700; } - .terminal-1571808136-matrix { + .terminal-3741042815-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1571808136-title { + .terminal-3741042815-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1571808136-r1 { fill: #e4e5e6 } - .terminal-1571808136-r2 { fill: #1b1b1b } - .terminal-1571808136-r3 { fill: #f92672 } - .terminal-1571808136-r4 { fill: #c5c8c6 } - .terminal-1571808136-r5 { fill: #7b7e82 } - .terminal-1571808136-r6 { fill: #75715e } - .terminal-1571808136-r7 { fill: #e6db74 } - .terminal-1571808136-r8 { fill: #ae81ff } - .terminal-1571808136-r9 { fill: #a6e22e } - .terminal-1571808136-r10 { fill: #68a0b3 } + .terminal-3741042815-r1 { fill: #e4e5e6 } + .terminal-3741042815-r2 { fill: #1b1b1b } + .terminal-3741042815-r3 { fill: #f92672 } + .terminal-3741042815-r4 { fill: #c5c8c6 } + .terminal-3741042815-r5 { fill: #7b7e82 } + .terminal-3741042815-r6 { fill: #e2e3e3 } + .terminal-3741042815-r7 { fill: #75715e } + .terminal-3741042815-r8 { fill: #e6db74 } + .terminal-3741042815-r9 { fill: #ae81ff } + .terminal-3741042815-r10 { fill: #a6e22e } + .terminal-3741042815-r11 { fill: #68a0b3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  importmath -  2  fromosimportpath -  3   -  4  # I'm a comment :) -  5   -  6  string_var = "Hello, world!" -  7  int_var = 42 -  8  float_var = 3.14                                                             -  9  complex_var = 1 + 2j - 10   - 11  list_var = [12345]                                                   - 12  tuple_var = (12345)                                                  - 13  set_var = {12345}                                                    - 14  dict_var = {"a"1"b"2"c"3}                                          - 15   - 16  deffunction_no_args():                                                      - 17  return"No arguments" - 18   - 19  deffunction_with_args(ab):                                                - 20  returna + b - 21   - 22  deffunction_with_default_args(a=0b=0):                                    - 23  returna * b - 24   - 25  lambda_func = lambdaxx**2 - 26   - 27  ifint_var == 42:                                                            - 28  print("It's the answer!")                                                - 29  elifint_var < 42:                                                           - 30  print("Less than the answer.")                                           - 31  else:                                                                        - 32  print("Greater than the answer.")                                        - 33   - 34  forindexvalue in enumerate(list_var):                                     - 35  print(f"Index: {index}, Value: {value}")                                 - 36   - 37  counter = 0 - 38  whilecounter < 5:                                                           - 39  print(f"Counter value: {counter}")                                       - 40  counter += 1 - 41   - 42  squared_numbers = [x**2forx in range(10ifx % 2 == 0]                    - 43   - 44  try:                                                                         - 45  result = 10 / 0 - 46  exceptZeroDivisionError:                                                    - 47  print("Cannot divide by zero!")                                          - 48  finally:                                                                     - 49  print("End of try-except block.")                                        - 50   - 51  classAnimal:                                                                - 52  def__init__(selfname):                                                - 53  self.name = name - 54   - 55  defspeak(self):                                                         - 56  raiseNotImplementedError("Subclasses must implement this method." - 57   - 58  classDog(Animal):                                                           - 59  defspeak(self):                                                         - 60  returnf"{self.name} says Woof!" - 61   - 62  deffibonacci(n):                                                            - 63  ab = 01 - 64  for_ in range(n):                                                       - 65  yielda - 66  ab = ba + b - 67   - 68  fornum in fibonacci(5):                                                     - 69  print(num)                                                               - 70   - 71  withopen('test.txt''w'asf:                                             - 72  f.write("Testing with statement.")                                       - 73   - 74  @my_decorator - 75  defsay_hello():                                                             - 76  print("Hello!")                                                          - 77   - 78  say_hello()                                                                  - 79   + +  1  importmath +  2  fromosimportpath +  3   +  4  # I'm a comment :) +  5   +  6  string_var = "Hello, world!" +  7  int_var = 42 +  8  float_var = 3.14                                                             +  9  complex_var = 1 + 2j + 10   + 11  list_var = [12345]                                                   + 12  tuple_var = (12345)                                                  + 13  set_var = {12345}                                                    + 14  dict_var = {"a"1"b"2"c"3}                                          + 15   + 16  deffunction_no_args():                                                      + 17  return"No arguments" + 18   + 19  deffunction_with_args(ab):                                                + 20  returna + b + 21   + 22  deffunction_with_default_args(a=0b=0):                                    + 23  returna * b + 24   + 25  lambda_func = lambdaxx**2 + 26   + 27  ifint_var == 42:                                                            + 28  print("It's the answer!")                                                + 29  elifint_var < 42:                                                           + 30  print("Less than the answer.")                                           + 31  else:                                                                        + 32  print("Greater than the answer.")                                        + 33   + 34  forindexvalue in enumerate(list_var):                                     + 35  print(f"Index: {index}, Value: {value}")                                 + 36   + 37  counter = 0 + 38  whilecounter < 5:                                                           + 39  print(f"Counter value: {counter}")                                       + 40  counter += 1 + 41   + 42  squared_numbers = [x**2forx in range(10ifx % 2 == 0]                    + 43   + 44  try:                                                                         + 45  result = 10 / 0 + 46  exceptZeroDivisionError:                                                    + 47  print("Cannot divide by zero!")                                          + 48  finally:                                                                     + 49  print("End of try-except block.")                                        + 50   + 51  classAnimal:                                                                + 52  def__init__(selfname):                                                + 53  self.name = name + 54   + 55  defspeak(self):                                                         + 56  raiseNotImplementedError("Subclasses must implement this method." + 57   + 58  classDog(Animal):                                                           + 59  defspeak(self):                                                         + 60  returnf"{self.name} says Woof!" + 61   + 62  deffibonacci(n):                                                            + 63  ab = 01 + 64  for_ in range(n):                                                       + 65  yielda + 66  ab = ba + b + 67   + 68  fornum in fibonacci(5):                                                     + 69  print(num)                                                               + 70   + 71  withopen('test.txt''w'asf:                                             + 72  f.write("Testing with statement.")                                       + 73   + 74  @my_decorator + 75  defsay_hello():                                                             + 76  print("Hello!")                                                          + 77   + 78  say_hello()                                                                  + 79   @@ -28415,144 +28419,145 @@ font-weight: 700; } - .terminal-163005199-matrix { + .terminal-3114921998-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-163005199-title { + .terminal-3114921998-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-163005199-r1 { fill: #e4e5e6 } - .terminal-163005199-r2 { fill: #1b1b1b } - .terminal-163005199-r3 { fill: #c5c8c6 } - .terminal-163005199-r4 { fill: #7b7e82 } - .terminal-163005199-r5 { fill: #e2e3e3 } - .terminal-163005199-r6 { fill: #23568b } + .terminal-3114921998-r1 { fill: #e4e5e6 } + .terminal-3114921998-r2 { fill: #1b1b1b } + .terminal-3114921998-r3 { fill: #c5c8c6 } + .terminal-3114921998-r4 { fill: #7b7e82 } + .terminal-3114921998-r5 { fill: #e2e3e3 } + .terminal-3114921998-r6 { fill: #f92672 } + .terminal-3114921998-r7 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  ^abc            # Matches any string that starts with "abc"                  -  2  abc$            # Matches any string that ends with "abc"                    -  3  ^abc$           # Matches the string "abc" and nothing else                  -  4  a.b             # Matches any string containing "a", any character, then "b" -  5  a[.]b           # Matches the string "a.b"                                   -  6  a|b             # Matches either "a" or "b"                                  -  7  a{2}            # Matches "aa"                                               -  8  a{2,}           # Matches two or more consecutive "a" characters             -  9  a{2,5}          # Matches between 2 and 5 consecutive "a" characters         - 10  a?              # Matches "a" or nothing (0 or 1 occurrence of "a")          - 11  a*              # Matches zero or more consecutive "a" characters            - 12  a+              # Matches one or more consecutive "a" characters             - 13  \d              # Matches any digit (equivalent to [0-9])                    - 14  \D              # Matches any non-digit                                      - 15  \w              # Matches any word character (equivalent to [a-zA-Z0-9_])    - 16  \W              # Matches any non-word character                             - 17  \s              # Matches any whitespace character (spaces, tabs, line break - 18  \S              # Matches any non-whitespace character                       - 19  (?i)abc         # Case-insensitive match for "abc"                           - 20  (?:a|b)         # Non-capturing group for either "a" or "b"                  - 21  (?<=a)b         # Positive lookbehind: matches "b" that is preceded by "a"   - 22  (?<!a)b         # Negative lookbehind: matches "b" that is not preceded by " - 23  a(?=b)          # Positive lookahead: matches "a" that is followed by "b"    - 24  a(?!b)          # Negative lookahead: matches "a" that is not followed by "b - 25   - + + + +  1  ^abc            # Matches any string that starts with "abc"                  +  2  abc$            # Matches any string that ends with "abc"                    +  3  ^abc$           # Matches the string "abc" and nothing else                  +  4  a.b             # Matches any string containing "a", any character, then "b" +  5  a[.]b           # Matches the string "a.b"                                   +  6  a|b             # Matches either "a" or "b"                                  +  7  a{2}            # Matches "aa"                                               +  8  a{2,}           # Matches two or more consecutive "a" characters             +  9  a{2,5}          # Matches between 2 and 5 consecutive "a" characters         + 10  a?              # Matches "a" or nothing (0 or 1 occurrence of "a") + 11  a*              # Matches zero or more consecutive "a" characters            + 12  a+              # Matches one or more consecutive "a" characters             + 13  \d              # Matches any digit (equivalent to [0-9]) + 14  \D              # Matches any non-digit                                      + 15  \w              # Matches any word character (equivalent to [a-zA-Z0-9_]) + 16  \W              # Matches any non-word character                             + 17  \s              # Matches any whitespace character (spaces, tabs, line break + 18  \S              # Matches any non-whitespace character                       + 19  (?i)abc         # Case-insensitive match for "abc"                           + 20  (?:a|b)         # Non-capturing group for either "a" or "b"                  + 21  (?<=a)b         # Positive lookbehind: matches "b" that is preceded by "a"   + 22  (?<!a)b         # Negative lookbehind: matches "b" that is not preceded by " + 23  a(?=b)          # Positive lookahead: matches "a" that is followed by "b"    + 24  a(?!b)          # Negative lookahead: matches "a" that is not followed by "b + 25   + @@ -28582,222 +28587,222 @@ font-weight: 700; } - .terminal-2584882847-matrix { + .terminal-3443060286-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2584882847-title { + .terminal-3443060286-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2584882847-r1 { fill: #e4e5e6 } - .terminal-2584882847-r2 { fill: #1b1b1b } - .terminal-2584882847-r3 { fill: #75715e } - .terminal-2584882847-r4 { fill: #c5c8c6 } - .terminal-2584882847-r5 { fill: #7b7e82 } - .terminal-2584882847-r6 { fill: #f92672 } - .terminal-2584882847-r7 { fill: #ae81ff } - .terminal-2584882847-r8 { fill: #e6db74 } - .terminal-2584882847-r9 { fill: #e2e3e3 } + .terminal-3443060286-r1 { fill: #e4e5e6 } + .terminal-3443060286-r2 { fill: #1b1b1b } + .terminal-3443060286-r3 { fill: #75715e } + .terminal-3443060286-r4 { fill: #c5c8c6 } + .terminal-3443060286-r5 { fill: #7b7e82 } + .terminal-3443060286-r6 { fill: #e2e3e3 } + .terminal-3443060286-r7 { fill: #f92672 } + .terminal-3443060286-r8 { fill: #ae81ff } + .terminal-3443060286-r9 { fill: #e6db74 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  -- This is a comment in SQL -  2   -  3  -- Create tables -  4  CREATETABLEAuthors (                                                       -  5  AuthorIDINTPRIMARY KEY,                                                -  6  NameVARCHAR(255NOT NULL,                                              -  7  CountryVARCHAR(50)                                                      -  8  );                                                                           -  9   - 10  CREATETABLEBooks (                                                         - 11  BookIDINTPRIMARY KEY,                                                  - 12  TitleVARCHAR(255NOT NULL,                                             - 13  AuthorIDINT,                                                            - 14  PublishedDateDATE,                                                      - 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)                      - 16  );                                                                           - 17   - 18  -- Insert data - 19  INSERTINTOAuthors (AuthorIDNameCountryVALUES (1'George Orwell''U - 20   - 21  INSERTINTOBooks (BookIDTitleAuthorIDPublishedDateVALUES (1'1984' - 22   - 23  -- Update data - 24  UPDATEAuthorsSETCountry = 'United Kingdom'WHERECountry = 'UK';          - 25   - 26  -- Select data with JOIN - 27  SELECTBooks.TitleAuthors.Name - 28  FROMBooks - 29  JOINAuthorsONBooks.AuthorID = Authors.AuthorID;                           - 30   - 31  -- Delete data (commented to preserve data for other examples) - 32  -- DELETE FROM Books WHERE BookID = 1; - 33   - 34  -- Alter table structure - 35  ALTER TABLEAuthors ADD COLUMN BirthDateDATE;                               - 36   - 37  -- Create index - 38  CREATEINDEXidx_author_nameONAuthors(Name);                               - 39   - 40  -- Drop index (commented to avoid actually dropping it) - 41  -- DROP INDEX idx_author_name ON Authors; - 42   - 43  -- End of script - 44   + +  1  -- This is a comment in SQL +  2   +  3  -- Create tables +  4  CREATETABLEAuthors (                                                       +  5  AuthorIDINTPRIMARY KEY,                                                +  6  NameVARCHAR(255NOT NULL,                                              +  7  CountryVARCHAR(50)                                                      +  8  );                                                                           +  9   + 10  CREATETABLEBooks (                                                         + 11  BookIDINTPRIMARY KEY,                                                  + 12  TitleVARCHAR(255NOT NULL,                                             + 13  AuthorIDINT,                                                            + 14  PublishedDateDATE,                                                      + 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)                      + 16  );                                                                           + 17   + 18  -- Insert data + 19  INSERTINTOAuthors (AuthorIDNameCountryVALUES (1'George Orwell''U + 20   + 21  INSERTINTOBooks (BookIDTitleAuthorIDPublishedDateVALUES (1'1984' + 22   + 23  -- Update data + 24  UPDATEAuthorsSETCountry = 'United Kingdom'WHERECountry = 'UK';          + 25   + 26  -- Select data with JOIN + 27  SELECTBooks.TitleAuthors.Name + 28  FROMBooks + 29  JOINAuthorsONBooks.AuthorID = Authors.AuthorID;                           + 30   + 31  -- Delete data (commented to preserve data for other examples) + 32  -- DELETE FROM Books WHERE BookID = 1; + 33   + 34  -- Alter table structure + 35  ALTER TABLEAuthors ADD COLUMN BirthDateDATE;                               + 36   + 37  -- Create index + 38  CREATEINDEXidx_author_nameONAuthors(Name);                               + 39   + 40  -- Drop index (commented to avoid actually dropping it) + 41  -- DROP INDEX idx_author_name ON Authors; + 42   + 43  -- End of script + 44   @@ -28828,148 +28833,149 @@ font-weight: 700; } - .terminal-1746006137-matrix { + .terminal-3176236472-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1746006137-title { + .terminal-3176236472-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1746006137-r1 { fill: #e4e5e6 } - .terminal-1746006137-r2 { fill: #1b1b1b } - .terminal-1746006137-r3 { fill: #75715e } - .terminal-1746006137-r4 { fill: #c5c8c6 } - .terminal-1746006137-r5 { fill: #7b7e82 } - .terminal-1746006137-r6 { fill: #e6db74 } - .terminal-1746006137-r7 { fill: #ae81ff } + .terminal-3176236472-r1 { fill: #e4e5e6 } + .terminal-3176236472-r2 { fill: #1b1b1b } + .terminal-3176236472-r3 { fill: #75715e } + .terminal-3176236472-r4 { fill: #c5c8c6 } + .terminal-3176236472-r5 { fill: #7b7e82 } + .terminal-3176236472-r6 { fill: #e2e3e3 } + .terminal-3176236472-r7 { fill: #e6db74 } + .terminal-3176236472-r8 { fill: #ae81ff } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  # This is a comment in TOML -  2   -  3  string = "Hello, world!" -  4  integer = 42 -  5  float = 3.14                            -  6  boolean = true                          -  7  datetime = 1979-05-27T07:32:00Z         -  8   -  9  fruits = ["apple""banana""cherry" - 10   - 11  [address]                               - 12  street = "123 Main St" - 13  city = "Anytown" - 14  state = "CA" - 15  zip = "12345" - 16   - 17  [person.john]                           - 18  name = "John Doe" - 19  age = 28 - 20  is_student = false                      - 21   - 22   - 23  [[animals]]                             - 24  name = "Fido" - 25  type = "dog" - 26   + +  1  # This is a comment in TOML +  2   +  3  string = "Hello, world!" +  4  integer = 42 +  5  float = 3.14                            +  6  boolean = true                          +  7  datetime = 1979-05-27T07:32:00Z         +  8   +  9  fruits = ["apple""banana""cherry" + 10   + 11  [address]                               + 12  street = "123 Main St" + 13  city = "Anytown" + 14  state = "CA" + 15  zip = "12345" + 16   + 17  [person.john]                           + 18  name = "John Doe" + 19  age = 28 + 20  is_student = false                      + 21   + 22   + 23  [[animals]]                             + 24  name = "Fido" + 25  type = "dog" + 26   @@ -29000,196 +29006,197 @@ font-weight: 700; } - .terminal-2588352134-matrix { + .terminal-603738614-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2588352134-title { + .terminal-603738614-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2588352134-r1 { fill: #e4e5e6 } - .terminal-2588352134-r2 { fill: #1b1b1b } - .terminal-2588352134-r3 { fill: #75715e } - .terminal-2588352134-r4 { fill: #c5c8c6 } - .terminal-2588352134-r5 { fill: #7b7e82 } - .terminal-2588352134-r6 { fill: #e6db74 } - .terminal-2588352134-r7 { fill: #ae81ff } + .terminal-603738614-r1 { fill: #e4e5e6 } + .terminal-603738614-r2 { fill: #1b1b1b } + .terminal-603738614-r3 { fill: #75715e } + .terminal-603738614-r4 { fill: #c5c8c6 } + .terminal-603738614-r5 { fill: #7b7e82 } + .terminal-603738614-r6 { fill: #e2e3e3 } + .terminal-603738614-r7 { fill: #e6db74 } + .terminal-603738614-r8 { fill: #ae81ff } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  # This is a comment in YAML -  2   -  3  # Scalars -  4  string"Hello, world!" -  5  integer42 -  6  float3.14 -  7  boolean: true                                         -  8   -  9  # Sequences (Arrays) - 10  fruits:                                               - 11    - Apple - 12    - Banana - 13    - Cherry - 14   - 15  # Nested sequences - 16  persons:                                              - 17    - nameJohn - 18  age28 - 19  is_student: false                                 - 20    - nameJane - 21  age22 - 22  is_student: true                                  - 23   - 24  # Mappings (Dictionaries) - 25  address:                                              - 26  street123 Main St - 27  cityAnytown - 28  stateCA - 29  zip'12345' - 30   - 31  # Multiline string - 32  description| - 33    This is a multiline - 34    string in YAML. - 35   - 36  # Inline and nested collections - 37  colors: { redFF0000green00FF00blue0000FF }  - 38   + +  1  # This is a comment in YAML +  2   +  3  # Scalars +  4  string"Hello, world!" +  5  integer42 +  6  float3.14 +  7  boolean: true                                         +  8   +  9  # Sequences (Arrays) + 10  fruits:                                               + 11    - Apple + 12    - Banana + 13    - Cherry + 14   + 15  # Nested sequences + 16  persons:                                              + 17    - nameJohn + 18  age28 + 19  is_student: false                                 + 20    - nameJane + 21  age22 + 22  is_student: true                                  + 23   + 24  # Mappings (Dictionaries) + 25  address:                                              + 26  street123 Main St + 27  cityAnytown + 28  stateCA + 29  zip'12345' + 30   + 31  # Multiline string + 32  description| + 33    This is a multiline + 34    string in YAML. + 35   + 36  # Inline and nested collections + 37  colors: { redFF0000green00FF00blue0000FF }  + 38   From 63cdf16c9f49f5d5d7bc4af961e05520e547824a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 10 Aug 2023 16:42:53 +0100 Subject: [PATCH 166/366] Cached utf8 encoding --- src/textual/document/_document.py | 7 ++++ .../document/_syntax_aware_document.py | 39 ++++++++++--------- src/textual/widgets/_text_area.py | 1 - 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 2290984c5b..785880b8da 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -1,6 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from functools import lru_cache from typing import NamedTuple, Tuple from rich.text import Text @@ -14,6 +15,12 @@ VALID_NEWLINES = set(get_args(Newline)) +@lru_cache(maxsize=1024) +def _utf8_encode(text: str) -> bytes: + """Encode the input text as utf-8 bytes.""" + return text.encode("utf-8") + + def _detect_newline_style(text: str) -> Newline: """Return the newline type used in this document. diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 531c8d8848..fd6f97b061 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -22,7 +22,7 @@ from textual._fix_direction import _fix_direction from textual._languages import VALID_LANGUAGES -from textual.document._document import Document, Highlight +from textual.document._document import Document, Highlight, _utf8_encode TREE_SITTER_PATH = Path(__file__) / "../../../../tree-sitter/" HIGHLIGHTS_PATH = TREE_SITTER_PATH / "highlights/" @@ -97,7 +97,7 @@ def insert_range( end_location = super().insert_range(start, end, text) if TREE_SITTER: - text_byte_length = len(text.encode("utf-8")) + text_byte_length = len(_utf8_encode(text)) self._syntax_tree.edit( start_byte=start_byte, old_end_byte=old_end_byte, @@ -135,7 +135,7 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: deleted_text = super().delete_range(start, end) if TREE_SITTER: - deleted_text_byte_length = len(deleted_text.encode("utf-8")) + deleted_text_byte_length = len(_utf8_encode(deleted_text)) self._syntax_tree.edit( start_byte=start_byte, old_end_byte=old_end_byte, @@ -159,7 +159,7 @@ def get_line_text(self, line_index: int) -> Text: Returns: The syntax highlighted Text of the line. """ - line_bytes = self[line_index].encode("utf-8") + line_bytes = _utf8_encode(self[line_index]) byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) line = Text(self[line_index], end="") if self._highlights: @@ -186,10 +186,10 @@ def _tree_sitter_byte_offset(self, location: tuple[int, int]) -> int: lines_above = lines[:row] end_of_line_width = len(self.newline) bytes_lines_above = sum( - len(line.encode("utf-8")) + end_of_line_width for line in lines_above + len(_utf8_encode(line)) + end_of_line_width for line in lines_above ) if row < len(lines): - bytes_on_left = len(lines[row][:column].encode("utf-8")) + bytes_on_left = len(_utf8_encode(lines[row][:column])) else: bytes_on_left = 0 byte_offset = bytes_lines_above + bytes_on_left @@ -214,6 +214,7 @@ def _prepare_highlights( if end_point is not None: captures_kwargs["end_point"] = end_point + # We could optimise by only preparing highlights for a subset of lines here. captures = query.captures(self._syntax_tree.root_node, **captures_kwargs) highlight_updates: dict[int, list[Highlight]] = defaultdict(list) @@ -264,24 +265,26 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No row, column = point lines = self._lines + newline = self.newline + row_out_of_bounds = row >= len(lines) if row_out_of_bounds: return None else: row_text = lines[row] - # If it's not the last row, add a newline. - # We could optimise this by not concatenating and just returning - # the appropriate character. This might be worth it since it's a very - # hot function. - if row < len(lines) - 1: - row_text += self.newline - - encoded_row = row_text.encode("utf-8") - column_out_of_bounds = column >= len(encoded_row) - if column_out_of_bounds: - return None + + encoded_row = _utf8_encode(row_text) + encoded_row_length = len(encoded_row) + + if column < encoded_row_length: + return encoded_row[column:] + _utf8_encode(newline) + elif column == encoded_row_length: + return _utf8_encode(newline[0]) + elif column == encoded_row_length + 1: + if newline == "\r\n": + return b"\n" else: - return encoded_row[column:] + return None @lru_cache(maxsize=128) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 33b5caad98..4169a18312 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -4,7 +4,6 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, ClassVar -from rich.segment import Segment from rich.style import Style from rich.text import Text From b0290eb8db6d30f1c5da7e526a4c850d56d4a0d4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 11:02:30 +0100 Subject: [PATCH 167/366] TextArea selection snapshot testing --- .../__snapshots__/test_snapshots.ambr | 500 ++++++++++++++++++ tests/snapshot_tests/test_snapshots.py | 35 +- 2 files changed, 533 insertions(+), 2 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 9800c680da..26426a21e9 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -29843,6 +29843,506 @@ ''' # --- +# name: test_text_area_selection_rendering[selection0] + ''' + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + 1  Iamaline. + 2   + 3  Iamanotherline.          + 4   + 5  Iamthefinalline + + + + + ''' +# --- +# name: test_text_area_selection_rendering[selection1] + ''' + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + 1  Iamaline. + 2   + 3  Iamanotherline.    + 4   + 5  Iamthefinalline + + + + + ''' +# --- +# name: test_text_area_selection_rendering[selection2] + ''' + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + 1  Iamaline. + 2   + 3  Iamanotherline. + 4   + 5  Iamthefinalline + + + + + ''' +# --- +# name: test_text_area_selection_rendering[selection3] + ''' + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + 1  Iamaline. + 2   + 3  Iamanotherline. + 4   + 5  Iamthefinalline. + + + + + ''' +# --- +# name: test_text_area_selection_rendering[selection4] + ''' + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + 1  Iamaline.          + 2   + 3  Iamanotherline.    + 4   + 5  Iamthefinalline + + + + + ''' +# --- +# name: test_text_area_selection_rendering[selection5] + ''' + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + 1  Iamaline.          + 2   + 3  Iamanotherline.          + 4   + 5  Iamthefinalline + + + + + ''' +# --- # name: test_text_log_blank_write ''' diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index d5396d9a58..fcd02cb866 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -4,6 +4,7 @@ from tests.snapshot_tests.language_snippets import SNIPPETS from textual._languages import VALID_LANGUAGES +from textual.document._document import Selection from textual.widgets import TextArea # These paths should be relative to THIS directory. @@ -633,7 +634,7 @@ def test_nested_fr(snap_compare) -> None: # https://github.com/Textualize/textual/pull/3059 assert snap_compare(SNAPSHOT_APPS_DIR / "nested_fr.py") - + @pytest.mark.parametrize("language", VALID_LANGUAGES) def test_text_area_language_rendering(language, snap_compare): # This test will fail if we're missing a snapshot test for a valid @@ -654,6 +655,36 @@ def setup_language(pilot) -> None: ) +@pytest.mark.parametrize( + "selection", + [ + Selection((0, 0), (2, 8)), + Selection((1, 0), (0, 0)), + Selection((5, 2), (0, 0)), + Selection((0, 0), (4, 20)), + Selection.cursor((1, 0)), + Selection.cursor((2, 6)), + ], +) +def test_text_area_selection_rendering(snap_compare, selection): + text = """I am a line. + +I am another line. + +I am the final line.""" + + def setup_selection(pilot): + text_area = pilot.app.query_one(TextArea) + text_area.load_text(text) + text_area.language = "python" + text_area.selection = selection + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area_languages.py", + run_before=setup_selection, + terminal_size=(30, text.count("\n") + 1), + ) + + def test_digits(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "digits.py") - From e1304092f8cca364fa7f840e003994261e8cb631 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 11:41:59 +0100 Subject: [PATCH 168/366] Tidying docstrings and queries --- src/textual/_syntax_theme.py | 29 ++++++++++--- src/textual/document/_document.py | 42 ++++++++++++++++--- .../document/_syntax_aware_document.py | 14 ++++++- src/textual/widgets/_text_area.py | 5 ++- tree-sitter/highlights/python.scm | 9 +--- 5 files changed, 76 insertions(+), 23 deletions(-) diff --git a/src/textual/_syntax_theme.py b/src/textual/_syntax_theme.py index 8f7746b852..85fc57a6a8 100644 --- a/src/textual/_syntax_theme.py +++ b/src/textual/_syntax_theme.py @@ -29,15 +29,16 @@ "heading": Style(color="#F92672", bold=True), "regex.punctuation.bracket": Style(color="#F92672"), "regex.operator": Style(color="#F92672"), + "error": Style(color="black", bgcolor="red"), } -BUILTIN_THEMES = { +_BUILTIN_THEMES = { "monokai": _MONOKAI, "bluokai": {**_MONOKAI, "string": Style.parse("cyan")}, } -NULL_STYLE = Style.null() +_NULL_STYLE = Style.null() @dataclass @@ -64,7 +65,7 @@ class SyntaxTheme: """The name of the theme.""" style_mapping: dict[str, Style] = field(default_factory=dict) - """The mapping of names from the `highlight_query` to Rich styles.""" + """The mapping of tree-sitter names from the `highlight_query` to Rich styles.""" highlight_query: str = "" """The tree-sitter query to use for highlighting. @@ -78,14 +79,30 @@ class SyntaxTheme: @classmethod def get_theme(cls, theme_name: str) -> "SyntaxTheme": - return cls(theme_name, BUILTIN_THEMES.get(theme_name, {})) + """Get a `SyntaxTheme` by name. + + Given a `theme_name` return the corresponding `SyntaxTheme` object. + + Check the available `SyntaxTheme`s by calling `SyntaxTheme.available_themes()`. + + Args: + theme_name: The name of the theme. + + Returns: + The `SyntaxTheme` corresponding to the name. + """ + return cls(theme_name, _BUILTIN_THEMES.get(theme_name, {})) def get_highlight(self, name: str) -> Style: - return self.style_mapping.get(name, NULL_STYLE) + """Return the Rich style corresponding to the name defined in the tree-sitter + highlight query for the current theme.""" + return self.style_mapping.get(name, _NULL_STYLE) @classmethod def available_themes(cls) -> list[SyntaxTheme]: - return [SyntaxTheme(name, mapping) for name, mapping in BUILTIN_THEMES.items()] + """A list of all available SyntaxThemes.""" + return [SyntaxTheme(name, mapping) for name, mapping in _BUILTIN_THEMES.items()] DEFAULT_SYNTAX_THEME = SyntaxTheme.get_theme("monokai") +"""The default syntax highlighting theme used by Textual.""" diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 785880b8da..811e163b8c 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -12,12 +12,23 @@ from textual.geometry import Size Newline = Literal["\r\n", "\n", "\r"] +"""The type representing valid line separators.""" VALID_NEWLINES = set(get_args(Newline)) +"""The set of valid line separator strings.""" @lru_cache(maxsize=1024) def _utf8_encode(text: str) -> bytes: - """Encode the input text as utf-8 bytes.""" + """Encode the input text as utf-8 bytes. + + The returned encoded bytes may be retrieved from a cache. + + Args: + text: The text to encode. + + Returns: + The utf-8 bytes representing the input string. + """ return text.encode("utf-8") @@ -148,9 +159,17 @@ def newline(self) -> Newline: """Get the Newline used in this document (e.g. '\r\n', '\n'. etc.)""" return self._newline - def get_size(self, indent_width: int) -> Size: + def get_size(self, tab_width: int) -> Size: + """The Size of the document, taking into account the tab rendering width. + + Args: + tab_width: The width to use for tab indents. + + Returns: + The size (width, height) of the document. + """ lines = self._lines - cell_lengths = [cell_len(line.expandtabs(indent_width)) for line in lines] + cell_lengths = [cell_len(line.expandtabs(tab_width)) for line in lines] max_cell_length = max(cell_lengths or [1]) height = len(lines) return Size(max_cell_length, height) @@ -225,6 +244,11 @@ def delete_range(self, start: Location, end: Location) -> str: def get_text_range(self, start: Location, end: Location) -> str: """Get the text that falls between the start and end locations. + Returns the text between `start` and `end`, including the appropriate + line separator character as specified by `Document._newline`. Note that + `_newline` is set automatically to the first line separator character + found in the document. + Args: start: The start location of the selection. end: The end location of the selection. @@ -271,8 +295,16 @@ def get_line_text(self, index: int) -> Text: line_string = line_string.replace("\n", "").replace("\r", "") return Text(line_string, end="") - def __getitem__(self, item: SupportsIndex | slice) -> str: - return self._lines[item] + def __getitem__(self, line_index: SupportsIndex | slice) -> str | list[str]: + """Return the content of a line as a string, excluding newline characters. + + Args: + line_index: The index or slice of the line(s) to retrieve. + + Returns: + The line or list of lines requested. + """ + return self._lines[line_index] Location = Tuple[int, int] diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index fd6f97b061..6ec3b5b320 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -177,6 +177,12 @@ def get_line_text(self, line_index: int) -> Text: def _tree_sitter_byte_offset(self, location: tuple[int, int]) -> int: """Given a document coordinate, return the byte offset of that coordinate. This method only does work if tree-sitter was imported, otherwise it returns 0. + + Args: + location: The location to convert. + + Returns: + An integer byte offset for the given location. """ if not TREE_SITTER: return 0 @@ -200,8 +206,12 @@ def _prepare_highlights( start_point: tuple[int, int] | None = None, end_point: tuple[int, int] = None, ) -> None: - """Query the tree for ranges to highlights, and update the internal - highlights mapping.""" + """Query the tree for ranges to highlights, and update the internal highlights mapping. + + Args: + start_point: The point to start looking for highlights from. + end_point: The point to look for highlights to. + """ if not TREE_SITTER: return None diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 4169a18312..de80a5ce3a 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -230,7 +230,7 @@ class TextArea(ScrollView, can_focus=True): indent_width: Reactive[int] = reactive(4) """The width of tabs or the number of spaces to insert on pressing the `tab` key.""" - show_width_guide: Reactive[bool] = reactive(False) + _show_width_guide: Reactive[bool] = reactive(False) """If True, a vertical line will indicate the width of the document.""" def __init__( @@ -382,7 +382,8 @@ def render_line(self, widget_y: int) -> Strip: line.stylize(cursor_style, cursor_column, cursor_column + 1) line.stylize_before(active_line_style) - if self.show_width_guide: + # The width guide is a visual indicator showing the virtual width of the TextArea widget. + if self._show_width_guide: width_guide_style = self.get_component_rich_style("text-area--width-guide") width_column = virtual_width - self.gutter_width line.stylize_before(width_guide_style, width_column - 1, width_column) diff --git a/tree-sitter/highlights/python.scm b/tree-sitter/highlights/python.scm index bd2fe2ffc7..249306b58d 100644 --- a/tree-sitter/highlights/python.scm +++ b/tree-sitter/highlights/python.scm @@ -1,5 +1,6 @@ ;; From tree-sitter-python licensed under MIT License ; Copyright (c) 2016 Max Brunsfeld +; Adapted for Textual from: ; https://github.com/nvim-treesitter/nvim-treesitter/blob/f95ffd09ed35880c3a46ad2b968df361fa592a76/queries/python/highlights.scm ; Variables @@ -282,14 +283,6 @@ (else_clause "else" @exception)) -["(" ")" "[" "]" "{" "}"] @punctuation.bracket - -(interpolation - "{" @punctuation.special - "}" @punctuation.special) - -["," "." ":" ";" (ellipsis)] @punctuation.delimiter - ;; Class definitions (class_definition name: (identifier) @type) From de774cd82ca663cd8d604f7fe6306d3f43de489b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 12:19:05 +0100 Subject: [PATCH 169/366] Updating selection snapshot output --- src/textual/widgets/_text_area.py | 5 +- .../__snapshots__/test_snapshots.ambr | 423 +++++++++--------- 2 files changed, 217 insertions(+), 211 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index de80a5ce3a..47df9604df 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -500,11 +500,14 @@ def edit(self, edit: Edit) -> Any: async def _on_key(self, event: events.Key) -> None: """Handle key presses which correspond to document inserts.""" key = event.key + + if key == "escape": + self.app.action_focus_next() + insert_values = { "tab": " " * self.indent_width if self.indent_type == "spaces" else "\t", "enter": "\n", } - if event.is_printable or key in insert_values: event.stop() event.prevent_default() diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 26426a21e9..b800a59abd 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -28137,168 +28137,169 @@ font-weight: 700; } - .terminal-3215715892-matrix { + .terminal-2767018341-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3215715892-title { + .terminal-2767018341-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3215715892-r1 { fill: #e4e5e6 } - .terminal-3215715892-r2 { fill: #1b1b1b } - .terminal-3215715892-r3 { fill: #c5c8c6 } - .terminal-3215715892-r4 { fill: #7b7e82 } - .terminal-3215715892-r5 { fill: #e2e3e3 } - .terminal-3215715892-r6 { fill: #e6db74 } - .terminal-3215715892-r7 { fill: #ae81ff } + .terminal-2767018341-r1 { fill: #e4e5e6 } + .terminal-2767018341-r2 { fill: #1b1b1b } + .terminal-2767018341-r3 { fill: #c5c8c6 } + .terminal-2767018341-r4 { fill: #7b7e82 } + .terminal-2767018341-r5 { fill: #e2e3e3 } + .terminal-2767018341-r6 { fill: #e6db74 } + .terminal-2767018341-r7 { fill: #ae81ff } + .terminal-2767018341-r8 { fill: #4b4e55 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  { -  2      "name": "John Doe",                            -  3      "age": 30,                                     -  4      "isStudent": false,                            -  5      "address": {                                   -  6          "street": "123 Main St",                   -  7          "city": "Anytown",                         -  8          "state": "CA",                             -  9          "zip": "12345" - 10      },                                             - 11      "phoneNumbers": [                              - 12          {                                          - 13              "type": "home",                        - 14              "number": "555-555-1234" - 15          },                                         - 16          {                                          - 17              "type": "work",                        - 18              "number": "555-555-5678" - 19          }                                          - 20      ],                                             - 21      "hobbies": ["reading""hiking""swimming"],  - 22      "pets": [                                      - 23          {                                          - 24              "type": "dog",                         - 25              "name": "Fido" - 26          },                                         - 27      ],                                             - 28      "graduationYear": null                         - 29  }                                                  - 30   - 31   + + + +  1  { +  2      "name": "John Doe",                            +  3      "age": 30,                                     +  4      "isStudent": false,                            +  5      "address": {                                   +  6          "street": "123 Main St",                   +  7          "city": "Anytown",                         +  8          "state": "CA",                             +  9          "zip": "12345" + 10      },                                             + 11      "phoneNumbers": [                              + 12          {                                          + 13              "type": "home",                        + 14              "number": "555-555-1234" + 15          },                                         + 16          {                                          + 17              "type": "work",                        + 18              "number": "555-555-5678" + 19          }                                          + 20      ],                                             + 21      "hobbies": ["reading""hiking""swimming"],  + 22      "pets": [                                      + 23          {                                          + 24              "type": "dog",                         + 25              "name": "Fido" + 26          }, + 27      ],                                             + 28      "graduationYear": null                         + 29  }                                                  + 30   + 31   @@ -29866,61 +29867,61 @@ font-weight: 700; } - .terminal-1898545512-matrix { + .terminal-3303661732-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1898545512-title { + .terminal-3303661732-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1898545512-r1 { fill: #7b7e82 } - .terminal-1898545512-r2 { fill: #c5c8c6 } - .terminal-1898545512-r3 { fill: #dde6ed } - .terminal-1898545512-r4 { fill: #e2e3e3 } - .terminal-1898545512-r5 { fill: #004578 } - .terminal-1898545512-r6 { fill: #e4e5e6 } - .terminal-1898545512-r7 { fill: #1b1b1b } + .terminal-3303661732-r1 { fill: #7b7e82 } + .terminal-3303661732-r2 { fill: #c5c8c6 } + .terminal-3303661732-r3 { fill: #4b4e55 } + .terminal-3303661732-r4 { fill: #e2e3e3 } + .terminal-3303661732-r5 { fill: #004578 } + .terminal-3303661732-r6 { fill: #e4e5e6 } + .terminal-3303661732-r7 { fill: #1b1b1b } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline. - 2   - 3  Iamanotherline.          - 4   - 5  Iamthefinalline + + + + 1  Iamaline. + 2   + 3  Iamanotherline. + 4   + 5  Iamthefinalline. @@ -29950,61 +29951,61 @@ font-weight: 700; } - .terminal-3929544230-matrix { + .terminal-3329921234-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3929544230-title { + .terminal-3329921234-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3929544230-r1 { fill: #e4e5e6 } - .terminal-3929544230-r2 { fill: #1b1b1b } - .terminal-3929544230-r3 { fill: #dde6ed } - .terminal-3929544230-r4 { fill: #c5c8c6 } - .terminal-3929544230-r5 { fill: #7b7e82 } - .terminal-3929544230-r6 { fill: #004578 } - .terminal-3929544230-r7 { fill: #e2e3e3 } + .terminal-3329921234-r1 { fill: #e4e5e6 } + .terminal-3329921234-r2 { fill: #1b1b1b } + .terminal-3329921234-r3 { fill: #4b4e55 } + .terminal-3329921234-r4 { fill: #c5c8c6 } + .terminal-3329921234-r5 { fill: #7b7e82 } + .terminal-3329921234-r6 { fill: #004578 } + .terminal-3329921234-r7 { fill: #e2e3e3 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline. - 2   - 3  Iamanotherline.    - 4   - 5  Iamthefinalline + + + + 1  Iamaline. + 2   + 3  Iamanotherline. + 4   + 5  Iamthefinalline. @@ -30034,61 +30035,61 @@ font-weight: 700; } - .terminal-2787989997-matrix { + .terminal-331256201-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2787989997-title { + .terminal-331256201-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2787989997-r1 { fill: #e4e5e6 } - .terminal-2787989997-r2 { fill: #1b1b1b } - .terminal-2787989997-r3 { fill: #dde6ed } - .terminal-2787989997-r4 { fill: #c5c8c6 } - .terminal-2787989997-r5 { fill: #7b7e82 } - .terminal-2787989997-r6 { fill: #004578 } - .terminal-2787989997-r7 { fill: #e2e3e3 } + .terminal-331256201-r1 { fill: #e4e5e6 } + .terminal-331256201-r2 { fill: #1b1b1b } + .terminal-331256201-r3 { fill: #4b4e55 } + .terminal-331256201-r4 { fill: #c5c8c6 } + .terminal-331256201-r5 { fill: #7b7e82 } + .terminal-331256201-r6 { fill: #004578 } + .terminal-331256201-r7 { fill: #e2e3e3 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline. - 2   - 3  Iamanotherline. - 4   - 5  Iamthefinalline + + + + 1  Iamaline. + 2   + 3  Iamanotherline. + 4   + 5  Iamthefinalline. @@ -30118,61 +30119,61 @@ font-weight: 700; } - .terminal-208589394-matrix { + .terminal-2227222898-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-208589394-title { + .terminal-2227222898-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-208589394-r1 { fill: #7b7e82 } - .terminal-208589394-r2 { fill: #c5c8c6 } - .terminal-208589394-r3 { fill: #dde6ed } - .terminal-208589394-r4 { fill: #e2e3e3 } - .terminal-208589394-r5 { fill: #004578 } - .terminal-208589394-r6 { fill: #e4e5e6 } - .terminal-208589394-r7 { fill: #1b1b1b } + .terminal-2227222898-r1 { fill: #7b7e82 } + .terminal-2227222898-r2 { fill: #c5c8c6 } + .terminal-2227222898-r3 { fill: #4b4e55 } + .terminal-2227222898-r4 { fill: #e2e3e3 } + .terminal-2227222898-r5 { fill: #004578 } + .terminal-2227222898-r6 { fill: #e4e5e6 } + .terminal-2227222898-r7 { fill: #1b1b1b } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline. - 2   - 3  Iamanotherline. - 4   - 5  Iamthefinalline. + + + + 1  Iamaline. + 2   + 3  Iamanotherline. + 4   + 5  Iamthefinalline. @@ -30202,59 +30203,60 @@ font-weight: 700; } - .terminal-1589249993-matrix { + .terminal-2147792900-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1589249993-title { + .terminal-2147792900-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1589249993-r1 { fill: #7b7e82 } - .terminal-1589249993-r2 { fill: #c5c8c6 } - .terminal-1589249993-r3 { fill: #e2e3e3 } - .terminal-1589249993-r4 { fill: #e4e5e6 } - .terminal-1589249993-r5 { fill: #1b1b1b } + .terminal-2147792900-r1 { fill: #7b7e82 } + .terminal-2147792900-r2 { fill: #c5c8c6 } + .terminal-2147792900-r3 { fill: #4b4e55 } + .terminal-2147792900-r4 { fill: #e2e3e3 } + .terminal-2147792900-r5 { fill: #e4e5e6 } + .terminal-2147792900-r6 { fill: #1b1b1b } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline.          - 2   - 3  Iamanotherline.    - 4   - 5  Iamthefinalline + + + + 1  Iamaline. + 2   + 3  Iamanotherline. + 4   + 5  Iamthefinalline. @@ -30284,59 +30286,60 @@ font-weight: 700; } - .terminal-1622228738-matrix { + .terminal-4132495341-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1622228738-title { + .terminal-4132495341-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1622228738-r1 { fill: #7b7e82 } - .terminal-1622228738-r2 { fill: #c5c8c6 } - .terminal-1622228738-r3 { fill: #e2e3e3 } - .terminal-1622228738-r4 { fill: #e4e5e6 } - .terminal-1622228738-r5 { fill: #1b1b1b } + .terminal-4132495341-r1 { fill: #7b7e82 } + .terminal-4132495341-r2 { fill: #c5c8c6 } + .terminal-4132495341-r3 { fill: #4b4e55 } + .terminal-4132495341-r4 { fill: #e2e3e3 } + .terminal-4132495341-r5 { fill: #e4e5e6 } + .terminal-4132495341-r6 { fill: #1b1b1b } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline.          - 2   - 3  Iamanotherline.          - 4   - 5  Iamthefinalline + + + + 1  Iamaline. + 2   + 3  Iamanotherline. + 4   + 5  Iamthefinalline. From ef2e38ebfcb64d1009e542cecb62f7e0e1afff89 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 12:22:36 +0100 Subject: [PATCH 170/366] Binding for ESC to shift focus --- src/textual/widgets/_text_area.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 47df9604df..36ccaf2715 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -168,6 +168,7 @@ class TextArea(ScrollView, can_focus=True): } BINDINGS = [ + Binding("escape", "screen.focus_next", "focus next", show=False), # Cursor movement Binding("up", "cursor_up", "cursor up", show=False), Binding("down", "cursor_down", "cursor down", show=False), @@ -500,10 +501,6 @@ def edit(self, edit: Edit) -> Any: async def _on_key(self, event: events.Key) -> None: """Handle key presses which correspond to document inserts.""" key = event.key - - if key == "escape": - self.app.action_focus_next() - insert_values = { "tab": " " * self.indent_width if self.indent_type == "spaces" else "\t", "enter": "\n", From c2d927bff6643b4cb5509948c247704d0736e707 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 12:44:29 +0100 Subject: [PATCH 171/366] Only build the tree-sitter query once! --- src/textual/document/_syntax_aware_document.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 6ec3b5b320..e9c49ce91f 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -72,6 +72,9 @@ def __init__( self._syntax_theme.highlight_query = highlight_query_path.read_text() self._syntax_tree = self._build_ast(self._parser) + self._query: Query = self._language.query( + self._syntax_theme.highlight_query + ) self._prepare_highlights() def insert_range( @@ -216,7 +219,6 @@ def _prepare_highlights( return None highlights = self._highlights - query: Query = self._language.query(self._syntax_theme.highlight_query) captures_kwargs = {} if start_point is not None: @@ -225,7 +227,7 @@ def _prepare_highlights( captures_kwargs["end_point"] = end_point # We could optimise by only preparing highlights for a subset of lines here. - captures = query.captures(self._syntax_tree.root_node, **captures_kwargs) + captures = self._query.captures(self._syntax_tree.root_node, **captures_kwargs) highlight_updates: dict[int, list[Highlight]] = defaultdict(list) for capture in captures: From a422bf6b9e67bc8299e73f16a6ff8e86bbdd6466 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 12:47:12 +0100 Subject: [PATCH 172/366] Expand cursor scroll horizontal leeway in TextArea --- src/textual/widgets/_text_area.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 36ccaf2715..9dbd6ae45e 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -605,7 +605,7 @@ def scroll_cursor_visible(self) -> Offset: text = self.cursor_line_text[:column] column_offset = cell_len(text.expandtabs(self.indent_width)) scroll_offset = self.scroll_to_region( - Region(x=column_offset, y=row, width=2, height=1), + Region(x=column_offset, y=row, width=3, height=1), spacing=Spacing(right=self.gutter_width), animate=False, force=True, From 5ce9d457161ab48cbd3547202dedf0c109b55c95 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 12:47:51 +0100 Subject: [PATCH 173/366] Property setter for cursor_location in TextArea shouldnt return value --- src/textual/widgets/_text_area.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 9dbd6ae45e..4174882eff 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -618,14 +618,13 @@ def cursor_location(self) -> Location: return self.selection.end @cursor_location.setter - def cursor_location(self, new_location: Location) -> Location: + def cursor_location(self, new_location: Location) -> None: """Set the cursor_location to a new location. If a selection is in progress, the anchor point will remain. """ start, end = self.selection self.selection = Selection(start, new_location) - return self.selection.end @property def cursor_at_first_row(self) -> bool: From 3d356751b749f3148c7b0c3e1c70a66deb4ad289 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 13:46:22 +0100 Subject: [PATCH 174/366] Avoiding NamedTuple subclassing - using type aliasing instead --- src/textual/document/_document.py | 8 -------- .../document/_syntax_aware_document.py | 20 ++++++++++--------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 811e163b8c..2049dcf7fd 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -349,11 +349,3 @@ def range(self) -> tuple[Location, Location]: where the minimum point is inclusive and the maximum point is exclusive.""" start, end = self return _fix_direction(start, end) - - -class Highlight(NamedTuple): - """A range to highlight within a single line""" - - start_column: int | None - end_column: int | None - highlight_name: str | None diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index e9c49ce91f..f909f0c828 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -3,6 +3,7 @@ from collections import defaultdict from functools import lru_cache from pathlib import Path +from typing import Optional, Tuple from rich.text import Text from typing_extensions import TYPE_CHECKING @@ -22,11 +23,16 @@ from textual._fix_direction import _fix_direction from textual._languages import VALID_LANGUAGES -from textual.document._document import Document, Highlight, _utf8_encode +from textual.document._document import Document, _utf8_encode TREE_SITTER_PATH = Path(__file__) / "../../../../tree-sitter/" HIGHLIGHTS_PATH = TREE_SITTER_PATH / "highlights/" +StartColumn = Optional[int] +EndColumn = Optional[int] +HighlightName = Optional[str] +Highlight = Tuple[StartColumn, EndColumn, HighlightName] + class SyntaxAwareDocument(Document): """A wrapper around a Document which also maintains a tree-sitter syntax @@ -236,25 +242,21 @@ def _prepare_highlights( node_end_row, node_end_column = node.end_point if node_start_row == node_end_row: - highlight = Highlight( - node_start_column, node_end_column, highlight_name - ) + highlight = (node_start_column, node_end_column, highlight_name) highlight_updates[node_start_row].append(highlight) else: # Add the first line of the node range highlight_updates[node_start_row].append( - Highlight(node_start_column, None, highlight_name) + (node_start_column, None, highlight_name) ) # Add the middle lines - entire row of this node is highlighted for node_row in range(node_start_row + 1, node_end_row): - highlight_updates[node_row].append( - Highlight(0, None, highlight_name) - ) + highlight_updates[node_row].append((0, None, highlight_name)) # Add the last line of the node range highlight_updates[node_end_row].append( - Highlight(0, node_end_column, highlight_name) + (0, node_end_column, highlight_name) ) for line_index, updated_highlights in highlight_updates.items(): From 85a3ed7eed9de8690c2b1f75a75c4d1525fa57a4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 14:34:30 +0100 Subject: [PATCH 175/366] Tidying API, docstrings etc. --- src/textual/document/__init__.py | 21 ++++++++++++++ src/textual/{ => document}/_syntax_theme.py | 9 ++++++ src/textual/widgets/_text_area.py | 32 ++++++++++++++++++--- 3 files changed, 58 insertions(+), 4 deletions(-) rename src/textual/{ => document}/_syntax_theme.py (94%) diff --git a/src/textual/document/__init__.py b/src/textual/document/__init__.py index e69de29bb2..1b940ddcc9 100644 --- a/src/textual/document/__init__.py +++ b/src/textual/document/__init__.py @@ -0,0 +1,21 @@ +from ._document import Document, Location, Selection +from ._syntax_aware_document import ( + EndColumn, + Highlight, + HighlightName, + StartColumn, + SyntaxAwareDocument, +) +from ._syntax_theme import DEFAULT_SYNTAX_THEME, SyntaxTheme + +__all__ = [ + "Document", + "EndColumn", + "Highlight", + "HighlightName", + "Location", + "Selection", + "StartColumn", + "SyntaxAwareDocument", + "SyntaxTheme", +] diff --git a/src/textual/_syntax_theme.py b/src/textual/document/_syntax_theme.py similarity index 94% rename from src/textual/_syntax_theme.py rename to src/textual/document/_syntax_theme.py index 85fc57a6a8..7ccad1874e 100644 --- a/src/textual/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -103,6 +103,15 @@ def available_themes(cls) -> list[SyntaxTheme]: """A list of all available SyntaxThemes.""" return [SyntaxTheme(name, mapping) for name, mapping in _BUILTIN_THEMES.items()] + @classmethod + def default(cls) -> SyntaxTheme: + """Get the default syntax theme. + + Returns: + The default SyntaxTheme (probably Monokai). + """ + return DEFAULT_SYNTAX_THEME + DEFAULT_SYNTAX_THEME = SyntaxTheme.get_theme("monokai") """The default syntax highlighting theme used by Textual.""" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 4174882eff..0d33090bb3 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -7,16 +7,23 @@ from rich.style import Style from rich.text import Text +from textual._languages import VALID_LANGUAGES + if TYPE_CHECKING: from tree_sitter import Language from textual import events, log from textual._cells import cell_len from textual._fix_direction import _fix_direction -from textual._syntax_theme import DEFAULT_SYNTAX_THEME, SyntaxTheme from textual._types import Literal, Protocol, runtime_checkable from textual.binding import Binding -from textual.document._document import Document, Location, Selection +from textual.document import ( + DEFAULT_SYNTAX_THEME, + Document, + Location, + Selection, + SyntaxTheme, +) from textual.events import MouseEvent from textual.geometry import Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive @@ -208,9 +215,12 @@ class TextArea(ScrollView, can_focus=True): This must be set to a valid, non-None value for syntax highlighting to work. + Check valid languages using the `TextArea.valid_languages` property. + If the value is a string, a built-in parser will be used. - If a tree-sitter `Language` object is used, + If you wish to add support for an unsupported language, you'll have to pass in the + tree-sitter `Language` object directly rather than the string language name. """ theme: Reactive[str | SyntaxTheme] = reactive(DEFAULT_SYNTAX_THEME) @@ -223,7 +233,16 @@ class TextArea(ScrollView, can_focus=True): """ selection: Reactive[Selection] = reactive(Selection(), always_update=True) - """The selection start and end locations (zero-based line_index, offset).""" + """The selection start and end locations (zero-based line_index, offset). + + This represents the cursor location and the current selection. + + The `Selection.end` always refers to the cursor location. + + If no text is selected, then `Selection.end == Selection.start`. + + The text selected in the document is available via the `TextArea.selected_text` property. + """ show_line_numbers: Reactive[bool] = reactive(True) """True to show the line number column, otherwise False.""" @@ -315,6 +334,11 @@ def load_text(self, text: str) -> None: self._reload_document() self._refresh_size() + @property + def valid_languages(self) -> list[str]: + """The list of valid language names.""" + return VALID_LANGUAGES + def _refresh_size(self) -> None: """Calculate the size of the document.""" width, height = self._document.get_size(self.indent_width) From 154c30c038f4fc2c430cb1e26093b854b785edfa Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 15:39:22 +0100 Subject: [PATCH 176/366] Tidying the API and docstrings --- src/textual/document/__init__.py | 2 + src/textual/{ => document}/_languages.py | 0 .../document/_syntax_aware_document.py | 5 +- src/textual/widgets/_text_area.py | 47 +++++++++++++++---- 4 files changed, 42 insertions(+), 12 deletions(-) rename src/textual/{ => document}/_languages.py (100%) diff --git a/src/textual/document/__init__.py b/src/textual/document/__init__.py index 1b940ddcc9..3933b0b7b6 100644 --- a/src/textual/document/__init__.py +++ b/src/textual/document/__init__.py @@ -1,4 +1,5 @@ from ._document import Document, Location, Selection +from ._languages import VALID_LANGUAGES from ._syntax_aware_document import ( EndColumn, Highlight, @@ -18,4 +19,5 @@ "StartColumn", "SyntaxAwareDocument", "SyntaxTheme", + "VALID_LANGUAGES", ] diff --git a/src/textual/_languages.py b/src/textual/document/_languages.py similarity index 100% rename from src/textual/_languages.py rename to src/textual/document/_languages.py diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index f909f0c828..606279b98d 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -8,8 +8,6 @@ from rich.text import Text from typing_extensions import TYPE_CHECKING -from textual._syntax_theme import SyntaxTheme - try: from tree_sitter_languages import get_language, get_parser @@ -22,8 +20,9 @@ TREE_SITTER = False from textual._fix_direction import _fix_direction -from textual._languages import VALID_LANGUAGES from textual.document._document import Document, _utf8_encode +from textual.document._languages import VALID_LANGUAGES +from textual.document._syntax_theme import SyntaxTheme TREE_SITTER_PATH = Path(__file__) / "../../../../tree-sitter/" HIGHLIGHTS_PATH = TREE_SITTER_PATH / "highlights/" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 0d33090bb3..88afab0469 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -7,8 +7,6 @@ from rich.style import Style from rich.text import Text -from textual._languages import VALID_LANGUAGES - if TYPE_CHECKING: from tree_sitter import Language @@ -209,6 +207,32 @@ class TextArea(ScrollView, can_focus=True): ), Binding("ctrl+k", "delete_to_end_of_line", "delete to line end", show=False), ] + """ + | Key(s) | Description | + | :- | :- | + | escape | Focus on the next item. | + | up | Move the cursor up. | + | down | Move the cursor down. | + | left | Move the cursor left. | + | ctrl+left | Move the cursor to the start of the word. | + | right | Move the cursor right. | + | ctrl+right | Move the cursor to the end of the word. | + | home,ctrl+a | Move the cursor to the start of the line. | + | end,ctrl+e | Move the cursor to the end of the line. | + | pageup | Move the cursor one page up. | + | pagedown | Move the cursor one page down. | + | shift+up | Select while moving the cursor up. | + | shift+down | Select while moving the cursor down. | + | shift+left | Select while moving the cursor left. | + | shift+right | Select while moving the cursor right. | + | backspace | Delete character to the left of cursor. | + | ctrl+w | Delete from cursor to start of the word. | + | delete,ctrl+d | Delete character to the right of cursor. | + | ctrl+f | Delete from cursor to end of the word. | + | ctrl+x | Delete the current line. | + | ctrl+u | Delete from cursor to the start of the line. | + | ctrl+k | Delete from cursor to the end of the line. | + """ language: Reactive[str | "Language" | None] = reactive(None, always_update=True) """The language to use. @@ -245,10 +269,16 @@ class TextArea(ScrollView, can_focus=True): """ show_line_numbers: Reactive[bool] = reactive(True) - """True to show the line number column, otherwise False.""" + """True to show the line number column on the left edge, otherwise False. + + Changing this value will immediately re-render the `TextArea`.""" indent_width: Reactive[int] = reactive(4) - """The width of tabs or the number of spaces to insert on pressing the `tab` key.""" + """The width of tabs or the number of spaces to insert on pressing the `tab` key. + + If the document currently open contains tabs that are currently visible on screen, + altering this value will immediately change the display width of the visible tabs. + """ _show_width_guide: Reactive[bool] = reactive(False) """If True, a vertical line will indicate the width of the document.""" @@ -334,11 +364,6 @@ def load_text(self, text: str) -> None: self._reload_document() self._refresh_size() - @property - def valid_languages(self) -> list[str]: - """The list of valid language names.""" - return VALID_LANGUAGES - def _refresh_size(self) -> None: """Calculate the size of the document.""" width, height = self._document.get_size(self.indent_width) @@ -606,6 +631,10 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: # --- Cursor/selection utilities def clamp_visitable(self, location: Location) -> Location: + """Clamp the given location to the nearest visitable location. + + Clamps the given location the nearest location which could be navigated to + """ document = self._document row, column = location From ddf6c59ed141fcda172b223aa476d2bc1f35d2b1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 16:57:46 +0100 Subject: [PATCH 177/366] TextArea additional cursor tests --- src/textual/widgets/_text_area.py | 5 +- tests/document/test_document.py | 2 +- tests/document/test_document_delete.py | 2 +- tests/snapshot_tests/test_snapshots.py | 3 +- tests/text_area/test_edit.py | 81 +++++++++++++++++++++ tests/text_area/test_selection.py | 97 +++++++++++++++++++++++++- 6 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 tests/text_area/test_edit.py diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 88afab0469..38af659b8d 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -677,7 +677,10 @@ def cursor_location(self, new_location: Location) -> None: If a selection is in progress, the anchor point will remain. """ start, end = self.selection - self.selection = Selection(start, new_location) + if start != end: + self.selection = Selection(start, new_location) + else: + self.selection = Selection.cursor(new_location) @property def cursor_at_first_row(self) -> bool: diff --git a/tests/document/test_document.py b/tests/document/test_document.py index ca4e2036f4..af232fafb2 100644 --- a/tests/document/test_document.py +++ b/tests/document/test_document.py @@ -1,6 +1,6 @@ import pytest -from textual.document._document import Document +from textual.document import Document TEXT = """I must not fear. Fear is the mind-killer.""" diff --git a/tests/document/test_document_delete.py b/tests/document/test_document_delete.py index 442ddcf7ea..9bd797c6c8 100644 --- a/tests/document/test_document_delete.py +++ b/tests/document/test_document_delete.py @@ -1,6 +1,6 @@ import pytest -from textual.document._document import Document +from textual.document import Document TEXT = """I must not fear. Fear is the mind-killer. diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index fcd02cb866..8b8299f587 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -3,8 +3,7 @@ import pytest from tests.snapshot_tests.language_snippets import SNIPPETS -from textual._languages import VALID_LANGUAGES -from textual.document._document import Selection +from textual.document import Selection, VALID_LANGUAGES from textual.widgets import TextArea # These paths should be relative to THIS directory. diff --git a/tests/text_area/test_edit.py b/tests/text_area/test_edit.py new file mode 100644 index 0000000000..6ad0f4f66d --- /dev/null +++ b/tests/text_area/test_edit.py @@ -0,0 +1,81 @@ +from textual.app import App, ComposeResult +from textual.widgets import TextArea + +TEXT = """I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.load_text(TEXT) + yield text_area + + +async def test_insert_text_start(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert_text("Hello") + assert text_area.text == "Hello" + TEXT + assert text_area.cursor_location == (0, 5) + + +async def test_insert_multiline_text(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.cursor_location = (2, 5) + text_area.insert_text("Hello,\nworld!") + assert text_area.cursor_location == (3, 6) + assert ( + text_area.text + == """I must not fear. +Fear is the mind-killer. +Fear Hello, +world!is the little-death that brings total obliteration. +I will face my fear. +""" + ) + + +async def test_delete_left(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("Hello, world!") + text_area.cursor_location = (0, 6) + await pilot.press("backspace") + assert text_area.text == "Hello world!" + + +async def test_delete_left_start(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("Hello, world!") + await pilot.press("backspace") + assert text_area.text == "Hello, world!" + + +async def test_delete_left_end(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("Hello, world!") + text_area.cursor_location = (0, 13) + await pilot.press("backspace") + assert text_area.text == "Hello, world" + + +async def test_delete_right(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("Hello, world!") + text_area.cursor_location = (0, 13) + await pilot.press("delete") + assert text_area.text == "Hello, world" diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 1b469e4c0c..98dbc5fef1 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -1,5 +1,7 @@ +import pytest + from textual.app import App, ComposeResult -from textual.document._document import Selection +from textual.document import Selection from textual.geometry import Offset from textual.widgets import TextArea @@ -149,3 +151,96 @@ async def test_cursor_selection_left_to_previous_line(): # The cursor jumps up to the end of the line above. end_of_previous_line = len(TEXT.splitlines()[1]) assert text_area.selection == Selection((2, 2), (1, end_of_previous_line)) + + +async def test_cursor_to_line_end(): + """You can use the keyboard to jump the cursor to the end of the current line.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 2)) + await pilot.press("end") + eol_index = len(TEXT.splitlines()[2]) + assert text_area.cursor_location == (2, eol_index) + assert text_area.selection.is_empty + + +async def test_cursor_to_line_home(): + """You can use the keyboard to jump the cursor to the start of the current line.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 2)) + await pilot.press("home") + assert text_area.cursor_location == (2, 0) + assert text_area.selection.is_empty + + +@pytest.mark.parametrize( + "start,end", + [ + ((0, 0), (0, 0)), + ((0, 4), (0, 3)), + ((1, 0), (0, 16)), + ], +) +async def test_get_cursor_left_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.cursor_location = start + assert text_area.get_cursor_left_location() == end + + +@pytest.mark.parametrize( + "start,end", + [ + ((0, 0), (0, 1)), + ((0, 16), (1, 0)), + ((3, 20), (4, 0)), + ], +) +async def test_get_cursor_right_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.cursor_location = start + assert text_area.get_cursor_right_location() == end + + +@pytest.mark.parametrize( + "start,end", + [ + ((0, 4), (0, 0)), # jump to start + ((1, 2), (0, 2)), # go to column above + ((2, 56), (1, 24)), # snap to end of row above + ], +) +async def test_get_cursor_up_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.cursor_location = start + # This is required otherwise the cursor will snap back to the + # last location navigated to (0, 0) + text_area.record_cursor_offset() + assert text_area.get_cursor_up_location() == end + + +@pytest.mark.parametrize( + "start,end", + [ + ((3, 4), (4, 0)), # jump to end + ((1, 2), (2, 2)), # go to column above + ((2, 56), (3, 20)), # snap to end of row below + ], +) +async def test_get_cursor_down_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.cursor_location = start + # This is required otherwise the cursor will snap back to the + # last location navigated to (0, 0) + text_area.record_cursor_offset() + assert text_area.get_cursor_down_location() == end From 05f5363694423ee19ea30048ae67715ee06fae1c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 17:05:35 +0100 Subject: [PATCH 178/366] Testing pageup and pagedown in TextArea --- tests/text_area/test_selection.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 98dbc5fef1..85d7261ef5 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -244,3 +244,27 @@ async def test_get_cursor_down_location(start, end): # last location navigated to (0, 0) text_area.record_cursor_offset() assert text_area.get_cursor_down_location() == end + + +async def test_cursor_page_down(): + """Pagedown moves the cursor down 1 page, retaining column index.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("XXX\n" * 200) + text_area.selection = Selection.cursor((0, 1)) + await pilot.press("pagedown") + assert text_area.selection == Selection.cursor((app.console.height - 1, 1)) + + +async def test_cursor_page_up(): + """Pageup moves the cursor up 1 page, retaining column index.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("XXX\n" * 200) + text_area.selection = Selection.cursor((100, 1)) + await pilot.press("pageup") + assert text_area.selection == Selection.cursor( + (100 - app.console.height + 1, 1) + ) From 6e09716a5664991b0fba776f37299ec7efe3e6ef Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 17:06:42 +0100 Subject: [PATCH 179/366] Fix a faulty test --- tests/text_area/test_edit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/text_area/test_edit.py b/tests/text_area/test_edit.py index 6ad0f4f66d..e5a320cf61 100644 --- a/tests/text_area/test_edit.py +++ b/tests/text_area/test_edit.py @@ -78,4 +78,4 @@ async def test_delete_right(): text_area.load_text("Hello, world!") text_area.cursor_location = (0, 13) await pilot.press("delete") - assert text_area.text == "Hello, world" + assert text_area.text == "Hello, world!" From df0e7b7fcbf414a74d4f2a29079d1e8ad9cec037 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 17:08:53 +0100 Subject: [PATCH 180/366] Docstring in a test for TextArea edit --- tests/text_area/test_edit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/text_area/test_edit.py b/tests/text_area/test_edit.py index e5a320cf61..1a8f0c299e 100644 --- a/tests/text_area/test_edit.py +++ b/tests/text_area/test_edit.py @@ -1,3 +1,6 @@ +"""Tests some edit operations in the TextArea - note that more +extensive testing for editing is done at the Document level.""" + from textual.app import App, ComposeResult from textual.widgets import TextArea From 44e31d7a707baf693a99da0f6bd626fc92787f37 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 17:10:50 +0100 Subject: [PATCH 181/366] Stop using DEFAULT_SYNTAX_THEME --- src/textual/widgets/_text_area.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 38af659b8d..a8276771c9 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -15,13 +15,7 @@ from textual._fix_direction import _fix_direction from textual._types import Literal, Protocol, runtime_checkable from textual.binding import Binding -from textual.document import ( - DEFAULT_SYNTAX_THEME, - Document, - Location, - Selection, - SyntaxTheme, -) +from textual.document import Document, Location, Selection, SyntaxTheme from textual.events import MouseEvent from textual.geometry import Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive @@ -247,7 +241,7 @@ class TextArea(ScrollView, can_focus=True): tree-sitter `Language` object directly rather than the string language name. """ - theme: Reactive[str | SyntaxTheme] = reactive(DEFAULT_SYNTAX_THEME) + theme: Reactive[str | SyntaxTheme] = reactive(SyntaxTheme.default()) """The theme to syntax highlight with. Supply a `SyntaxTheme` object to customise highlighting, or supply a builtin From 0452a5639c86e832b2af91ee643d8b97fc17a49a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 14 Aug 2023 22:43:46 +0100 Subject: [PATCH 182/366] Docstrings --- src/textual/document/_document.py | 9 ++++++++ .../document/_syntax_aware_document.py | 21 ++++++++++++++++++- src/textual/widgets/_text_area.py | 1 + 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 2049dcf7fd..5a25f76961 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -124,6 +124,12 @@ def get_size(self, indent_width: int) -> Size: def line_count(self) -> int: """Returns the number of lines in the document.""" + @abstractmethod + def tree_query(self, tree_query: str) -> list[object]: + """Query the syntax tree, if available for the current + document. If no syntax tree is available, the returned + list will be empty.""" + class Document(DocumentBase): """A document which can be opened in a TextArea.""" @@ -295,6 +301,9 @@ def get_line_text(self, index: int) -> Text: line_string = line_string.replace("\n", "").replace("\r", "") return Text(line_string, end="") + def tree_query(self, tree_query: str) -> list[object]: + return [] + def __getitem__(self, line_index: SupportsIndex | slice) -> str | list[str]: """Return the content of a line as a string, excluding newline characters. diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 606279b98d..7d055b3a3b 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -35,7 +35,17 @@ class SyntaxAwareDocument(Document): """A wrapper around a Document which also maintains a tree-sitter syntax - tree when the document is edited.""" + tree when the document is edited. + + The primary reason for this split is actually to keep tree-sitter stuff separate, + since it isn't supported in Python 3.7. By having the tree-sitter code + isolated in this subclass, it makes it easier to conditionally import. However, + it does come with other design flaws (e.g. Document is required to have methods + which only really make sense on SyntaxAwareDocument). + + If you're reading this and Python 3.7 is no longer supported by Textual, + consider merging this subclass into the `Document` superclass. + """ def __init__( self, text: str, language: str | Language, syntax_theme: str | SyntaxTheme @@ -104,6 +114,7 @@ def insert_range( end_location = super().insert_range(start, end, text) + # TODO: columns in tree-sitter points appear to be byte-offsets if TREE_SITTER: text_byte_length = len(_utf8_encode(text)) self._syntax_tree.edit( @@ -182,6 +193,14 @@ def get_line_text(self, line_index: int) -> Text: return line + def tree_query(self, tree_query: str) -> list[object]: + """Query the syntax tree.""" + query = self._language.query(tree_query) + + captures = query.captures(self._syntax_tree.root_node) + + return list(captures) + def _tree_sitter_byte_offset(self, location: tuple[int, int]) -> int: """Given a document coordinate, return the byte offset of that coordinate. This method only does work if tree-sitter was imported, otherwise it returns 0. diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index a8276771c9..4eac6029d9 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -657,6 +657,7 @@ def scroll_cursor_visible(self) -> Offset: animate=False, force=True, ) + # TODO - need a move_cursor method so we can do center=True etc. return scroll_offset @property From 750eef6c8192eab1311f0abbf66e8adfd18c02bb Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 11:36:36 +0100 Subject: [PATCH 183/366] Change cursor_destination to move_cursor, add more tests --- src/textual/document/_document.py | 2 +- src/textual/widgets/_text_area.py | 65 ++--- tests/text_area/test_edit_via_api.py | 250 ++++++++++++++++++ ...test_edit.py => test_edit_via_bindings.py} | 42 +-- 4 files changed, 306 insertions(+), 53 deletions(-) create mode 100644 tests/text_area/test_edit_via_api.py rename tests/text_area/{test_edit.py => test_edit_via_bindings.py} (65%) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 2049dcf7fd..8c656a24f2 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -170,7 +170,7 @@ def get_size(self, tab_width: int) -> Size: """ lines = self._lines cell_lengths = [cell_len(line.expandtabs(tab_width)) for line in lines] - max_cell_length = max(cell_lengths or [1]) + max_cell_length = max(cell_lengths or [0]) height = len(lines) return Size(max_cell_length, height) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index a8276771c9..9b99ba3347 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -52,10 +52,10 @@ class Insert: """The start location of the insert.""" to_location: Location """The end location of the insert""" - cursor_destination: Location | None = None - """The location to move the cursor to after the operation completes.""" + move_cursor: bool = False + """True if the cursor should move to the end of the inserted text.""" _edit_end: Location | None = field(init=False, default=None) - """Computed location to move the cursor to if `cursor_destination` is None.""" + """Computed location to move the cursor to if `move_cursor` is True.""" def do(self, text_area: TextArea) -> None: """Perform the Insert operation. @@ -63,6 +63,9 @@ def do(self, text_area: TextArea) -> None: Args: text_area: The TextArea to perform the insert on. """ + # TODO: For undo to work, we'll need to record the text that was replaced. + # We can use TextArea.get_text_range to do this, or perform it inside + # document.insert_range and return a compound object. self._edit_end = text_area._document.insert_range( self.from_location, self.to_location, @@ -77,16 +80,15 @@ def undo(self, text_area: TextArea) -> None: """ def post_edit(self, text_area: TextArea) -> None: - """Update the cursor location after the widget has been refreshed. + """Possibly update the cursor location after the widget has been refreshed. Args: text_area: The TextArea this operation was performed on. """ - cursor_destination = self.cursor_destination - if cursor_destination is not None: - text_area.selection = cursor_destination - else: + if self.move_cursor: text_area.selection = Selection.cursor(self._edit_end) + else: + text_area.refresh() text_area.record_cursor_offset() @@ -101,7 +103,7 @@ class Delete: to_location: Location """The location to delete to (exclusive).""" - cursor_destination: Location | None = None + move_cursor: bool = False """Where to move the cursor to after the deletion.""" _deleted_text: str | None = field(init=False, default=None) @@ -118,11 +120,10 @@ def undo(self, text_area: TextArea) -> None: """Undo the delete action.""" def post_edit(self, text_area: TextArea) -> None: - cursor_destination = self.cursor_destination - if cursor_destination is not None: - text_area.selection = Selection.cursor(cursor_destination) - else: + if self.move_cursor: text_area.selection = Selection.cursor(self.from_location) + else: + text_area.refresh() text_area.record_cursor_offset() @@ -307,9 +308,11 @@ def __init__( """True if we're currently selecting text using the mouse, otherwise False.""" def _watch_selection(self) -> None: + """When the cursor moves, scroll it into view.""" self.scroll_cursor_visible() def _validate_selection(self, selection: Selection) -> Selection: + """Clamp the selection to valid locations.""" start, end = selection clamp_visitable = self.clamp_visitable return Selection(clamp_visitable(start), clamp_visitable(end)) @@ -554,7 +557,7 @@ async def _on_key(self, event: events.Key) -> None: insert = insert_values.get(key, event.character) assert event.character is not None start, end = self.selection - self.insert_text_range(insert, start, end) + self.insert_text_range(insert, start, end, True) def get_target_document_location(self, event: MouseEvent) -> Location: """Given a MouseEvent, return the row and column offset of the event in document-space. @@ -942,30 +945,30 @@ def insert_text( self, text: str, location: Location | None = None, - cursor_destination: Location | None = None, + move_cursor: bool = False, ) -> None: if location is None: - location = self.selection.end - self.edit(Insert(text, location, location, cursor_destination)) + location = self.cursor_location + self.edit(Insert(text, location, location, move_cursor)) def insert_text_range( self, text: str, from_location: Location, to_location: Location, - cursor_destination: Location | None = None, + move_cursor: bool = False, ) -> None: - self.edit(Insert(text, from_location, to_location, cursor_destination)) + self.edit(Insert(text, from_location, to_location, move_cursor)) def delete_range( self, from_location: Location, to_location: Location, - cursor_destination: Location | None = None, + move_cursor: bool = False, ) -> str: """Delete text between from_location and to_location.""" top, bottom = _fix_direction(from_location, to_location) - deleted_text = self.edit(Delete(top, bottom, cursor_destination)) + deleted_text = self.edit(Delete(top, bottom, move_cursor)) return deleted_text def clear(self) -> None: @@ -973,7 +976,7 @@ def clear(self) -> None: document = self._document last_line = document[-1] document_end_location = (document.line_count, len(last_line)) - self.delete_range((0, 0), document_end_location, cursor_destination=(0, 0)) + self.delete_range((0, 0), document_end_location, move_cursor=True) def action_delete_left(self) -> None: """Deletes the character to the left of the cursor and updates the cursor location. @@ -994,7 +997,7 @@ def action_delete_left(self) -> None: else: end = (end_row, end_column - 1) - self.delete_range(start, end) + self.delete_range(start, end, move_cursor=True) def action_delete_right(self) -> None: """Deletes the character to the right of the cursor and keeps the cursor at the same location. @@ -1013,7 +1016,7 @@ def action_delete_right(self) -> None: else: end = (end_row, end_column + 1) - self.delete_range(start, end) + self.delete_range(start, end, move_cursor=True) def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" @@ -1024,21 +1027,21 @@ def action_delete_line(self) -> None: from_location = (start_row, 0) to_location = (end_row + 1, 0) - self.delete_range(from_location, to_location) + self.delete_range(from_location, to_location, move_cursor=True) def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor location to the start of the line.""" from_location = self.selection.end cursor_row, cursor_column = from_location to_location = (cursor_row, 0) - self.delete_range(from_location, to_location) + self.delete_range(from_location, to_location, move_cursor=True) def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor location to the end of the line.""" from_location = self.selection.end cursor_row, cursor_column = from_location to_location = (cursor_row, len(self._document[cursor_row])) - self.delete_range(from_location, to_location) + self.delete_range(from_location, to_location, move_cursor=True) def action_delete_word_left(self) -> None: """Deletes the word to the left of the cursor and updates the cursor location.""" @@ -1049,7 +1052,7 @@ def action_delete_word_left(self) -> None: # deletes the characters within the selection range, ignoring word boundaries. start, end = self.selection if start != end: - self.delete_range(start, end) + self.delete_range(start, end, move_cursor=True) cursor_row, cursor_column = end @@ -1067,7 +1070,7 @@ def action_delete_word_left(self) -> None: # If we're already on the first line and no word boundary is found, delete to the start of the line from_location = (cursor_row, 0) - self.delete_range(from_location, self.selection.end) + self.delete_range(from_location, self.selection.end, move_cursor=True) def action_delete_word_right(self) -> None: """Deletes the word to the right of the cursor and keeps the cursor at the same location.""" @@ -1076,7 +1079,7 @@ def action_delete_word_right(self) -> None: start, end = self.selection if start != end: - self.delete_range(start, end) + self.delete_range(start, end, move_cursor=True) cursor_row, cursor_column = end @@ -1094,4 +1097,4 @@ def action_delete_word_right(self) -> None: # If we're already on the last line and no word boundary is found, delete to the end of the line to_location = (cursor_row, len(self._document[cursor_row])) - self.delete_range(end, to_location) + self.delete_range(end, to_location, move_cursor=True) diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py new file mode 100644 index 0000000000..1a18046846 --- /dev/null +++ b/tests/text_area/test_edit_via_api.py @@ -0,0 +1,250 @@ +"""Tests editing the document using the API (insert_range etc.) + +The tests in this module directly call the edit APIs on the TextArea rather +than going via bindings. + +Note that more extensive testing for editing is done at the Document level. +""" + +from textual.app import App, ComposeResult +from textual.document import Selection +from textual.widgets import TextArea + +TEXT = """I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.load_text(TEXT) + yield text_area + + +async def test_insert_text_start(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert_text("Hello") + assert text_area.text == "Hello" + TEXT + assert text_area.cursor_location == (0, 0) + + +async def test_insert_text_start_move_cursor(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert_text("Hello", move_cursor=True) + assert text_area.text == "Hello" + TEXT + assert text_area.cursor_location == (0, 5) + + +async def test_insert_newlines_start(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert_text("\n\n\n") + assert text_area.text == "\n\n\n" + TEXT + + +async def test_insert_newlines_end(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert_text("\n\n\n", location=(4, 0)) + assert text_area.text == TEXT + "\n\n\n" + + +async def test_insert_windows_newlines(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + # Although we're inserting windows newlines, the configured newline on + # the Document inside the TextArea will be "\n", so when we check TextArea.text + # we expect to see "\n". + text_area.insert_text("\r\n\r\n\r\n") + assert text_area.text == "\n\n\n" + TEXT + + +async def test_insert_old_mac_newlines(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert_text("\r\r\r") + assert text_area.text == "\n\n\n" + TEXT + + +async def test_insert_text_non_cursor_location(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert_text("Hello", location=(4, 0)) + assert text_area.text == TEXT + "Hello" + assert text_area.selection == Selection.cursor((0, 0)) + + +async def test_insert_text_non_cursor_location_move_cursor(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert_text("Hello", location=(4, 0), move_cursor=True) + assert text_area.text == TEXT + "Hello" + assert text_area.selection == Selection.cursor((4, 5)) + + +async def test_insert_multiline_text(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.cursor_location = (2, 5) + text_area.insert_text("Hello,\nworld!") + expected_content = """\ +I must not fear. +Fear is the mind-killer. +Fear Hello, +world!is the little-death that brings total obliteration. +I will face my fear. +""" + assert text_area.cursor_location == (2, 5) # Cursor didn't move + assert text_area.text == expected_content + + +async def test_insert_multiline_text_move_cursor(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.cursor_location = (2, 5) + text_area.insert_text("Hello,\nworld!", move_cursor=True) + expected_content = """\ +I must not fear. +Fear is the mind-killer. +Fear Hello, +world!is the little-death that brings total obliteration. +I will face my fear. +""" + assert text_area.cursor_location == (3, 6) # Cursor moved to end of insert + assert text_area.text == expected_content + + +async def test_insert_range_multiline_text(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + # replace "Fear is the mind-killer\nFear is the little death...\n" + # with "Hello,\nworld!\n" + text_area.insert_text_range( + "Hello,\nworld!\n", from_location=(1, 0), to_location=(3, 0) + ) + expected_content = """\ +I must not fear. +Hello, +world! +I will face my fear. +""" + assert text_area.selection == Selection.cursor((0, 0)) # cursor didnt move + assert text_area.text == expected_content + + +async def test_insert_range_multiline_text_move_cursor(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + # replace "Fear is the mind-killer\nFear is the little death..." + text_area.insert_text_range( + "Hello,\nworld!\n", + from_location=(1, 0), + to_location=(3, 0), + move_cursor=True, + ) + expected_content = """\ +I must not fear. +Hello, +world! +I will face my fear. +""" + assert text_area.selection == Selection.cursor( + (3, 0) + ) # cursor moves to end of insert + assert text_area.text == expected_content + + +async def test_delete_range_within_line(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + deleted_text = text_area.delete_range((0, 6), (0, 10)) + assert deleted_text == " not" + expected_text = """\ +I must fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + assert text_area.selection == Selection.cursor((0, 0)) # cursor didnt move + assert text_area.text == expected_text + + +async def test_delete_range_within_line_move_cursor(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + deleted_text = text_area.delete_range((0, 6), (0, 10), move_cursor=True) + assert deleted_text == " not" + expected_text = """\ +I must fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + assert text_area.selection == Selection.cursor((0, 6)) # cursor moved + assert text_area.text == expected_text + + +async def test_delete_range_multiple_lines(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + deleted_text = text_area.delete_range((1, 0), (3, 0)) + assert text_area.selection == Selection.cursor((0, 0)) + assert ( + text_area.text + == """\ +I must not fear. +I will face my fear. +""" + ) + assert ( + deleted_text + == """\ +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +""" + ) + + +async def test_delete_range_empty_document(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("") + deleted_text = text_area.delete_range((0, 0), (1, 0)) + assert deleted_text == "" + assert text_area.text == "" + + +async def test_clear(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.clear() + + +async def test_clear_empty_document(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("") + text_area.clear() diff --git a/tests/text_area/test_edit.py b/tests/text_area/test_edit_via_bindings.py similarity index 65% rename from tests/text_area/test_edit.py rename to tests/text_area/test_edit_via_bindings.py index 1a8f0c299e..137de0bbdf 100644 --- a/tests/text_area/test_edit.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -1,7 +1,14 @@ -"""Tests some edit operations in the TextArea - note that more -extensive testing for editing is done at the Document level.""" +"""Tests some edits using the keyboard. + +All tests in this module should press keys on the keyboard which edit the document, +and check that the document content is updated as expected, as well as the cursor +location. + +Note that more extensive testing for editing is done at the Document level. +""" from textual.app import App, ComposeResult +from textual.document import Selection from textual.widgets import TextArea TEXT = """I must not fear. @@ -18,31 +25,20 @@ def compose(self) -> ComposeResult: yield text_area -async def test_insert_text_start(): +async def test_single_keypress_printable_character(): app = TextAreaApp() - async with app.run_test(): + async with app.run_test() as pilot: text_area = app.query_one(TextArea) - text_area.insert_text("Hello") - assert text_area.text == "Hello" + TEXT - assert text_area.cursor_location == (0, 5) + await pilot.press("x") + assert text_area.text == "x" + TEXT -async def test_insert_multiline_text(): +async def test_single_keypress_enter(): app = TextAreaApp() - async with app.run_test(): + async with app.run_test() as pilot: text_area = app.query_one(TextArea) - text_area.cursor_location = (2, 5) - text_area.insert_text("Hello,\nworld!") - assert text_area.cursor_location == (3, 6) - assert ( - text_area.text - == """I must not fear. -Fear is the mind-killer. -Fear Hello, -world!is the little-death that brings total obliteration. -I will face my fear. -""" - ) + await pilot.press("enter") + assert text_area.text == "\n" + TEXT async def test_delete_left(): @@ -53,6 +49,7 @@ async def test_delete_left(): text_area.cursor_location = (0, 6) await pilot.press("backspace") assert text_area.text == "Hello world!" + assert text_area.selection == Selection.cursor((0, 5)) async def test_delete_left_start(): @@ -62,6 +59,7 @@ async def test_delete_left_start(): text_area.load_text("Hello, world!") await pilot.press("backspace") assert text_area.text == "Hello, world!" + assert text_area.selection == Selection.cursor((0, 0)) async def test_delete_left_end(): @@ -72,6 +70,7 @@ async def test_delete_left_end(): text_area.cursor_location = (0, 13) await pilot.press("backspace") assert text_area.text == "Hello, world" + assert text_area.selection == Selection.cursor((0, 12)) async def test_delete_right(): @@ -82,3 +81,4 @@ async def test_delete_right(): text_area.cursor_location = (0, 13) await pilot.press("delete") assert text_area.text == "Hello, world!" + assert text_area.selection == Selection.cursor((0, 13)) From edaa298c995bfd21b6043458b4ae6f6371c85270 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 11:43:54 +0100 Subject: [PATCH 184/366] Remove faulty assertion --- src/textual/widgets/_text_area.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index ad205859e0..ac4007cae0 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -555,7 +555,6 @@ async def _on_key(self, event: events.Key) -> None: event.stop() event.prevent_default() insert = insert_values.get(key, event.character) - assert event.character is not None start, end = self.selection self.insert_text_range(insert, start, end, True) From 093cf7323f2bc6d50b33d21852f240f2963a5ad7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 13:15:50 +0100 Subject: [PATCH 185/366] Tidying cursor movement --- src/textual/widgets/_text_area.py | 170 +++++++++++++--------- tests/text_area/test_edit_via_api.py | 4 +- tests/text_area/test_edit_via_bindings.py | 20 ++- tests/text_area/test_selection.py | 10 +- 4 files changed, 123 insertions(+), 81 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index ac4007cae0..b3859e243b 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -644,9 +644,12 @@ def clamp_visitable(self, location: Location) -> Location: return row, column - def scroll_cursor_visible(self) -> Offset: + def scroll_cursor_visible(self, center: bool = False) -> Offset: """Scroll the `TextArea` such that the cursor is visible on screen. + Args: + center: True if the cursor should be scrolled to the center. + Returns: The offset that was scrolled to bring the cursor into view. """ @@ -658,26 +661,25 @@ def scroll_cursor_visible(self) -> Offset: spacing=Spacing(right=self.gutter_width), animate=False, force=True, + center=center, ) - # TODO - need a move_cursor method so we can do center=True etc. return scroll_offset @property def cursor_location(self) -> Location: - """The current location of the cursor in the document.""" + """The current location of the cursor in the document. + + This is a utility for accessing the `end` of `TextArea.selection`. + """ return self.selection.end @cursor_location.setter - def cursor_location(self, new_location: Location) -> None: + def cursor_location(self, location: Location) -> None: """Set the cursor_location to a new location. If a selection is in progress, the anchor point will remain. """ - start, end = self.selection - if start != end: - self.selection = Selection(start, new_location) - else: - self.selection = Selection.cursor(new_location) + self.move_cursor(location, select=not self.selection.is_empty) @property def cursor_at_first_row(self) -> bool: @@ -707,39 +709,6 @@ def cursor_at_end_of_document(self) -> bool: """True if the cursor is at the very end of the document.""" return self.cursor_at_last_row and self.cursor_at_end_of_row - def cursor_to_line_end(self, select: bool = False) -> None: - """Move the cursor to the end of the line. - - Args: - select: Select the text between the old and new cursor locations. - """ - - start, end = self.selection - cursor_row, cursor_column = end - target_column = len(self._document[cursor_row]) - - if select: - self.selection = Selection(start, target_column) - else: - self.selection = Selection.cursor((cursor_row, target_column)) - - self.record_cursor_offset() - - def cursor_to_line_start(self, select: bool = False) -> None: - """Move the cursor to the start of the line. - - Args: - select: Select the text between the old and new cursor locations. - """ - start, end = self.selection - cursor_row, cursor_column = end - if select: - self.selection = Selection(start, (cursor_row, 0)) - else: - self.selection = Selection.cursor((cursor_row, 0)) - - self.record_cursor_offset() - # ------ Cursor movement actions def action_cursor_left(self) -> None: """Move the cursor one location to the left. @@ -762,7 +731,11 @@ def action_cursor_left_select(self): self.record_cursor_offset() def get_cursor_left_location(self) -> Location: - """Get the location the cursor will move to if it moves left.""" + """Get the location the cursor will move to if it moves left. + + Returns: + The location of the cursor if it moves left. + """ if self.cursor_at_start_of_document: return 0, 0 cursor_row, cursor_column = self.selection.end @@ -771,13 +744,41 @@ def get_cursor_left_location(self) -> Location: target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above return target_row, target_column + def move_cursor( + self, + location: Location, + select: bool = False, + center: bool = False, + record_width: bool = True, + ) -> None: + """Move the cursor to a location, optionally selecting text on the way. + + Args: + location: The location to move the cursor to. + select: If True, select text between the old and new location. + center: If True, scroll such that the cursor is centered. + record_width: If True, record the cursor column cell width so that we + jump to a corresponding location when moving between rows. + """ + if not select: + self.selection = Selection.cursor(location) + else: + start, end = self.selection + self.selection = Selection(start, location) + + if record_width: + self.record_cursor_offset() + + if center: + self.scroll_cursor_visible(center) + def action_cursor_right(self) -> None: """Move the cursor one location to the right. If the cursor is at the end of a line, attempt to go to the start of the next line. """ target = self.get_cursor_right_location() - self.selection = Selection.cursor(target) + self.move_cursor(target) self.record_cursor_offset() def action_cursor_right_select(self): @@ -785,10 +786,8 @@ def action_cursor_right_select(self): This will expand or contract the selection. """ - new_cursor_location = self.get_cursor_right_location() - selection_start, selection_end = self.selection - self.selection = Selection(selection_start, new_cursor_location) - self.record_cursor_offset() + target = self.get_cursor_right_location() + self.move_cursor(target, select=True) def get_cursor_right_location(self) -> Location: """Get the location the cursor will move to if it moves right.""" @@ -802,13 +801,12 @@ def get_cursor_right_location(self) -> Location: def action_cursor_down(self) -> None: """Move the cursor down one cell.""" target = self.get_cursor_down_location() - self.selection = Selection.cursor(target) + self.move_cursor(target, record_width=False) def action_cursor_down_select(self) -> None: """Move the cursor down one cell, selecting the range between the old and new locations.""" target = self.get_cursor_down_location() - start, end = self.selection - self.selection = Selection(start, target) + self.move_cursor(target, select=True, record_width=False) def get_cursor_down_location(self): """Get the location the cursor will move to if it moves down.""" @@ -827,13 +825,12 @@ def get_cursor_down_location(self): def action_cursor_up(self) -> None: """Move the cursor up one cell.""" target = self.get_cursor_up_location() - self.selection = Selection.cursor(target) + self.move_cursor(target, record_width=False) def action_cursor_up_select(self) -> None: """Move the cursor up one cell, selecting the range between the old and new locations.""" target = self.get_cursor_up_location() - start, end = self.selection - self.selection = Selection(start, target) + self.move_cursor(target, select=True, record_width=False) def get_cursor_up_location(self) -> Location: """Get the location the cursor will move to if it moves up.""" @@ -850,24 +847,52 @@ def get_cursor_up_location(self) -> Location: def action_cursor_line_end(self) -> None: """Move the cursor to the end of the line.""" - self.cursor_to_line_end() + location = self.get_cursor_line_end_location() + self.move_cursor(location) + + def get_cursor_line_end_location(self) -> Location: + """Get the location of the end of the current line. + + Returns: + The (row, column) location of the end of the cursors current line. + """ + start, end = self.selection + cursor_row, cursor_column = end + target_column = len(self._document[cursor_row]) + return cursor_row, target_column def action_cursor_line_start(self) -> None: """Move the cursor to the start of the line.""" - self.cursor_to_line_start() + target = self.get_cursor_line_start_location() + self.move_cursor(target) + + def get_cursor_line_start_location(self) -> Location: + """Get the location of the end of the current line. + + Returns: + The (row, column) location of the end of the cursors current line. + """ + _start, end = self.selection + cursor_row, _cursor_column = end + return cursor_row, 0 def action_cursor_left_word(self) -> None: """Move the cursor left by a single word, skipping spaces.""" - if self.cursor_at_start_of_document: return + target = self.get_cursor_left_word_location() + self.move_cursor(target) - cursor_row, cursor_column = self.selection.end + def get_cursor_left_word_location(self) -> Location: + """Get the location the cursor will jump to if it goes 1 word left. + Returns: + The location the cursor will jump on "jump word left". + """ + cursor_row, cursor_column = self.cursor_location # Check the current line for a word boundary line = self._document[cursor_row][:cursor_column] matches = list(re.finditer(self.word_pattern, line)) - if matches: # If a word boundary is found, move the cursor there cursor_column = matches[-1].start() @@ -878,9 +903,7 @@ def action_cursor_left_word(self) -> None: else: # If we're already on the first line and no word boundary is found, move to the start of the line cursor_column = 0 - - self.selection = Selection.cursor((cursor_row, cursor_column)) - self.record_cursor_offset() + return cursor_row, cursor_column def action_cursor_right_word(self) -> None: """Move the cursor right by a single word, skipping spaces.""" @@ -888,12 +911,19 @@ def action_cursor_right_word(self) -> None: if self.cursor_at_end_of_document: return - cursor_row, cursor_column = self.selection.end + target = self.get_cursor_right_word_location() + self.move_cursor(target) + def get_cursor_right_word_location(self): + """Get the location the cursor will jump to if it goes 1 word right. + + Returns: + The location the cursor will jump on "jump word right". + """ + cursor_row, cursor_column = self.selection.end # Check the current line for a word boundary line = self._document[cursor_row][cursor_column:] matches = list(re.finditer(self.word_pattern, line)) - if matches: # If a word boundary is found, move the cursor there cursor_column += matches[0].end() @@ -904,25 +934,23 @@ def action_cursor_right_word(self) -> None: else: # If we're already on the last line and no word boundary is found, move to the end of the line cursor_column = len(self._document[cursor_row]) - - self.selection = Selection.cursor((cursor_row, cursor_column)) - self.record_cursor_offset() + return cursor_row, cursor_column def action_cursor_page_up(self) -> None: height = self.content_size.height _, cursor_location = self.selection row, column = cursor_location target = (row - height, column) - self.scroll_y -= height - self.selection = Selection.cursor(target) + self.scroll_relative(y=-height, animate=False) + self.move_cursor(target) def action_cursor_page_down(self) -> None: height = self.content_size.height _, cursor_location = self.selection row, column = cursor_location target = (row + height, column) - self.scroll_y += height - self.selection = Selection.cursor(target) + self.scroll_relative(y=height, animate=False) + self.move_cursor(target) @property def cursor_line_text(self) -> str: diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index 1a18046846..d0afc0b174 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -99,7 +99,7 @@ async def test_insert_multiline_text(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.cursor_location = (2, 5) + text_area.move_cursor((2, 5)) text_area.insert_text("Hello,\nworld!") expected_content = """\ I must not fear. @@ -116,7 +116,7 @@ async def test_insert_multiline_text_move_cursor(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.cursor_location = (2, 5) + text_area.move_cursor((2, 5)) text_area.insert_text("Hello,\nworld!", move_cursor=True) expected_content = """\ I must not fear. diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index 137de0bbdf..27be5fee4a 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -46,7 +46,7 @@ async def test_delete_left(): async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.load_text("Hello, world!") - text_area.cursor_location = (0, 6) + text_area.move_cursor((0, 6)) await pilot.press("backspace") assert text_area.text == "Hello world!" assert text_area.selection == Selection.cursor((0, 5)) @@ -67,18 +67,32 @@ async def test_delete_left_end(): async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.load_text("Hello, world!") - text_area.cursor_location = (0, 13) + text_area.move_cursor((0, 13)) await pilot.press("backspace") assert text_area.text == "Hello, world" assert text_area.selection == Selection.cursor((0, 12)) async def test_delete_right(): + """Pressing 'delete' deletes the character to the right of the cursor.""" app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.load_text("Hello, world!") - text_area.cursor_location = (0, 13) + text_area.move_cursor((0, 13)) await pilot.press("delete") assert text_area.text == "Hello, world!" assert text_area.selection == Selection.cursor((0, 13)) + + +async def test_delete_right_end_of_line(): + """Pressing 'delete' at the end of the line merges this line with the line below.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("hello\nworld!") + end_of_line = text_area.get_cursor_line_end_location() + text_area.move_cursor(end_of_line) + await pilot.press("delete") + assert text_area.selection == Selection.cursor((0, 5)) + assert text_area.text == "helloworld!" diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 85d7261ef5..b77629a636 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -38,7 +38,7 @@ async def test_cursor_location_set(): async with app.run_test(): text_area = app.query_one(TextArea) text_area.selection = Selection((1, 1), (2, 2)) - text_area.cursor_location = (2, 3) + text_area.move_cursor((2, 3)) assert text_area.selection == Selection((1, 1), (2, 3)) @@ -188,7 +188,7 @@ async def test_get_cursor_left_location(start, end): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.cursor_location = start + text_area.move_cursor(start) assert text_area.get_cursor_left_location() == end @@ -204,7 +204,7 @@ async def test_get_cursor_right_location(start, end): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.cursor_location = start + text_area.move_cursor(start) assert text_area.get_cursor_right_location() == end @@ -220,7 +220,7 @@ async def test_get_cursor_up_location(start, end): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.cursor_location = start + text_area.move_cursor(start) # This is required otherwise the cursor will snap back to the # last location navigated to (0, 0) text_area.record_cursor_offset() @@ -239,7 +239,7 @@ async def test_get_cursor_down_location(start, end): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.cursor_location = start + text_area.move_cursor(start) # This is required otherwise the cursor will snap back to the # last location navigated to (0, 0) text_area.record_cursor_offset() From 00a4f43c104b3162eaa5a2b31dadc66883871c6b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 13:28:22 +0100 Subject: [PATCH 186/366] Tidying up, adding docstrings for component classes --- src/textual/widgets/_text_area.py | 126 ++++++++++++++++-------------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index b3859e243b..f3f22e08e7 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -131,15 +131,16 @@ def post_edit(self, text_area: TextArea) -> None: class TextArea(ScrollView, can_focus=True): DEFAULT_CSS = """\ $text-area-active-line-bg: white 8%; + TextArea { background: $panel; width: 1fr; height: 1fr; } -TextArea > .text-area--active-line { +TextArea > .text-area--cursor-line { background: $text-area-active-line-bg; } -TextArea > .text-area--active-line-gutter { +TextArea > .text-area--cursor-line-gutter { color: $text; background: $text-area-active-line-bg; } @@ -159,13 +160,22 @@ class TextArea(ScrollView, can_focus=True): """ COMPONENT_CLASSES: ClassVar[set[str]] = { - "text-area--active-line", - "text-area--active-line-gutter", - "text-area--gutter", "text-area--cursor", + "text-area--gutter", + "text-area--cursor-line", + "text-area--cursor-line-gutter", "text-area--selection", "text-area--width-guide", } + """| Class | Description | +|:--------------------------------|:-------------------------------------------------| +| `text-area--cursor` | Targets the cursor. | +| `text-area--gutter` | Targets the gutter (line number column). | +| `text-area--cursor-line` | Targets the line of text the cursor is on. | +| `text-area--cursor-line-gutter` | Targets the gutter of the line the cursor is on. | +| `text-area--selection` | Targets the selected text. | +| `text-area--width-guide` | Targets the width guide. | + """ BINDINGS = [ Binding("escape", "screen.focus_next", "focus next", show=False), @@ -423,7 +433,7 @@ def render_line(self, widget_y: int) -> Strip: # Highlight the cursor cursor_row, cursor_column = end - active_line_style = self.get_component_rich_style("text-area--active-line") + active_line_style = self.get_component_rich_style("text-area--cursor-line") if cursor_row == line_index: cursor_style = self.get_component_rich_style("text-area--cursor") line.stylize(cursor_style, cursor_column, cursor_column + 1) @@ -439,7 +449,7 @@ def render_line(self, widget_y: int) -> Strip: if self.show_line_numbers: if cursor_row == line_index: gutter_style = self.get_component_rich_style( - "text-area--active-line-gutter" + "text-area--cursor-line-gutter" ) else: gutter_style = self.get_component_rich_style("text-area--gutter") @@ -501,22 +511,6 @@ def get_text_range(self, start: Location, end: Location) -> str: start, end = _fix_direction(start, end) return self._document.get_text_range(start, end) - @property - def gutter_width(self) -> int: - """The width of the gutter (the left column containing line numbers). - - Returns: - The cell-width of the line number column. If `show_line_numbers` is `False` returns 0. - """ - # The longest number in the gutter plus two extra characters: `│ `. - gutter_margin = 2 - gutter_width = ( - len(str(self._document.line_count + 1)) + gutter_margin - if self.show_line_numbers - else 0 - ) - return gutter_width - def edit(self, edit: Edit) -> Any: """Perform an Edit. @@ -538,12 +532,6 @@ def edit(self, edit: Edit) -> Any: return result - # def undo(self) -> None: - # if self._undo_stack: - # action = self._undo_stack.pop() - # action.undo(self) - - # --- Lower level event/key handling async def _on_key(self, event: events.Key) -> None: """Handle key presses which correspond to document inserts.""" key = event.key @@ -558,6 +546,12 @@ async def _on_key(self, event: events.Key) -> None: start, end = self.selection self.insert_text_range(insert, start, end, True) + # def undo(self) -> None: + # if self._undo_stack: + # action = self._undo_stack.pop() + # action.undo(self) + + # --- Lower level event/key handling def get_target_document_location(self, event: MouseEvent) -> Location: """Given a MouseEvent, return the row and column offset of the event in document-space. @@ -578,6 +572,22 @@ def get_target_document_location(self, event: MouseEvent) -> Location: target_column = self.cell_width_to_column_index(target_x, target_row) return target_row, target_column + @property + def gutter_width(self) -> int: + """The width of the gutter (the left column containing line numbers). + + Returns: + The cell-width of the line number column. If `show_line_numbers` is `False` returns 0. + """ + # The longest number in the gutter plus two extra characters: `│ `. + gutter_margin = 2 + gutter_width = ( + len(str(self._document.line_count + 1)) + gutter_margin + if self.show_line_numbers + else 0 + ) + return gutter_width + def _on_mouse_down(self, event: events.MouseDown) -> None: """Update the cursor position, and begin a selection using the mouse.""" target = self.get_target_document_location(event) @@ -625,7 +635,6 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: return column_index return len(line) - # --- Cursor/selection utilities def clamp_visitable(self, location: Location) -> Location: """Clamp the given location to the nearest visitable location. @@ -644,6 +653,7 @@ def clamp_visitable(self, location: Location) -> Location: return row, column + # --- Cursor/selection utilities def scroll_cursor_visible(self, center: bool = False) -> Offset: """Scroll the `TextArea` such that the cursor is visible on screen. @@ -665,6 +675,34 @@ def scroll_cursor_visible(self, center: bool = False) -> Offset: ) return scroll_offset + def move_cursor( + self, + location: Location, + select: bool = False, + center: bool = False, + record_width: bool = True, + ) -> None: + """Move the cursor to a location, optionally selecting text on the way. + + Args: + location: The location to move the cursor to. + select: If True, select text between the old and new location. + center: If True, scroll such that the cursor is centered. + record_width: If True, record the cursor column cell width so that we + jump to a corresponding location when moving between rows. + """ + if not select: + self.selection = Selection.cursor(location) + else: + start, end = self.selection + self.selection = Selection(start, location) + + if record_width: + self.record_cursor_offset() + + if center: + self.scroll_cursor_visible(center) + @property def cursor_location(self) -> Location: """The current location of the cursor in the document. @@ -744,34 +782,6 @@ def get_cursor_left_location(self) -> Location: target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above return target_row, target_column - def move_cursor( - self, - location: Location, - select: bool = False, - center: bool = False, - record_width: bool = True, - ) -> None: - """Move the cursor to a location, optionally selecting text on the way. - - Args: - location: The location to move the cursor to. - select: If True, select text between the old and new location. - center: If True, scroll such that the cursor is centered. - record_width: If True, record the cursor column cell width so that we - jump to a corresponding location when moving between rows. - """ - if not select: - self.selection = Selection.cursor(location) - else: - start, end = self.selection - self.selection = Selection(start, location) - - if record_width: - self.record_cursor_offset() - - if center: - self.scroll_cursor_visible(center) - def action_cursor_right(self) -> None: """Move the cursor one location to the right. From cfc53c820aa67140c801fb133d0e1c4d8717aa8b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 13:31:12 +0100 Subject: [PATCH 187/366] Fix a broken selection test --- src/textual/widgets/_text_area.py | 8 ++------ tests/text_area/test_selection.py | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index f3f22e08e7..b0a6c71d31 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -86,12 +86,10 @@ def post_edit(self, text_area: TextArea) -> None: text_area: The TextArea this operation was performed on. """ if self.move_cursor: - text_area.selection = Selection.cursor(self._edit_end) + text_area.move_cursor(self._edit_end) else: text_area.refresh() - text_area.record_cursor_offset() - @dataclass class Delete: @@ -121,12 +119,10 @@ def undo(self, text_area: TextArea) -> None: def post_edit(self, text_area: TextArea) -> None: if self.move_cursor: - text_area.selection = Selection.cursor(self.from_location) + text_area.move_cursor(self.from_location) else: text_area.refresh() - text_area.record_cursor_offset() - class TextArea(ScrollView, can_focus=True): DEFAULT_CSS = """\ diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index b77629a636..3a72e3b611 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -38,7 +38,7 @@ async def test_cursor_location_set(): async with app.run_test(): text_area = app.query_one(TextArea) text_area.selection = Selection((1, 1), (2, 2)) - text_area.move_cursor((2, 3)) + text_area.move_cursor((2, 3), select=True) assert text_area.selection == Selection((1, 1), (2, 3)) From a62a4ec540483b001908701be6cebe19dde0104b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 13:50:37 +0100 Subject: [PATCH 188/366] Remove some unused highlighting machinery --- tools/collect_highlights.py | 29 --- tools/highlights/python.scm | 345 ------------------------------------ 2 files changed, 374 deletions(-) delete mode 100644 tools/collect_highlights.py delete mode 100644 tools/highlights/python.scm diff --git a/tools/collect_highlights.py b/tools/collect_highlights.py deleted file mode 100644 index 16f09c3930..0000000000 --- a/tools/collect_highlights.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -import shutil -from pathlib import Path - -from textual._languages import VALID_LANGUAGES - -# The directory that contains the language folders -source_dir = Path(__file__).parent / "../../nvim-treesitter/queries" -# The directory to store the collected highlights files -target_dir = Path(__file__).parent / "highlights" - -# Ensure the target directory exists -os.makedirs(target_dir, exist_ok=True) - -# Walk through the source directory -for root, dirs, files in os.walk(source_dir): - # If a highlights.scm file exists in the current directory - if "highlights.scm" in files: - # Get the name of the current language directory - language = os.path.basename(root) - if language in VALID_LANGUAGES: - # Create the full path to the source and target files - source_file = os.path.join(root, "highlights.scm") - target_file = os.path.join(target_dir, f"{language}.scm") - # Copy the file - shutil.copyfile(source_file, target_file) - -# Print a success message -print("Done!") diff --git a/tools/highlights/python.scm b/tools/highlights/python.scm deleted file mode 100644 index c18b748674..0000000000 --- a/tools/highlights/python.scm +++ /dev/null @@ -1,345 +0,0 @@ -;; From tree-sitter-python licensed under MIT License -; Copyright (c) 2016 Max Brunsfeld - -; Variables -(identifier) @variable - -; Reset highlighting in f-string interpolations -(interpolation) @none - -;; Identifier naming conventions -((identifier) @type - (#lua-match? @type "^[A-Z].*[a-z]")) -((identifier) @constant - (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) - -((identifier) @constant.builtin - (#lua-match? @constant.builtin "^__[a-zA-Z0-9_]*__$")) - -((identifier) @constant.builtin - (#any-of? @constant.builtin - ;; https://docs.python.org/3/library/constants.html - "NotImplemented" - "Ellipsis" - "quit" - "exit" - "copyright" - "credits" - "license")) - -((attribute - attribute: (identifier) @field) - (#lua-match? @field "^[%l_].*$")) - -((assignment - left: (identifier) @type.definition - (type (identifier) @_annotation)) - (#eq? @_annotation "TypeAlias")) - -((assignment - left: (identifier) @type.definition - right: (call - function: (identifier) @_func)) - (#any-of? @_func "TypeVar" "NewType")) - -; Function calls - -(call - function: (identifier) @function.call) - -(call - function: (attribute - attribute: (identifier) @method.call)) - -((call - function: (identifier) @constructor) - (#lua-match? @constructor "^%u")) - -((call - function: (attribute - attribute: (identifier) @constructor)) - (#lua-match? @constructor "^%u")) - -;; Decorators - -((decorator "@" @attribute) - (#set! "priority" 101)) - -(decorator - (identifier) @attribute) -(decorator - (attribute - attribute: (identifier) @attribute)) -(decorator - (call (identifier) @attribute)) -(decorator - (call (attribute - attribute: (identifier) @attribute))) - -((decorator - (identifier) @attribute.builtin) - (#any-of? @attribute.builtin "classmethod" "property")) - -;; Builtin functions - -((call - function: (identifier) @function.builtin) - (#any-of? @function.builtin - "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__")) - -;; Function definitions - -(function_definition - name: (identifier) @function) - -(type (identifier) @type) -(type - (subscript - (identifier) @type)) ; type subscript: Tuple[int] - -((call - function: (identifier) @_isinstance - arguments: (argument_list - (_) - (identifier) @type)) - (#eq? @_isinstance "isinstance")) - -;; Normal parameters -(parameters - (identifier) @parameter) -;; Lambda parameters -(lambda_parameters - (identifier) @parameter) -(lambda_parameters - (tuple_pattern - (identifier) @parameter)) -; Default parameters -(keyword_argument - name: (identifier) @parameter) -; Naming parameters on call-site -(default_parameter - name: (identifier) @parameter) -(typed_parameter - (identifier) @parameter) -(typed_default_parameter - (identifier) @parameter) -; Variadic parameters *args, **kwargs -(parameters - (list_splat_pattern ; *args - (identifier) @parameter)) -(parameters - (dictionary_splat_pattern ; **kwargs - (identifier) @parameter)) - - -;; Literals - -(none) @constant.builtin -[(true) (false)] @boolean -((identifier) @variable.builtin - (#eq? @variable.builtin "self")) -((identifier) @variable.builtin - (#eq? @variable.builtin "cls")) - -(integer) @number -(float) @float - -(comment) @comment @spell - -((module . (comment) @preproc) - (#lua-match? @preproc "^#!/")) - -(string) @string -(escape_sequence) @string.escape - -; doc-strings - -(module . (expression_statement (string) @string.documentation @spell)) - -(class_definition - body: - (block - . (expression_statement (string) @string.documentation @spell))) - -(function_definition - body: - (block - . (expression_statement (string) @string.documentation @spell))) - -; Tokens - -[ - "-" - "-=" - ":=" - "!=" - "*" - "**" - "**=" - "*=" - "/" - "//" - "//=" - "/=" - "&" - "&=" - "%" - "%=" - "^" - "^=" - "+" - "+=" - "<" - "<<" - "<<=" - "<=" - "<>" - "=" - "==" - ">" - ">=" - ">>" - ">>=" - "@" - "@=" - "|" - "|=" - "~" - "->" -] @operator - -; Keywords -[ - "and" - "in" - "is" - "not" - "or" - "is not" - "not in" - - "del" -] @keyword.operator - -[ - "def" - "lambda" -] @keyword.function - -[ - "assert" - "class" - "exec" - "global" - "nonlocal" - "pass" - "print" - "with" - "as" -] @keyword - -[ - "async" - "await" -] @keyword.coroutine - -[ - "return" - "yield" -] @keyword.return -(yield "from" @keyword.return) - -(future_import_statement - "from" @include - "__future__" @constant.builtin) -(import_from_statement "from" @include) -"import" @include - -(aliased_import "as" @include) - -["if" "elif" "else" "match" "case"] @conditional - -["for" "while" "break" "continue"] @repeat - -[ - "try" - "except" - "except*" - "raise" - "finally" -] @exception - -(raise_statement "from" @exception) - -(try_statement - (else_clause - "else" @exception)) - -["(" ")" "[" "]" "{" "}"] @punctuation.bracket - -(interpolation - "{" @punctuation.special - "}" @punctuation.special) - -(type_conversion) @function.macro - -["," "." ":" ";" (ellipsis)] @punctuation.delimiter - -;; Class definitions - -(class_definition name: (identifier) @type) - -(class_definition - body: (block - (function_definition - name: (identifier) @method))) - -(class_definition - superclasses: (argument_list - (identifier) @type)) - -((class_definition - body: (block - (expression_statement - (assignment - left: (identifier) @field)))) - (#lua-match? @field "^%l.*$")) -((class_definition - body: (block - (expression_statement - (assignment - left: (_ - (identifier) @field))))) - (#lua-match? @field "^%l.*$")) - -((class_definition - (block - (function_definition - name: (identifier) @constructor))) - (#any-of? @constructor "__new__" "__init__")) - -((identifier) @type.builtin - (#any-of? @type.builtin - ;; https://docs.python.org/3/library/exceptions.html - "BaseException" "Exception" "ArithmeticError" "BufferError" "LookupError" "AssertionError" "AttributeError" - "EOFError" "FloatingPointError" "GeneratorExit" "ImportError" "ModuleNotFoundError" "IndexError" "KeyError" - "KeyboardInterrupt" "MemoryError" "NameError" "NotImplementedError" "OSError" "OverflowError" "RecursionError" - "ReferenceError" "RuntimeError" "StopIteration" "StopAsyncIteration" "SyntaxError" "IndentationError" "TabError" - "SystemError" "SystemExit" "TypeError" "UnboundLocalError" "UnicodeError" "UnicodeEncodeError" "UnicodeDecodeError" - "UnicodeTranslateError" "ValueError" "ZeroDivisionError" "EnvironmentError" "IOError" "WindowsError" - "BlockingIOError" "ChildProcessError" "ConnectionError" "BrokenPipeError" "ConnectionAbortedError" - "ConnectionRefusedError" "ConnectionResetError" "FileExistsError" "FileNotFoundError" "InterruptedError" - "IsADirectoryError" "NotADirectoryError" "PermissionError" "ProcessLookupError" "TimeoutError" "Warning" - "UserWarning" "DeprecationWarning" "PendingDeprecationWarning" "SyntaxWarning" "RuntimeWarning" - "FutureWarning" "ImportWarning" "UnicodeWarning" "BytesWarning" "ResourceWarning" - ;; https://docs.python.org/3/library/stdtypes.html - "bool" "int" "float" "complex" "list" "tuple" "range" "str" - "bytes" "bytearray" "memoryview" "set" "frozenset" "dict" "type" "object")) - -;; Error -(ERROR) @error From 2043e063ff118d8c4cd0823af0eb07a80f4034d6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 14:39:24 +0100 Subject: [PATCH 189/366] Fix some Python highlighting issues --- .../document/_syntax_aware_document.py | 40 +++++++++++++------ tree-sitter/highlights/python.scm | 8 ++++ 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 7d055b3a3b..0783d1293a 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -20,7 +20,7 @@ TREE_SITTER = False from textual._fix_direction import _fix_direction -from textual.document._document import Document, _utf8_encode +from textual.document._document import Document, Location, _utf8_encode from textual.document._languages import VALID_LANGUAGES from textual.document._syntax_theme import SyntaxTheme @@ -109,21 +109,22 @@ def insert_range( # An optimisation would be finding the byte offsets as a single operation rather # than doing two passes over the document content. - start_byte = self._tree_sitter_byte_offset(top) - old_end_byte = self._tree_sitter_byte_offset(bottom) + start_byte = self._location_to_byte_offset(top) + start_point = self._location_to_point(top) + old_end_byte = self._location_to_byte_offset(bottom) + old_end_point = self._location_to_point(bottom) end_location = super().insert_range(start, end, text) - # TODO: columns in tree-sitter points appear to be byte-offsets if TREE_SITTER: text_byte_length = len(_utf8_encode(text)) self._syntax_tree.edit( start_byte=start_byte, old_end_byte=old_end_byte, new_end_byte=start_byte + text_byte_length, - start_point=top, - old_end_point=bottom, - new_end_point=end_location, + start_point=start_point, + old_end_point=old_end_point, + new_end_point=self._location_to_point(end_location), ) self._syntax_tree = self._parser.parse( self._read_callable, self._syntax_tree @@ -148,8 +149,10 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: """ top, bottom = _fix_direction(start, end) - start_byte = self._tree_sitter_byte_offset(top) - old_end_byte = self._tree_sitter_byte_offset(bottom) + start_point = self._location_to_point(top) + old_end_point = self._location_to_point(bottom) + start_byte = self._location_to_byte_offset(top) + old_end_byte = self._location_to_byte_offset(bottom) deleted_text = super().delete_range(start, end) @@ -159,9 +162,9 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: start_byte=start_byte, old_end_byte=old_end_byte, new_end_byte=old_end_byte - deleted_text_byte_length, - start_point=top, - old_end_point=bottom, - new_end_point=top, + start_point=start_point, + old_end_point=old_end_point, + new_end_point=self._location_to_point(top), ) new_tree = self._parser.parse(self._read_callable, self._syntax_tree) self._syntax_tree = new_tree @@ -201,7 +204,7 @@ def tree_query(self, tree_query: str) -> list[object]: return list(captures) - def _tree_sitter_byte_offset(self, location: tuple[int, int]) -> int: + def _location_to_byte_offset(self, location: tuple[int, int]) -> int: """Given a document coordinate, return the byte offset of that coordinate. This method only does work if tree-sitter was imported, otherwise it returns 0. @@ -228,6 +231,17 @@ def _tree_sitter_byte_offset(self, location: tuple[int, int]) -> int: byte_offset = bytes_lines_above + bytes_on_left return byte_offset + def _location_to_point(self, location: Location) -> tuple[int, int]: + """Convert a document location (row_index, column_index) to a tree-sitter + point (row_index, byte_offset_from_start_of_row).""" + lines = self._lines + row, column = location + if row < len(lines): + bytes_on_left = len(_utf8_encode(lines[row][:column])) + else: + bytes_on_left = 0 + return row, bytes_on_left + def _prepare_highlights( self, start_point: tuple[int, int] | None = None, diff --git a/tree-sitter/highlights/python.scm b/tree-sitter/highlights/python.scm index 249306b58d..74074043ff 100644 --- a/tree-sitter/highlights/python.scm +++ b/tree-sitter/highlights/python.scm @@ -283,6 +283,14 @@ (else_clause "else" @exception)) +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + +(interpolation + "{" @punctuation.special + "}" @punctuation.special) + +["," "." ":" ";" (ellipsis)] @punctuation.delimiter + ;; Class definitions (class_definition name: (identifier) @type) From a886250e1070dbd035243a31f842b2aa19f4db42 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 14:47:59 +0100 Subject: [PATCH 190/366] Make HTML syntax highlight nicely --- src/textual/document/_syntax_theme.py | 1 + .../__snapshots__/test_snapshots.ambr | 255 +++++++++--------- tree-sitter/highlights/html.scm | 61 ++++- 3 files changed, 190 insertions(+), 127 deletions(-) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index 7ccad1874e..02cb2c864a 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -30,6 +30,7 @@ "regex.punctuation.bracket": Style(color="#F92672"), "regex.operator": Style(color="#F92672"), "error": Style(color="black", bgcolor="red"), + "tag": Style(color="#F92672"), } _BUILTIN_THEMES = { diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index b800a59abd..81c12f996c 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -27843,270 +27843,273 @@ font-weight: 700; } - .terminal-1143762621-matrix { + .terminal-2642746815-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1143762621-title { + .terminal-2642746815-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1143762621-r1 { fill: #e4e5e6 } - .terminal-1143762621-r2 { fill: #1b1b1b } - .terminal-1143762621-r3 { fill: #c5c8c6 } - .terminal-1143762621-r4 { fill: #7b7e82 } - .terminal-1143762621-r5 { fill: #e2e3e3 } + .terminal-2642746815-r1 { fill: #e4e5e6 } + .terminal-2642746815-r2 { fill: #1b1b1b } + .terminal-2642746815-r3 { fill: #c5c8c6 } + .terminal-2642746815-r4 { fill: #7b7e82 } + .terminal-2642746815-r5 { fill: #e2e3e3 } + .terminal-2642746815-r6 { fill: #f92672 } + .terminal-2642746815-r7 { fill: #e6db74 } + .terminal-2642746815-r8 { fill: #75715e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  <!DOCTYPE html>                                                              -  2  <html lang="en">                                                            -  3   -  4  <head>                                                                      -  5      <!-- Meta tags -->                                                      -  6      <meta charset="UTF-8">                                                  -  7      <meta name="viewport" content="width=device-width, initial-scale=1.0">  -  8      <!-- Title -->                                                          -  9      <title>HTML Test Page</title>                                           - 10      <!-- Link to CSS -->                                                    - 11      <link rel="stylesheet" href="styles.css">                               - 12  </head>                                                                     - 13   - 14  <body>                                                                      - 15      <!-- Header section -->                                                 - 16      <header class="header">                                                 - 17          <h1 id="logo">HTML Test Page</h1>                                   - 18      </header>                                                               - 19   - 20      <!-- Navigation -->                                                     - 21      <nav class="nav">                                                       - 22          <ul>                                                                - 23              <li><a href="#">Home</a></li>                                   - 24              <li><a href="#">About</a></li>                                  - 25              <li><a href="#">Contact</a></li>                                - 26          </ul>                                                               - 27      </nav>                                                                  - 28   - 29      <!-- Main content area -->                                              - 30      <main>                                                                  - 31          <article>                                                           - 32              <h2>Welcome to the Test Page</h2>                               - 33              <p>This is a paragraph to test the HTML structure.</p>          - 34              <img src="test-image.jpg" alt="Test Image" width="300">         - 35          </article>                                                          - 36      </main>                                                                 - 37   - 38      <!-- Form -->                                                           - 39      <section>                                                               - 40          <form action="/submit" method="post">                               - 41              <label for="name">Name:</label>                                 - 42              <input type="text" id="name" name="name">                       - 43              <input type="submit" value="Submit">                            - 44          </form>                                                             - 45      </section>                                                              - 46   - 47      <!-- Footer -->                                                         - 48      <footer>                                                                - 49          <p>&copy; 2023 HTML Test Page</p>                                   - 50      </footer>                                                               - 51   - 52      <!-- Script tag -->                                                     - 53      <script src="scripts.js"></script>                                      - 54  </body>                                                                     - 55   - 56  </html>                                                                     - 57   + + + +  1  <!DOCTYPE html>                                                              +  2  <html lang="en">                                                            +  3   +  4  <head>                                                                      +  5  <!-- Meta tags --> +  6      <meta charset="UTF-8">                                                  +  7      <meta name="viewport" content="width=device-width, initial-scale=1.0" +  8  <!-- Title --> +  9      <title>HTML Test Page</title>                                           + 10  <!-- Link to CSS --> + 11      <link rel="stylesheet" href="styles.css">                               + 12  </head>                                                                     + 13   + 14  <body>                                                                      + 15  <!-- Header section --> + 16      <header class="header">                                                 + 17          <h1 id="logo">HTML Test Page</h1>                                   + 18      </header>                                                               + 19   + 20  <!-- Navigation --> + 21      <nav class="nav">                                                       + 22          <ul>                                                                + 23              <li><a href="#">Home</a></li>                                   + 24              <li><a href="#">About</a></li>                                  + 25              <li><a href="#">Contact</a></li>                                + 26          </ul>                                                               + 27      </nav>                                                                  + 28   + 29  <!-- Main content area --> + 30      <main>                                                                  + 31          <article>                                                           + 32              <h2>Welcome to the Test Page</h2>                               + 33              <p>This is a paragraph to test the HTML structure.</p>          + 34              <img src="test-image.jpg" alt="Test Image" width="300">         + 35          </article>                                                          + 36      </main>                                                                 + 37   + 38  <!-- Form --> + 39      <section>                                                               + 40          <form action="/submit" method="post">                               + 41              <label for="name">Name:</label>                                 + 42              <input type="text" id="name" name="name">                       + 43              <input type="submit" value="Submit">                            + 44          </form>                                                             + 45      </section>                                                              + 46   + 47  <!-- Footer --> + 48      <footer>                                                                + 49          <p>&copy; 2023 HTML Test Page</p>                                   + 50      </footer>                                                               + 51   + 52  <!-- Script tag --> + 53      <script src="scripts.js"></script>                                      + 54  </body>                                                                     + 55   + 56  </html>                                                                     + 57   diff --git a/tree-sitter/highlights/html.scm b/tree-sitter/highlights/html.scm index 6da261c0aa..feac8009ec 100644 --- a/tree-sitter/highlights/html.scm +++ b/tree-sitter/highlights/html.scm @@ -1,4 +1,63 @@ -; inherits: html_tags +(tag_name) @tag +(erroneous_end_tag_name) @error +(comment) @comment +(attribute_name) @tag.attribute +(attribute + (quoted_attribute_value) @string) +(text) @text @spell + +((element (start_tag (tag_name) @_tag) (text) @text.title) + (#eq? @_tag "title")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.1) + (#eq? @_tag "h1")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.2) + (#eq? @_tag "h2")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.3) + (#eq? @_tag "h3")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.4) + (#eq? @_tag "h4")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.5) + (#eq? @_tag "h5")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.6) + (#eq? @_tag "h6")) + +((element (start_tag (tag_name) @_tag) (text) @text.strong) + (#any-of? @_tag "strong" "b")) + +((element (start_tag (tag_name) @_tag) (text) @text.emphasis) + (#any-of? @_tag "em" "i")) + +((element (start_tag (tag_name) @_tag) (text) @text.strike) + (#any-of? @_tag "s" "del")) + +((element (start_tag (tag_name) @_tag) (text) @text.underline) + (#eq? @_tag "u")) + +((element (start_tag (tag_name) @_tag) (text) @text.literal) + (#any-of? @_tag "code" "kbd")) + +((element (start_tag (tag_name) @_tag) (text) @text.uri) + (#eq? @_tag "a")) + +((attribute + (attribute_name) @_attr + (quoted_attribute_value (attribute_value) @text.uri)) + (#any-of? @_attr "href" "src")) + +[ + "<" + ">" + "" +] @tag.delimiter + +"=" @operator (doctype) @constant From 5582b12e6b133defbceb55c1e616c7a659144f83 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 15:14:31 +0100 Subject: [PATCH 191/366] Create tag name for mismatching HTML end tag --- src/textual/document/_syntax_theme.py | 1 + tree-sitter/highlights/html.scm | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index 02cb2c864a..a234c0abbe 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -30,6 +30,7 @@ "regex.punctuation.bracket": Style(color="#F92672"), "regex.operator": Style(color="#F92672"), "error": Style(color="black", bgcolor="red"), + "html.end_tag_error": Style(color="black", bgcolor="red"), "tag": Style(color="#F92672"), } diff --git a/tree-sitter/highlights/html.scm b/tree-sitter/highlights/html.scm index feac8009ec..15f2adb436 100644 --- a/tree-sitter/highlights/html.scm +++ b/tree-sitter/highlights/html.scm @@ -1,5 +1,5 @@ (tag_name) @tag -(erroneous_end_tag_name) @error +(erroneous_end_tag_name) @html.end_tag_error (comment) @comment (attribute_name) @tag.attribute (attribute From fd3dc98d48d7802886bf4335e15325d379b4d6ac Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 15:46:51 +0100 Subject: [PATCH 192/366] Add styling for YAML, update boolean styling --- src/textual/document/_syntax_theme.py | 2 + .../__snapshots__/test_snapshots.ambr | 474 +++++++++--------- tree-sitter/highlights/yaml.scm | 8 +- 3 files changed, 245 insertions(+), 239 deletions(-) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index a234c0abbe..7321447c3d 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -21,6 +21,7 @@ "function.call": Style(color="#A6E22E"), "method": Style(color="#A6E22E"), "method.call": Style(color="#A6E22E"), + "boolean": Style(color="#66D9EF", italic=True), # "constant": Style(color="#AE81FF"), "variable": Style(color="white"), "parameter": Style(color="cyan"), @@ -32,6 +33,7 @@ "error": Style(color="black", bgcolor="red"), "html.end_tag_error": Style(color="black", bgcolor="red"), "tag": Style(color="#F92672"), + "yaml.field": Style(color="#F92672", bold=True), } _BUILTIN_THEMES = { diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 81c12f996c..79dd13af2c 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -28140,169 +28140,170 @@ font-weight: 700; } - .terminal-2767018341-matrix { + .terminal-647816404-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2767018341-title { + .terminal-647816404-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2767018341-r1 { fill: #e4e5e6 } - .terminal-2767018341-r2 { fill: #1b1b1b } - .terminal-2767018341-r3 { fill: #c5c8c6 } - .terminal-2767018341-r4 { fill: #7b7e82 } - .terminal-2767018341-r5 { fill: #e2e3e3 } - .terminal-2767018341-r6 { fill: #e6db74 } - .terminal-2767018341-r7 { fill: #ae81ff } - .terminal-2767018341-r8 { fill: #4b4e55 } + .terminal-647816404-r1 { fill: #e4e5e6 } + .terminal-647816404-r2 { fill: #1b1b1b } + .terminal-647816404-r3 { fill: #c5c8c6 } + .terminal-647816404-r4 { fill: #7b7e82 } + .terminal-647816404-r5 { fill: #e2e3e3 } + .terminal-647816404-r6 { fill: #e6db74 } + .terminal-647816404-r7 { fill: #ae81ff } + .terminal-647816404-r8 { fill: #66d9ef;font-style: italic; } + .terminal-647816404-r9 { fill: #4b4e55 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  { -  2      "name": "John Doe",                            -  3      "age": 30,                                     -  4      "isStudent": false,                            -  5      "address": {                                   -  6          "street": "123 Main St",                   -  7          "city": "Anytown",                         -  8          "state": "CA",                             -  9          "zip": "12345" - 10      },                                             - 11      "phoneNumbers": [                              - 12          {                                          - 13              "type": "home",                        - 14              "number": "555-555-1234" - 15          },                                         - 16          {                                          - 17              "type": "work",                        - 18              "number": "555-555-5678" - 19          }                                          - 20      ],                                             - 21      "hobbies": ["reading""hiking""swimming"],  - 22      "pets": [                                      - 23          {                                          - 24              "type": "dog",                         - 25              "name": "Fido" - 26          }, - 27      ],                                             - 28      "graduationYear": null                         - 29  }                                                  - 30   - 31   + + + +  1  { +  2      "name": "John Doe",                            +  3      "age": 30,                                     +  4      "isStudent": false,                            +  5      "address": {                                   +  6          "street": "123 Main St",                   +  7          "city": "Anytown",                         +  8          "state": "CA",                             +  9          "zip": "12345" + 10      },                                             + 11      "phoneNumbers": [                              + 12          {                                          + 13              "type": "home",                        + 14              "number": "555-555-1234" + 15          },                                         + 16          {                                          + 17              "type": "work",                        + 18              "number": "555-555-5678" + 19          }                                          + 20      ],                                             + 21      "hobbies": ["reading""hiking""swimming"],  + 22      "pets": [                                      + 23          {                                          + 24              "type": "dog",                         + 25              "name": "Fido" + 26          }, + 27      ],                                             + 28      "graduationYear": null                         + 29  }                                                  + 30   + 31   @@ -29476,149 +29477,150 @@ font-weight: 700; } - .terminal-3176236472-matrix { + .terminal-988519589-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3176236472-title { + .terminal-988519589-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3176236472-r1 { fill: #e4e5e6 } - .terminal-3176236472-r2 { fill: #1b1b1b } - .terminal-3176236472-r3 { fill: #75715e } - .terminal-3176236472-r4 { fill: #c5c8c6 } - .terminal-3176236472-r5 { fill: #7b7e82 } - .terminal-3176236472-r6 { fill: #e2e3e3 } - .terminal-3176236472-r7 { fill: #e6db74 } - .terminal-3176236472-r8 { fill: #ae81ff } + .terminal-988519589-r1 { fill: #e4e5e6 } + .terminal-988519589-r2 { fill: #1b1b1b } + .terminal-988519589-r3 { fill: #75715e } + .terminal-988519589-r4 { fill: #c5c8c6 } + .terminal-988519589-r5 { fill: #7b7e82 } + .terminal-988519589-r6 { fill: #e2e3e3 } + .terminal-988519589-r7 { fill: #e6db74 } + .terminal-988519589-r8 { fill: #ae81ff } + .terminal-988519589-r9 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  # This is a comment in TOML -  2   -  3  string = "Hello, world!" -  4  integer = 42 -  5  float = 3.14                            -  6  boolean = true                          -  7  datetime = 1979-05-27T07:32:00Z         -  8   -  9  fruits = ["apple""banana""cherry" - 10   - 11  [address]                               - 12  street = "123 Main St" - 13  city = "Anytown" - 14  state = "CA" - 15  zip = "12345" - 16   - 17  [person.john]                           - 18  name = "John Doe" - 19  age = 28 - 20  is_student = false                      - 21   - 22   - 23  [[animals]]                             - 24  name = "Fido" - 25  type = "dog" - 26   + + + +  1  # This is a comment in TOML +  2   +  3  string = "Hello, world!" +  4  integer = 42 +  5  float = 3.14                            +  6  boolean = true +  7  datetime = 1979-05-27T07:32:00Z         +  8   +  9  fruits = ["apple""banana""cherry" + 10   + 11  [address]                               + 12  street = "123 Main St" + 13  city = "Anytown" + 14  state = "CA" + 15  zip = "12345" + 16   + 17  [person.john]                           + 18  name = "John Doe" + 19  age = 28 + 20  is_student = false + 21   + 22   + 23  [[animals]]                             + 24  name = "Fido" + 25  type = "dog" + 26   @@ -29649,197 +29651,199 @@ font-weight: 700; } - .terminal-603738614-matrix { + .terminal-3785768015-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-603738614-title { + .terminal-3785768015-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-603738614-r1 { fill: #e4e5e6 } - .terminal-603738614-r2 { fill: #1b1b1b } - .terminal-603738614-r3 { fill: #75715e } - .terminal-603738614-r4 { fill: #c5c8c6 } - .terminal-603738614-r5 { fill: #7b7e82 } - .terminal-603738614-r6 { fill: #e2e3e3 } - .terminal-603738614-r7 { fill: #e6db74 } - .terminal-603738614-r8 { fill: #ae81ff } + .terminal-3785768015-r1 { fill: #e4e5e6 } + .terminal-3785768015-r2 { fill: #1b1b1b } + .terminal-3785768015-r3 { fill: #75715e } + .terminal-3785768015-r4 { fill: #c5c8c6 } + .terminal-3785768015-r5 { fill: #7b7e82 } + .terminal-3785768015-r6 { fill: #e2e3e3 } + .terminal-3785768015-r7 { fill: #f92672;font-weight: bold } + .terminal-3785768015-r8 { fill: #e6db74 } + .terminal-3785768015-r9 { fill: #ae81ff } + .terminal-3785768015-r10 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  # This is a comment in YAML -  2   -  3  # Scalars -  4  string"Hello, world!" -  5  integer42 -  6  float3.14 -  7  boolean: true                                         -  8   -  9  # Sequences (Arrays) - 10  fruits:                                               - 11    - Apple - 12    - Banana - 13    - Cherry - 14   - 15  # Nested sequences - 16  persons:                                              - 17    - nameJohn - 18  age28 - 19  is_student: false                                 - 20    - nameJane - 21  age22 - 22  is_student: true                                  - 23   - 24  # Mappings (Dictionaries) - 25  address:                                              - 26  street123 Main St - 27  cityAnytown - 28  stateCA - 29  zip'12345' - 30   - 31  # Multiline string - 32  description| - 33    This is a multiline - 34    string in YAML. - 35   - 36  # Inline and nested collections - 37  colors: { redFF0000green00FF00blue0000FF }  - 38   + + + +  1  # This is a comment in YAML +  2   +  3  # Scalars +  4  string"Hello, world!" +  5  integer42 +  6  float3.14 +  7  booleantrue +  8   +  9  # Sequences (Arrays) + 10  fruits:                                               + 11    - Apple + 12    - Banana + 13    - Cherry + 14   + 15  # Nested sequences + 16  persons:                                              + 17    - nameJohn + 18  age28 + 19  is_studentfalse + 20    - nameJane + 21  age22 + 22  is_studenttrue + 23   + 24  # Mappings (Dictionaries) + 25  address:                                              + 26  street123 Main St + 27  cityAnytown + 28  stateCA + 29  zip'12345' + 30   + 31  # Multiline string + 32  description| + 33    This is a multiline + 34    string in YAML. + 35   + 36  # Inline and nested collections + 37  colors: { redFF0000green00FF00blue0000FF }  + 38   diff --git a/tree-sitter/highlights/yaml.scm b/tree-sitter/highlights/yaml.scm index fb3338eb3a..d2ab774bf4 100644 --- a/tree-sitter/highlights/yaml.scm +++ b/tree-sitter/highlights/yaml.scm @@ -20,14 +20,14 @@ ] @preproc (block_mapping_pair - key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @field)) + key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @yaml.field)) (block_mapping_pair - key: (flow_node (plain_scalar (string_scalar) @field))) + key: (flow_node (plain_scalar (string_scalar) @yaml.field))) (flow_mapping - (_ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @field))) + (_ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @yaml.field))) (flow_mapping - (_ key: (flow_node (plain_scalar (string_scalar) @field)))) + (_ key: (flow_node (plain_scalar (string_scalar) @yaml.field)))) [ "," From 8804aa526e7e0d4b1db1b29f3d1c97baf602335a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 15:51:05 +0100 Subject: [PATCH 193/366] Stylising toml types --- src/textual/document/_syntax_theme.py | 1 + tree-sitter/highlights/toml.scm | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index 7321447c3d..7438d96835 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -34,6 +34,7 @@ "html.end_tag_error": Style(color="black", bgcolor="red"), "tag": Style(color="#F92672"), "yaml.field": Style(color="#F92672", bold=True), + "toml.type": Style(color="#F92672"), } _BUILTIN_THEMES = { diff --git a/tree-sitter/highlights/toml.scm b/tree-sitter/highlights/toml.scm index 5255a20ab0..edb609f43d 100644 --- a/tree-sitter/highlights/toml.scm +++ b/tree-sitter/highlights/toml.scm @@ -1,7 +1,7 @@ ; Properties ;----------- -(bare_key) @type +(bare_key) @toml.type (quoted_key) @string (pair (bare_key)) @property From 3770e652808777e79d5389b4f800568517c5acea Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 15:52:13 +0100 Subject: [PATCH 194/366] Styling floats --- src/textual/document/_syntax_theme.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index 7438d96835..71a8bde31f 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -16,6 +16,7 @@ "keyword.return": Style(color="#F92672"), "conditional": Style(color="#F92672"), "number": Style(color="#AE81FF"), + "float": Style(color="#AE81FF"), "class": Style(color="#A6E22E"), "function": Style(color="#A6E22E"), "function.call": Style(color="#A6E22E"), From ae2f70c9f0636b81e0626de00d7f65dc37cff17c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 16:38:55 +0100 Subject: [PATCH 195/366] JSON syntax highlighting --- src/textual/document/_syntax_aware_document.py | 1 - src/textual/document/_syntax_theme.py | 3 +++ tree-sitter/highlights/json.scm | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 0783d1293a..0fc6b7c64f 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -310,7 +310,6 @@ def _build_ast( def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | None: row, column = point lines = self._lines - newline = self.newline row_out_of_bounds = row >= len(lines) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index 71a8bde31f..65b8f69bdc 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -23,6 +23,7 @@ "method": Style(color="#A6E22E"), "method.call": Style(color="#A6E22E"), "boolean": Style(color="#66D9EF", italic=True), + "json.null": Style(color="#66D9EF", italic=True), # "constant": Style(color="#AE81FF"), "variable": Style(color="white"), "parameter": Style(color="cyan"), @@ -32,9 +33,11 @@ "regex.punctuation.bracket": Style(color="#F92672"), "regex.operator": Style(color="#F92672"), "error": Style(color="black", bgcolor="red"), + "json.error": None, "html.end_tag_error": Style(color="black", bgcolor="red"), "tag": Style(color="#F92672"), "yaml.field": Style(color="#F92672", bold=True), + "json.label": Style(color="#F92672", bold=True), "toml.type": Style(color="#F92672"), } diff --git a/tree-sitter/highlights/json.scm b/tree-sitter/highlights/json.scm index c4bfade8ab..c23e7b3ce9 100644 --- a/tree-sitter/highlights/json.scm +++ b/tree-sitter/highlights/json.scm @@ -3,18 +3,18 @@ (false) ] @boolean -(null) @constant.builtin +(null) @json.null (number) @number -(pair key: (string) @label) +(pair key: (string) @json.label) (pair value: (string) @string) (array (string) @string) (string_content) @spell -(ERROR) @error +(ERROR) @json.error ["," ":"] @punctuation.delimiter From 9c17c4ffe4aab35a5d696dee8c579946cd99cb82 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 16:39:31 +0100 Subject: [PATCH 196/366] Updating snapshots --- .../__snapshots__/test_snapshots.ambr | 645 +++++++++--------- 1 file changed, 323 insertions(+), 322 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 79dd13af2c..219f88317c 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -28140,170 +28140,170 @@ font-weight: 700; } - .terminal-647816404-matrix { + .terminal-3961698941-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-647816404-title { + .terminal-3961698941-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-647816404-r1 { fill: #e4e5e6 } - .terminal-647816404-r2 { fill: #1b1b1b } - .terminal-647816404-r3 { fill: #c5c8c6 } - .terminal-647816404-r4 { fill: #7b7e82 } - .terminal-647816404-r5 { fill: #e2e3e3 } - .terminal-647816404-r6 { fill: #e6db74 } - .terminal-647816404-r7 { fill: #ae81ff } - .terminal-647816404-r8 { fill: #66d9ef;font-style: italic; } - .terminal-647816404-r9 { fill: #4b4e55 } + .terminal-3961698941-r1 { fill: #e4e5e6 } + .terminal-3961698941-r2 { fill: #1b1b1b } + .terminal-3961698941-r3 { fill: #c5c8c6 } + .terminal-3961698941-r4 { fill: #7b7e82 } + .terminal-3961698941-r5 { fill: #e2e3e3 } + .terminal-3961698941-r6 { fill: #f92672;font-weight: bold } + .terminal-3961698941-r7 { fill: #e6db74 } + .terminal-3961698941-r8 { fill: #ae81ff } + .terminal-3961698941-r9 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  { -  2      "name": "John Doe",                            -  3      "age": 30,                                     -  4      "isStudent": false,                            -  5      "address": {                                   -  6          "street": "123 Main St",                   -  7          "city": "Anytown",                         -  8          "state": "CA",                             -  9          "zip": "12345" - 10      },                                             - 11      "phoneNumbers": [                              - 12          {                                          - 13              "type": "home",                        - 14              "number": "555-555-1234" - 15          },                                         - 16          {                                          - 17              "type": "work",                        - 18              "number": "555-555-5678" - 19          }                                          - 20      ],                                             - 21      "hobbies": ["reading""hiking""swimming"],  - 22      "pets": [                                      - 23          {                                          - 24              "type": "dog",                         - 25              "name": "Fido" - 26          }, - 27      ],                                             - 28      "graduationYear": null                         - 29  }                                                  - 30   - 31   + + + +  1  { +  2  "name""John Doe",                            +  3  "age"30,                                     +  4  "isStudent"false,                            +  5  "address": {                                   +  6  "street""123 Main St",                   +  7  "city""Anytown",                         +  8  "state""CA",                             +  9  "zip""12345" + 10      },                                             + 11  "phoneNumbers": [                              + 12          {                                          + 13  "type""home",                        + 14  "number""555-555-1234" + 15          },                                         + 16          {                                          + 17  "type""work",                        + 18  "number""555-555-5678" + 19          }                                          + 20      ],                                             + 21  "hobbies": ["reading""hiking""swimming"],  + 22  "pets": [                                      + 23          {                                          + 24  "type""dog",                         + 25  "name""Fido" + 26          },                                         + 27      ],                                             + 28  "graduationYear"null + 29  }                                                  + 30   + 31   @@ -28675,364 +28675,364 @@ font-weight: 700; } - .terminal-3741042815-matrix { + .terminal-1580289934-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3741042815-title { + .terminal-1580289934-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3741042815-r1 { fill: #e4e5e6 } - .terminal-3741042815-r2 { fill: #1b1b1b } - .terminal-3741042815-r3 { fill: #f92672 } - .terminal-3741042815-r4 { fill: #c5c8c6 } - .terminal-3741042815-r5 { fill: #7b7e82 } - .terminal-3741042815-r6 { fill: #e2e3e3 } - .terminal-3741042815-r7 { fill: #75715e } - .terminal-3741042815-r8 { fill: #e6db74 } - .terminal-3741042815-r9 { fill: #ae81ff } - .terminal-3741042815-r10 { fill: #a6e22e } - .terminal-3741042815-r11 { fill: #68a0b3 } + .terminal-1580289934-r1 { fill: #e4e5e6 } + .terminal-1580289934-r2 { fill: #1b1b1b } + .terminal-1580289934-r3 { fill: #f92672 } + .terminal-1580289934-r4 { fill: #c5c8c6 } + .terminal-1580289934-r5 { fill: #7b7e82 } + .terminal-1580289934-r6 { fill: #e2e3e3 } + .terminal-1580289934-r7 { fill: #75715e } + .terminal-1580289934-r8 { fill: #e6db74 } + .terminal-1580289934-r9 { fill: #ae81ff } + .terminal-1580289934-r10 { fill: #a6e22e } + .terminal-1580289934-r11 { fill: #68a0b3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  importmath -  2  fromosimportpath -  3   -  4  # I'm a comment :) -  5   -  6  string_var = "Hello, world!" -  7  int_var = 42 -  8  float_var = 3.14                                                             -  9  complex_var = 1 + 2j - 10   - 11  list_var = [12345]                                                   - 12  tuple_var = (12345)                                                  - 13  set_var = {12345}                                                    - 14  dict_var = {"a"1"b"2"c"3}                                          - 15   - 16  deffunction_no_args():                                                      - 17  return"No arguments" - 18   - 19  deffunction_with_args(ab):                                                - 20  returna + b - 21   - 22  deffunction_with_default_args(a=0b=0):                                    - 23  returna * b - 24   - 25  lambda_func = lambdaxx**2 - 26   - 27  ifint_var == 42:                                                            - 28  print("It's the answer!")                                                - 29  elifint_var < 42:                                                           - 30  print("Less than the answer.")                                           - 31  else:                                                                        - 32  print("Greater than the answer.")                                        - 33   - 34  forindexvalue in enumerate(list_var):                                     - 35  print(f"Index: {index}, Value: {value}")                                 - 36   - 37  counter = 0 - 38  whilecounter < 5:                                                           - 39  print(f"Counter value: {counter}")                                       - 40  counter += 1 - 41   - 42  squared_numbers = [x**2forx in range(10ifx % 2 == 0]                    - 43   - 44  try:                                                                         - 45  result = 10 / 0 - 46  exceptZeroDivisionError:                                                    - 47  print("Cannot divide by zero!")                                          - 48  finally:                                                                     - 49  print("End of try-except block.")                                        - 50   - 51  classAnimal:                                                                - 52  def__init__(selfname):                                                - 53  self.name = name - 54   - 55  defspeak(self):                                                         - 56  raiseNotImplementedError("Subclasses must implement this method." - 57   - 58  classDog(Animal):                                                           - 59  defspeak(self):                                                         - 60  returnf"{self.name} says Woof!" - 61   - 62  deffibonacci(n):                                                            - 63  ab = 01 - 64  for_ in range(n):                                                       - 65  yielda - 66  ab = ba + b - 67   - 68  fornum in fibonacci(5):                                                     - 69  print(num)                                                               - 70   - 71  withopen('test.txt''w'asf:                                             - 72  f.write("Testing with statement.")                                       - 73   - 74  @my_decorator - 75  defsay_hello():                                                             - 76  print("Hello!")                                                          - 77   - 78  say_hello()                                                                  - 79   + + + +  1  importmath +  2  fromosimportpath +  3   +  4  # I'm a comment :) +  5   +  6  string_var = "Hello, world!" +  7  int_var = 42 +  8  float_var = 3.14 +  9  complex_var = 1 + 2j + 10   + 11  list_var = [12345]                                                   + 12  tuple_var = (12345)                                                  + 13  set_var = {12345}                                                    + 14  dict_var = {"a"1"b"2"c"3}                                          + 15   + 16  deffunction_no_args():                                                      + 17  return"No arguments" + 18   + 19  deffunction_with_args(ab):                                                + 20  returna + b + 21   + 22  deffunction_with_default_args(a=0b=0):                                    + 23  returna * b + 24   + 25  lambda_func = lambdaxx**2 + 26   + 27  ifint_var == 42:                                                            + 28  print("It's the answer!")                                                + 29  elifint_var < 42:                                                           + 30  print("Less than the answer.")                                           + 31  else:                                                                        + 32  print("Greater than the answer.")                                        + 33   + 34  forindexvalue in enumerate(list_var):                                     + 35  print(f"Index: {index}, Value: {value}")                                 + 36   + 37  counter = 0 + 38  whilecounter < 5:                                                           + 39  print(f"Counter value: {counter}")                                       + 40  counter += 1 + 41   + 42  squared_numbers = [x**2forx in range(10ifx % 2 == 0]                    + 43   + 44  try:                                                                         + 45  result = 10 / 0 + 46  exceptZeroDivisionError:                                                    + 47  print("Cannot divide by zero!")                                          + 48  finally:                                                                     + 49  print("End of try-except block.")                                        + 50   + 51  classAnimal:                                                                + 52  def__init__(selfname):                                                + 53  self.name = name + 54   + 55  defspeak(self):                                                         + 56  raiseNotImplementedError("Subclasses must implement this method." + 57   + 58  classDog(Animal):                                                           + 59  defspeak(self):                                                         + 60  returnf"{self.name} says Woof!" + 61   + 62  deffibonacci(n):                                                            + 63  ab = 01 + 64  for_ in range(n):                                                       + 65  yielda + 66  ab = ba + b + 67   + 68  fornum in fibonacci(5):                                                     + 69  print(num)                                                               + 70   + 71  withopen('test.txt''w'asf:                                             + 72  f.write("Testing with statement.")                                       + 73   + 74  @my_decorator + 75  defsay_hello():                                                             + 76  print("Hello!")                                                          + 77   + 78  say_hello()                                                                  + 79   @@ -29477,150 +29477,151 @@ font-weight: 700; } - .terminal-988519589-matrix { + .terminal-2921211909-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-988519589-title { + .terminal-2921211909-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-988519589-r1 { fill: #e4e5e6 } - .terminal-988519589-r2 { fill: #1b1b1b } - .terminal-988519589-r3 { fill: #75715e } - .terminal-988519589-r4 { fill: #c5c8c6 } - .terminal-988519589-r5 { fill: #7b7e82 } - .terminal-988519589-r6 { fill: #e2e3e3 } - .terminal-988519589-r7 { fill: #e6db74 } - .terminal-988519589-r8 { fill: #ae81ff } - .terminal-988519589-r9 { fill: #66d9ef;font-style: italic; } + .terminal-2921211909-r1 { fill: #e4e5e6 } + .terminal-2921211909-r2 { fill: #1b1b1b } + .terminal-2921211909-r3 { fill: #75715e } + .terminal-2921211909-r4 { fill: #c5c8c6 } + .terminal-2921211909-r5 { fill: #7b7e82 } + .terminal-2921211909-r6 { fill: #e2e3e3 } + .terminal-2921211909-r7 { fill: #f92672 } + .terminal-2921211909-r8 { fill: #e6db74 } + .terminal-2921211909-r9 { fill: #ae81ff } + .terminal-2921211909-r10 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  # This is a comment in TOML -  2   -  3  string = "Hello, world!" -  4  integer = 42 -  5  float = 3.14                            -  6  boolean = true -  7  datetime = 1979-05-27T07:32:00Z         -  8   -  9  fruits = ["apple""banana""cherry" - 10   - 11  [address]                               - 12  street = "123 Main St" - 13  city = "Anytown" - 14  state = "CA" - 15  zip = "12345" - 16   - 17  [person.john]                           - 18  name = "John Doe" - 19  age = 28 - 20  is_student = false - 21   - 22   - 23  [[animals]]                             - 24  name = "Fido" - 25  type = "dog" - 26   + + + +  1  # This is a comment in TOML +  2   +  3  string = "Hello, world!" +  4  integer = 42 +  5  float = 3.14 +  6  boolean = true +  7  datetime = 1979-05-27T07:32:00Z         +  8   +  9  fruits = ["apple""banana""cherry" + 10   + 11  [address]                               + 12  street = "123 Main St" + 13  city = "Anytown" + 14  state = "CA" + 15  zip = "12345" + 16   + 17  [person.john]                           + 18  name = "John Doe" + 19  age = 28 + 20  is_student = false + 21   + 22   + 23  [[animals]]                             + 24  name = "Fido" + 25  type = "dog" + 26   From 6ace22bb972fa97a5f4c2588334369522bcf4d39 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 16:43:19 +0100 Subject: [PATCH 197/366] Syntax highlighting datetimes in TOML --- src/textual/document/_syntax_theme.py | 1 + tree-sitter/highlights/toml.scm | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index 65b8f69bdc..3a480a5070 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -39,6 +39,7 @@ "yaml.field": Style(color="#F92672", bold=True), "json.label": Style(color="#F92672", bold=True), "toml.type": Style(color="#F92672"), + "toml.datetime": Style(color="#AE81FF"), } _BUILTIN_THEMES = { diff --git a/tree-sitter/highlights/toml.scm b/tree-sitter/highlights/toml.scm index edb609f43d..bfab979453 100644 --- a/tree-sitter/highlights/toml.scm +++ b/tree-sitter/highlights/toml.scm @@ -13,10 +13,10 @@ (string) @string (integer) @number (float) @float -(offset_date_time) @string.special -(local_date_time) @string.special -(local_date) @string.special -(local_time) @string.special +(offset_date_time) @toml.datetime +(local_date_time) @toml.datetime +(local_date) @toml.datetime +(local_time) @toml.datetime ; Punctuation ;------------ From 1a73cd787e5a68bb9c2573f735e77ad89d7f39df Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 15 Aug 2023 16:49:36 +0100 Subject: [PATCH 198/366] Namespace TOML errors in highlighting --- src/textual/document/_syntax_theme.py | 1 + tree-sitter/highlights/toml.scm | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index 3a480a5070..1f79c75692 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -40,6 +40,7 @@ "json.label": Style(color="#F92672", bold=True), "toml.type": Style(color="#F92672"), "toml.datetime": Style(color="#AE81FF"), + "toml.error": None, } _BUILTIN_THEMES = { diff --git a/tree-sitter/highlights/toml.scm b/tree-sitter/highlights/toml.scm index bfab979453..21e0d5b95b 100644 --- a/tree-sitter/highlights/toml.scm +++ b/tree-sitter/highlights/toml.scm @@ -33,4 +33,4 @@ "{" @punctuation.bracket "}" @punctuation.bracket -(ERROR) @error +(ERROR) @toml.error From 435268328a026ff96a11f39cf33da400c80285c7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 16 Aug 2023 10:58:21 +0100 Subject: [PATCH 199/366] Add a move_cursor_relative method --- src/textual/widgets/_text_area.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index b0a6c71d31..b38ace8b08 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -678,7 +678,7 @@ def move_cursor( center: bool = False, record_width: bool = True, ) -> None: - """Move the cursor to a location, optionally selecting text on the way. + """Move the cursor to a location. Args: location: The location to move the cursor to. @@ -687,11 +687,11 @@ def move_cursor( record_width: If True, record the cursor column cell width so that we jump to a corresponding location when moving between rows. """ - if not select: - self.selection = Selection.cursor(location) - else: + if select: start, end = self.selection self.selection = Selection(start, location) + else: + self.selection = Selection.cursor(location) if record_width: self.record_cursor_offset() @@ -699,6 +699,29 @@ def move_cursor( if center: self.scroll_cursor_visible(center) + def move_cursor_relative( + self, + rows: int = 0, + columns: int = 0, + select: bool = False, + center: bool = False, + record_width: bool = True, + ): + """Move the cursor relative to its current location. + + Args: + location: The location to move the cursor to. + select: If True, select text between the old and new location. + center: If True, scroll such that the cursor is centered. + record_width: If True, record the cursor column cell width so that we + jump to a corresponding location when moving between rows. + """ + clamp_visitable = self.clamp_visitable + start, end = self.selection + current_row, current_column = end + target = clamp_visitable(Location(current_row + rows, current_column + columns)) + self.move_cursor(target, select, center, record_width) + @property def cursor_location(self) -> Location: """The current location of the cursor in the document. From 138c7d34bc3edb1ba61805e208f2a4a30d3fef92 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 16 Aug 2023 11:34:35 +0100 Subject: [PATCH 200/366] Update TOML TextArea snapshot for datetime highlighting support --- .../__snapshots__/test_snapshots.ambr | 138 +++++++++--------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 219f88317c..73381b46f1 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -29477,151 +29477,151 @@ font-weight: 700; } - .terminal-2921211909-matrix { + .terminal-618639109-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2921211909-title { + .terminal-618639109-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2921211909-r1 { fill: #e4e5e6 } - .terminal-2921211909-r2 { fill: #1b1b1b } - .terminal-2921211909-r3 { fill: #75715e } - .terminal-2921211909-r4 { fill: #c5c8c6 } - .terminal-2921211909-r5 { fill: #7b7e82 } - .terminal-2921211909-r6 { fill: #e2e3e3 } - .terminal-2921211909-r7 { fill: #f92672 } - .terminal-2921211909-r8 { fill: #e6db74 } - .terminal-2921211909-r9 { fill: #ae81ff } - .terminal-2921211909-r10 { fill: #66d9ef;font-style: italic; } + .terminal-618639109-r1 { fill: #e4e5e6 } + .terminal-618639109-r2 { fill: #1b1b1b } + .terminal-618639109-r3 { fill: #75715e } + .terminal-618639109-r4 { fill: #c5c8c6 } + .terminal-618639109-r5 { fill: #7b7e82 } + .terminal-618639109-r6 { fill: #e2e3e3 } + .terminal-618639109-r7 { fill: #f92672 } + .terminal-618639109-r8 { fill: #e6db74 } + .terminal-618639109-r9 { fill: #ae81ff } + .terminal-618639109-r10 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  # This is a comment in TOML -  2   -  3  string = "Hello, world!" -  4  integer = 42 -  5  float = 3.14 -  6  boolean = true -  7  datetime = 1979-05-27T07:32:00Z         -  8   -  9  fruits = ["apple""banana""cherry" - 10   - 11  [address]                               - 12  street = "123 Main St" - 13  city = "Anytown" - 14  state = "CA" - 15  zip = "12345" - 16   - 17  [person.john]                           - 18  name = "John Doe" - 19  age = 28 - 20  is_student = false - 21   - 22   - 23  [[animals]]                             - 24  name = "Fido" - 25  type = "dog" - 26   + + + +  1  # This is a comment in TOML +  2   +  3  string = "Hello, world!" +  4  integer = 42 +  5  float = 3.14 +  6  boolean = true +  7  datetime = 1979-05-27T07:32:00Z +  8   +  9  fruits = ["apple""banana""cherry" + 10   + 11  [address]                               + 12  street = "123 Main St" + 13  city = "Anytown" + 14  state = "CA" + 15  zip = "12345" + 16   + 17  [person.john]                           + 18  name = "John Doe" + 19  age = 28 + 20  is_student = false + 21   + 22   + 23  [[animals]]                             + 24  name = "Fido" + 25  type = "dog" + 26   From 7dd304cf1629a4504b9a799cab14fc01bfbe9dec Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 16 Aug 2023 15:31:04 +0100 Subject: [PATCH 201/366] Adjusting selections --- src/textual/_fix_direction.py | 2 +- src/textual/document/_document.py | 10 ++-- .../document/_syntax_aware_document.py | 6 +-- src/textual/widgets/_text_area.py | 53 +++++++++++++++---- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/textual/_fix_direction.py b/src/textual/_fix_direction.py index 0c2da806f6..af1609a20e 100644 --- a/src/textual/_fix_direction.py +++ b/src/textual/_fix_direction.py @@ -1,7 +1,7 @@ from __future__ import annotations -def _fix_direction( +def _sort_ascending( start: tuple[int, int], end: tuple[int, int] ) -> tuple[tuple[int, int], tuple[int, int]]: """Given a range, return a new range (x, y) such diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index deecf27ce0..8b7721a9e9 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -7,7 +7,7 @@ from rich.text import Text from textual._cells import cell_len -from textual._fix_direction import _fix_direction +from textual._fix_direction import _sort_ascending from textual._types import Literal, SupportsIndex, get_args from textual.geometry import Size @@ -194,7 +194,7 @@ def insert_range(self, start: Location, end: Location, text: str) -> Location: if not text: return end - top, bottom = _fix_direction(start, end) + top, bottom = _sort_ascending(start, end) top_row, top_column = top bottom_row, bottom_column = bottom @@ -228,7 +228,7 @@ def delete_range(self, start: Location, end: Location) -> str: Returns: The text that was deleted from the document. """ - top, bottom = _fix_direction(start, end) + top, bottom = _sort_ascending(start, end) top_row, top_column = top bottom_row, bottom_column = bottom @@ -262,7 +262,7 @@ def get_text_range(self, start: Location, end: Location) -> str: Returns: The text between start (inclusive) and end (exclusive). """ - top, bottom = _fix_direction(start, end) + top, bottom = _sort_ascending(start, end) top_row, top_column = top bottom_row, bottom_column = bottom lines = self._lines @@ -357,4 +357,4 @@ def range(self) -> tuple[Location, Location]: """Return the Selection as a "standard" range, from top to bottom i.e. (minimum point, maximum point) where the minimum point is inclusive and the maximum point is exclusive.""" start, end = self - return _fix_direction(start, end) + return _sort_ascending(start, end) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 0fc6b7c64f..016104dd73 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -19,7 +19,7 @@ except ImportError: TREE_SITTER = False -from textual._fix_direction import _fix_direction +from textual._fix_direction import _sort_ascending from textual.document._document import Document, Location, _utf8_encode from textual.document._languages import VALID_LANGUAGES from textual.document._syntax_theme import SyntaxTheme @@ -105,7 +105,7 @@ def insert_range( Returns: The new end location after the edit is complete. """ - top, bottom = _fix_direction(start, end) + top, bottom = _sort_ascending(start, end) # An optimisation would be finding the byte offsets as a single operation rather # than doing two passes over the document content. @@ -148,7 +148,7 @@ def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: A string containing the deleted text. """ - top, bottom = _fix_direction(start, end) + top, bottom = _sort_ascending(start, end) start_point = self._location_to_point(top) old_end_point = self._location_to_point(bottom) start_byte = self._location_to_byte_offset(top) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index b38ace8b08..174a2747e5 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -12,7 +12,7 @@ from textual import events, log from textual._cells import cell_len -from textual._fix_direction import _fix_direction +from textual._fix_direction import _sort_ascending from textual._types import Literal, Protocol, runtime_checkable from textual.binding import Binding from textual.document import Document, Location, Selection, SyntaxTheme @@ -54,7 +54,7 @@ class Insert: """The end location of the insert""" move_cursor: bool = False """True if the cursor should move to the end of the inserted text.""" - _edit_end: Location | None = field(init=False, default=None) + _updated_selection: Location | None = field(init=False, default=None) """Computed location to move the cursor to if `move_cursor` is True.""" def do(self, text_area: TextArea) -> None: @@ -66,10 +66,45 @@ def do(self, text_area: TextArea) -> None: # TODO: For undo to work, we'll need to record the text that was replaced. # We can use TextArea.get_text_range to do this, or perform it inside # document.insert_range and return a compound object. - self._edit_end = text_area._document.insert_range( - self.from_location, - self.to_location, - self.text, + + # Get the offset between Selection.end and _edit_end (the new bottom). + # Apply these offsets to the Selection start and end + # -> we might need to adjust the offset in the case of overlap. + text = self.text + + edit_from = self.from_location + edit_to = self.to_location + + edit_top, edit_bottom = _sort_ascending(edit_from, edit_to) + edit_bottom_row, edit_bottom_column = edit_bottom + + selection_start, selection_end = text_area.selection + selection_start_row, selection_start_column = selection_start + selection_end_row, selection_end_column = selection_end + + new_edit_bottom = text_area._document.insert_range(edit_from, edit_to, text) + self._edit_end = new_edit_bottom + new_edit_to_row, new_edit_to_column = new_edit_bottom + + column_offset = new_edit_to_column - edit_bottom_column + target_selection_start_column = ( + selection_start_column + column_offset + if edit_bottom_row == selection_start_row + else selection_start_column + ) + target_selection_end_column = ( + selection_end_column + column_offset + if edit_bottom_row == selection_end_row + else selection_end_column + ) + + row_offset = new_edit_to_row - edit_bottom_row + target_selection_start_row = selection_start_row + row_offset + target_selection_end_row = selection_end_row + row_offset + + self._updated_selection = Selection( + start=(target_selection_start_row, target_selection_start_column), + end=(target_selection_end_row, target_selection_end_column), ) def undo(self, text_area: TextArea) -> None: @@ -88,7 +123,7 @@ def post_edit(self, text_area: TextArea) -> None: if self.move_cursor: text_area.move_cursor(self._edit_end) else: - text_area.refresh() + text_area.selection = self._updated_selection @dataclass @@ -504,7 +539,7 @@ def get_text_range(self, start: Location, end: Location) -> str: Returns: The text between start and end. """ - start, end = _fix_direction(start, end) + start, end = _sort_ascending(start, end) return self._document.get_text_range(start, end) def edit(self, edit: Edit) -> Any: @@ -1024,7 +1059,7 @@ def delete_range( move_cursor: bool = False, ) -> str: """Delete text between from_location and to_location.""" - top, bottom = _fix_direction(from_location, to_location) + top, bottom = _sort_ascending(from_location, to_location) deleted_text = self.edit(Delete(top, bottom, move_cursor)) return deleted_text From e6c586e0f509103ada548f256d2041f2326bc785 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 16 Aug 2023 17:09:06 +0100 Subject: [PATCH 202/366] At TextArea widget level, delete_range is insert_range of empty string --- src/textual/document/_document.py | 13 ++++---- src/textual/widgets/_text_area.py | 50 +++++++++++++++------------- tests/text_area/test_edit_via_api.py | 22 ++++++++---- 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 8b7721a9e9..30b155338c 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -191,9 +191,6 @@ def insert_range(self, start: Location, end: Location, text: str) -> Location: Returns: The new end location after the edit is complete. """ - if not text: - return end - top, bottom = _sort_ascending(start, end) top_row, top_column = top bottom_row, bottom_column = bottom @@ -208,9 +205,13 @@ def insert_range(self, start: Location, end: Location, text: str) -> Location: before_selection = lines[top_row][:top_column] after_selection = lines[bottom_row][bottom_column:] - insert_lines[0] = before_selection + insert_lines[0] - destination_column = len(insert_lines[-1]) - insert_lines[-1] = insert_lines[-1] + after_selection + if insert_lines: + insert_lines[0] = before_selection + insert_lines[0] + destination_column = len(insert_lines[-1]) + insert_lines[-1] = insert_lines[-1] + after_selection + else: + destination_column = len(before_selection) + insert_lines = [before_selection + after_selection] lines[top_row : bottom_row + 1] = insert_lines destination_row = top_row + len(insert_lines) - 1 diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 174a2747e5..539cf7944b 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -52,8 +52,9 @@ class Insert: """The start location of the insert.""" to_location: Location """The end location of the insert""" - move_cursor: bool = False - """True if the cursor should move to the end of the inserted text.""" + sticky_cursor: bool + """If True, cursor will stick to the character it's currently above, + even if an insert is made programmatically via the API.""" _updated_selection: Location | None = field(init=False, default=None) """Computed location to move the cursor to if `move_cursor` is True.""" @@ -120,10 +121,10 @@ def post_edit(self, text_area: TextArea) -> None: Args: text_area: The TextArea this operation was performed on. """ - if self.move_cursor: - text_area.move_cursor(self._edit_end) - else: + if self.sticky_cursor: text_area.selection = self._updated_selection + else: + text_area.move_cursor(self._edit_end) @dataclass @@ -144,9 +145,10 @@ class Delete: def do(self, text_area: TextArea) -> str: """Do the delete action and record the text that was deleted.""" - self._deleted_text = text_area._document.delete_range( - self.from_location, self.to_location - ) + start = self.from_location + end = self.to_location + self._deleted_text = text_area.get_text_range(start, end) + text_area._document.insert_range(start, end, "") return self._deleted_text def undo(self, text_area: TextArea) -> None: @@ -1037,30 +1039,30 @@ def insert_text( self, text: str, location: Location | None = None, - move_cursor: bool = False, + sticky_cursor: bool = True, ) -> None: if location is None: location = self.cursor_location - self.edit(Insert(text, location, location, move_cursor)) + self.edit(Insert(text, location, location, sticky_cursor)) def insert_text_range( self, text: str, from_location: Location, to_location: Location, - move_cursor: bool = False, + sticky_cursor: bool = True, ) -> None: - self.edit(Insert(text, from_location, to_location, move_cursor)) + self.edit(Insert(text, from_location, to_location, sticky_cursor)) def delete_range( self, from_location: Location, to_location: Location, - move_cursor: bool = False, + sticky_cursor: bool = True, ) -> str: """Delete text between from_location and to_location.""" top, bottom = _sort_ascending(from_location, to_location) - deleted_text = self.edit(Delete(top, bottom, move_cursor)) + deleted_text = self.edit(Delete(top, bottom, sticky_cursor)) return deleted_text def clear(self) -> None: @@ -1068,7 +1070,7 @@ def clear(self) -> None: document = self._document last_line = document[-1] document_end_location = (document.line_count, len(last_line)) - self.delete_range((0, 0), document_end_location, move_cursor=True) + self.delete_range((0, 0), document_end_location) def action_delete_left(self) -> None: """Deletes the character to the left of the cursor and updates the cursor location. @@ -1089,7 +1091,7 @@ def action_delete_left(self) -> None: else: end = (end_row, end_column - 1) - self.delete_range(start, end, move_cursor=True) + self.delete_range(start, end) def action_delete_right(self) -> None: """Deletes the character to the right of the cursor and keeps the cursor at the same location. @@ -1108,7 +1110,7 @@ def action_delete_right(self) -> None: else: end = (end_row, end_column + 1) - self.delete_range(start, end, move_cursor=True) + self.delete_range(start, end) def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" @@ -1119,21 +1121,21 @@ def action_delete_line(self) -> None: from_location = (start_row, 0) to_location = (end_row + 1, 0) - self.delete_range(from_location, to_location, move_cursor=True) + self.delete_range(from_location, to_location) def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor location to the start of the line.""" from_location = self.selection.end cursor_row, cursor_column = from_location to_location = (cursor_row, 0) - self.delete_range(from_location, to_location, move_cursor=True) + self.delete_range(from_location, to_location) def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor location to the end of the line.""" from_location = self.selection.end cursor_row, cursor_column = from_location to_location = (cursor_row, len(self._document[cursor_row])) - self.delete_range(from_location, to_location, move_cursor=True) + self.delete_range(from_location, to_location) def action_delete_word_left(self) -> None: """Deletes the word to the left of the cursor and updates the cursor location.""" @@ -1144,7 +1146,7 @@ def action_delete_word_left(self) -> None: # deletes the characters within the selection range, ignoring word boundaries. start, end = self.selection if start != end: - self.delete_range(start, end, move_cursor=True) + self.delete_range(start, end) cursor_row, cursor_column = end @@ -1162,7 +1164,7 @@ def action_delete_word_left(self) -> None: # If we're already on the first line and no word boundary is found, delete to the start of the line from_location = (cursor_row, 0) - self.delete_range(from_location, self.selection.end, move_cursor=True) + self.delete_range(from_location, self.selection.end) def action_delete_word_right(self) -> None: """Deletes the word to the right of the cursor and keeps the cursor at the same location.""" @@ -1171,7 +1173,7 @@ def action_delete_word_right(self) -> None: start, end = self.selection if start != end: - self.delete_range(start, end, move_cursor=True) + self.delete_range(start, end) cursor_row, cursor_column = end @@ -1189,4 +1191,4 @@ def action_delete_word_right(self) -> None: # If we're already on the last line and no word boundary is found, delete to the end of the line to_location = (cursor_row, len(self._document[cursor_row])) - self.delete_range(end, to_location, move_cursor=True) + self.delete_range(end, to_location) diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index d0afc0b174..4a2ac5a0b1 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -25,19 +25,26 @@ def compose(self) -> ComposeResult: async def test_insert_text_start(): + """The cursor is in the middle of the line, and we programmatically insert + some text at the start of the document -> the cursor location should shift + such that it stays above the same character.""" app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.insert_text("Hello") + text_area.move_cursor((0, 5)) + text_area.insert_text("Hello", location=(0, 0)) assert text_area.text == "Hello" + TEXT - assert text_area.cursor_location == (0, 0) + assert text_area.cursor_location == (0, 10) async def test_insert_text_start_move_cursor(): + """When move_cursor=True, the cursor will automatically jump to the end + location of the edit operation.""" app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.insert_text("Hello", move_cursor=True) + text_area.move_cursor((0, 5)) + text_area.insert_text("Hello", location=(0, 0), sticky_cursor=True) assert text_area.text == "Hello" + TEXT assert text_area.cursor_location == (0, 5) @@ -48,6 +55,7 @@ async def test_insert_newlines_start(): text_area = app.query_one(TextArea) text_area.insert_text("\n\n\n") assert text_area.text == "\n\n\n" + TEXT + assert text_area.selection == Selection.cursor((3, 0)) async def test_insert_newlines_end(): @@ -90,7 +98,7 @@ async def test_insert_text_non_cursor_location_move_cursor(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.insert_text("Hello", location=(4, 0), move_cursor=True) + text_area.insert_text("Hello", location=(4, 0), sticky_cursor=True) assert text_area.text == TEXT + "Hello" assert text_area.selection == Selection.cursor((4, 5)) @@ -117,7 +125,7 @@ async def test_insert_multiline_text_move_cursor(): async with app.run_test(): text_area = app.query_one(TextArea) text_area.move_cursor((2, 5)) - text_area.insert_text("Hello,\nworld!", move_cursor=True) + text_area.insert_text("Hello,\nworld!", sticky_cursor=True) expected_content = """\ I must not fear. Fear is the mind-killer. @@ -157,7 +165,7 @@ async def test_insert_range_multiline_text_move_cursor(): "Hello,\nworld!\n", from_location=(1, 0), to_location=(3, 0), - move_cursor=True, + sticky_cursor=True, ) expected_content = """\ I must not fear. @@ -191,7 +199,7 @@ async def test_delete_range_within_line_move_cursor(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - deleted_text = text_area.delete_range((0, 6), (0, 10), move_cursor=True) + deleted_text = text_area.delete_range((0, 6), (0, 10), sticky_cursor=True) assert deleted_text == " not" expected_text = """\ I must fear. From 58b595593b9453e8417d493a3a0d018088706ccf Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 16:51:45 +0100 Subject: [PATCH 203/366] Refactoring --- src/textual/document/__init__.py | 3 +- src/textual/document/_document.py | 80 ++++------ .../document/_syntax_aware_document.py | 50 +----- src/textual/widgets/_text_area.py | 144 +++++++----------- tests/document/test_document_delete.py | 65 +++++--- tests/document/test_document_insert.py | 52 +++---- tests/text_area/test_edit_via_api.py | 136 +++++++++++------ 7 files changed, 251 insertions(+), 279 deletions(-) diff --git a/src/textual/document/__init__.py b/src/textual/document/__init__.py index 3933b0b7b6..5e8346111b 100644 --- a/src/textual/document/__init__.py +++ b/src/textual/document/__init__.py @@ -1,4 +1,4 @@ -from ._document import Document, Location, Selection +from ._document import Document, EditResult, Location, Selection from ._languages import VALID_LANGUAGES from ._syntax_aware_document import ( EndColumn, @@ -15,6 +15,7 @@ "Highlight", "HighlightName", "Location", + "EditResult", "Selection", "StartColumn", "SyntaxAwareDocument", diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 30b155338c..8bbfbbbb76 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -1,6 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from dataclasses import dataclass from functools import lru_cache from typing import NamedTuple, Tuple @@ -17,6 +18,14 @@ """The set of valid line separator strings.""" +@dataclass +class EditResult: + end_location: Location + """The new end Location after the selection is complete.""" + replaced_text: str + """The text that was replaced.""" + + @lru_cache(maxsize=1024) def _utf8_encode(text: str) -> bytes: """Encode the input text as utf-8 bytes. @@ -56,8 +65,8 @@ class DocumentBase(ABC): provide in order to be used by the TextArea widget.""" @abstractmethod - def insert_range(self, start: Location, end: Location, text: str) -> Location: - """Insert text at the given range. + def replace_range(self, start: Location, end: Location, text: str) -> Location: + """Replace the text at the given range. Args: start: A tuple (row, column) where the edit starts. @@ -68,18 +77,6 @@ def insert_range(self, start: Location, end: Location, text: str) -> Location: The new end location after the edit is complete. """ - @abstractmethod - def delete_range(self, start: Location, end: Location) -> str: - """Delete the text at the given range. - - Args: - start: A tuple (row, column) where the edit starts. - end: A tuple (row, column) where the edit ends. - - Returns: - The text that was deleted from the document. - """ - @property @abstractmethod def text(self) -> str: @@ -180,8 +177,8 @@ def get_size(self, tab_width: int) -> Size: height = len(lines) return Size(max_cell_length, height) - def insert_range(self, start: Location, end: Location, text: str) -> Location: - """Insert text at the given range. + def replace_range(self, start: Location, end: Location, text: str) -> EditResult: + """Replace text at the given range. Args: start: A tuple (row, column) where the edit starts. @@ -189,7 +186,8 @@ def insert_range(self, start: Location, end: Location, text: str) -> Location: text: The text to insert between start and end. Returns: - The new end location after the edit is complete. + The EditResult containing information about the completed + replace operation. """ top, bottom = _sort_ascending(start, end) top_row, top_column = top @@ -202,8 +200,16 @@ def insert_range(self, start: Location, end: Location, text: str) -> Location: lines = self._lines - before_selection = lines[top_row][:top_column] - after_selection = lines[bottom_row][bottom_column:] + replaced_text = self.get_text_range(top, bottom) + if bottom_row >= len(lines): + after_selection = "" + else: + after_selection = lines[bottom_row][bottom_column:] + + if top_row >= len(lines): + before_selection = "" + else: + before_selection = lines[top_row][:top_column] if insert_lines: insert_lines[0] = before_selection + insert_lines[0] @@ -216,37 +222,8 @@ def insert_range(self, start: Location, end: Location, text: str) -> Location: lines[top_row : bottom_row + 1] = insert_lines destination_row = top_row + len(insert_lines) - 1 - end_point = destination_row, destination_column - return end_point - - def delete_range(self, start: Location, end: Location) -> str: - """Delete the text at the given range. - - Args: - start: A tuple (row, column) where the edit starts. - end: A tuple (row, column) where the edit ends. - - Returns: - The text that was deleted from the document. - """ - top, bottom = _sort_ascending(start, end) - top_row, top_column = top - bottom_row, bottom_column = bottom - - lines = self._lines - - deleted_text = self.get_text_range(top, bottom) - - if top_row == bottom_row: - line = lines[top_row] - lines[top_row] = line[:top_column] + line[bottom_column:] - else: - start_line = lines[top_row] - end_line = lines[bottom_row] if bottom_row <= self.line_count - 1 else "" - lines[top_row] = start_line[:top_column] + end_line[bottom_column:] - del lines[top_row + 1 : bottom_row + 1] - - return deleted_text + end_location = (destination_row, destination_column) + return EditResult(end_location, replaced_text) def get_text_range(self, start: Location, end: Location) -> str: """Get the text that falls between the start and end locations. @@ -263,6 +240,9 @@ def get_text_range(self, start: Location, end: Location) -> str: Returns: The text between start (inclusive) and end (exclusive). """ + if start == end: + return "" + top, bottom = _sort_ascending(start, end) top_row, top_column = top bottom_row, bottom_column = bottom diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 016104dd73..1099017e77 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -20,7 +20,7 @@ TREE_SITTER = False from textual._fix_direction import _sort_ascending -from textual.document._document import Document, Location, _utf8_encode +from textual.document._document import Document, EditResult, Location, _utf8_encode from textual.document._languages import VALID_LANGUAGES from textual.document._syntax_theme import SyntaxTheme @@ -92,9 +92,9 @@ def __init__( ) self._prepare_highlights() - def insert_range( + def replace_range( self, start: tuple[int, int], end: tuple[int, int], text: str - ) -> tuple[int, int]: + ) -> EditResult: """Insert text at the given range. Args: @@ -114,10 +114,11 @@ def insert_range( old_end_byte = self._location_to_byte_offset(bottom) old_end_point = self._location_to_point(bottom) - end_location = super().insert_range(start, end, text) + replace_result = super().replace_range(start, end, text) if TREE_SITTER: text_byte_length = len(_utf8_encode(text)) + end_location = replace_result.end_location self._syntax_tree.edit( start_byte=start_byte, old_end_byte=old_end_byte, @@ -131,46 +132,7 @@ def insert_range( ) self._prepare_highlights() - return end_location - - def delete_range(self, start: tuple[int, int], end: tuple[int, int]) -> str: - """Delete text between `start` and `end`. - - This will update the internal syntax tree of the document, refreshing - the syntax highlighting data. Calling `get_line` will now return a Text - object with new highlights corresponding to this change. - - Args: - start: The start of the range. - end: The end of the range. - - Returns: - A string containing the deleted text. - """ - - top, bottom = _sort_ascending(start, end) - start_point = self._location_to_point(top) - old_end_point = self._location_to_point(bottom) - start_byte = self._location_to_byte_offset(top) - old_end_byte = self._location_to_byte_offset(bottom) - - deleted_text = super().delete_range(start, end) - - if TREE_SITTER: - deleted_text_byte_length = len(_utf8_encode(deleted_text)) - self._syntax_tree.edit( - start_byte=start_byte, - old_end_byte=old_end_byte, - new_end_byte=old_end_byte - deleted_text_byte_length, - start_point=start_point, - old_end_point=old_end_point, - new_end_point=self._location_to_point(top), - ) - new_tree = self._parser.parse(self._read_callable, self._syntax_tree) - self._syntax_tree = new_tree - self._prepare_highlights() - - return deleted_text + return replace_result def get_line_text(self, line_index: int) -> Text: """Apply syntax highlights and return the Text of the line. diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 539cf7944b..ff15b66f02 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -15,7 +15,7 @@ from textual._fix_direction import _sort_ascending from textual._types import Literal, Protocol, runtime_checkable from textual.binding import Binding -from textual.document import Document, Location, Selection, SyntaxTheme +from textual.document import Document, EditResult, Location, Selection, SyntaxTheme from textual.events import MouseEvent from textual.geometry import Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive @@ -24,7 +24,7 @@ @runtime_checkable -class Edit(Protocol): +class Undoable(Protocol): """Protocol for actions performed in the text editor which can be done and undone. These are typically actions which affect the document (e.g. inserting and deleting @@ -43,34 +43,31 @@ def post_edit(self, text_area: TextArea) -> None: @dataclass -class Insert: - """Implements the Edit protocol for inserting text at some location.""" +class Edit: + """Implements the Undoable protocol to replace text at some range within a document.""" text: str - """The text to insert.""" + """The text to insert. An empty string is equivalent to deletion.""" from_location: Location """The start location of the insert.""" to_location: Location """The end location of the insert""" - sticky_cursor: bool - """If True, cursor will stick to the character it's currently above, - even if an insert is made programmatically via the API.""" + maintain_selection_offset: bool + """If True, the selection will maintain its offset to the replacement range.""" + _replace_result: EditResult | None = field(init=False, default=None) + """Contains data relating to the replace operation.""" _updated_selection: Location | None = field(init=False, default=None) - """Computed location to move the cursor to if `move_cursor` is True.""" + """Where the selection should move to after the replace happens.""" - def do(self, text_area: TextArea) -> None: + def do(self, text_area: TextArea) -> EditResult: """Perform the Insert operation. Args: text_area: The TextArea to perform the insert on. - """ - # TODO: For undo to work, we'll need to record the text that was replaced. - # We can use TextArea.get_text_range to do this, or perform it inside - # document.insert_range and return a compound object. - # Get the offset between Selection.end and _edit_end (the new bottom). - # Apply these offsets to the Selection start and end - # -> we might need to adjust the offset in the case of overlap. + Returns: + An EditResult containing information about the replace operation. + """ text = self.text edit_from = self.from_location @@ -83,9 +80,9 @@ def do(self, text_area: TextArea) -> None: selection_start_row, selection_start_column = selection_start selection_end_row, selection_end_column = selection_end - new_edit_bottom = text_area._document.insert_range(edit_from, edit_to, text) - self._edit_end = new_edit_bottom - new_edit_to_row, new_edit_to_column = new_edit_bottom + replace_result = text_area._document.replace_range(edit_from, edit_to, text) + self._replace_result = replace_result + new_edit_to_row, new_edit_to_column = replace_result.end_location column_offset = new_edit_to_column - edit_bottom_column target_selection_start_column = ( @@ -103,13 +100,18 @@ def do(self, text_area: TextArea) -> None: target_selection_start_row = selection_start_row + row_offset target_selection_end_row = selection_end_row + row_offset - self._updated_selection = Selection( - start=(target_selection_start_row, target_selection_start_column), - end=(target_selection_end_row, target_selection_end_column), - ) + if self.maintain_selection_offset: + self._updated_selection = Selection( + start=(target_selection_start_row, target_selection_start_column), + end=(target_selection_end_row, target_selection_end_column), + ) + else: + self._updated_selection = Selection.cursor(replace_result.end_location) + + return replace_result - def undo(self, text_area: TextArea) -> None: - """Undo the Insert operation. + def undo(self, text_area: TextArea) -> EditResult: + """Undo the Replace operation. Args: text_area: The TextArea to undo the insert operation on. @@ -121,44 +123,7 @@ def post_edit(self, text_area: TextArea) -> None: Args: text_area: The TextArea this operation was performed on. """ - if self.sticky_cursor: - text_area.selection = self._updated_selection - else: - text_area.move_cursor(self._edit_end) - - -@dataclass -class Delete: - """Performs a delete operation.""" - - from_location: Location - """The location to delete from (inclusive).""" - - to_location: Location - """The location to delete to (exclusive).""" - - move_cursor: bool = False - """Where to move the cursor to after the deletion.""" - - _deleted_text: str | None = field(init=False, default=None) - """The text that was deleted, or None if the deletion hasn't occurred yet.""" - - def do(self, text_area: TextArea) -> str: - """Do the delete action and record the text that was deleted.""" - start = self.from_location - end = self.to_location - self._deleted_text = text_area.get_text_range(start, end) - text_area._document.insert_range(start, end, "") - return self._deleted_text - - def undo(self, text_area: TextArea) -> None: - """Undo the delete action.""" - - def post_edit(self, text_area: TextArea) -> None: - if self.move_cursor: - text_area.move_cursor(self.from_location) - else: - text_area.refresh() + text_area.selection = self._updated_selection class TextArea(ScrollView, can_focus=True): @@ -344,7 +309,7 @@ def __init__( visually moves rather than logical characters) the user explicitly navigated to so that we can reset to it whenever possible.""" - self._undo_stack: list[Edit] = [] + self._undo_stack: list[Undoable] = [] """A stack (the end of the list is the top of the stack) for tracking edits.""" self._selecting = False @@ -544,7 +509,7 @@ def get_text_range(self, start: Location, end: Location) -> str: start, end = _sort_ascending(start, end) return self._document.get_text_range(start, end) - def edit(self, edit: Edit) -> Any: + def edit(self, edit: Undoable) -> Any: """Perform an Edit. Args: @@ -577,7 +542,7 @@ async def _on_key(self, event: events.Key) -> None: event.prevent_default() insert = insert_values.get(key, event.character) start, end = self.selection - self.insert_text_range(insert, start, end, True) + self.insert_text_range(insert, start, end, False) # def undo(self) -> None: # if self._undo_stack: @@ -1039,38 +1004,41 @@ def insert_text( self, text: str, location: Location | None = None, - sticky_cursor: bool = True, - ) -> None: + maintain_selection_offset: bool = True, + ) -> EditResult: if location is None: location = self.cursor_location - self.edit(Insert(text, location, location, sticky_cursor)) + return self.edit(Edit(text, location, location, maintain_selection_offset)) def insert_text_range( self, text: str, from_location: Location, to_location: Location, - sticky_cursor: bool = True, - ) -> None: - self.edit(Insert(text, from_location, to_location, sticky_cursor)) + maintain_selection_offset: bool = True, + ) -> EditResult: + return self.edit( + Edit(text, from_location, to_location, maintain_selection_offset) + ) def delete_range( self, from_location: Location, to_location: Location, - sticky_cursor: bool = True, - ) -> str: + maintain_selection_offset: bool = True, + ) -> EditResult: """Delete text between from_location and to_location.""" top, bottom = _sort_ascending(from_location, to_location) - deleted_text = self.edit(Delete(top, bottom, sticky_cursor)) - return deleted_text + return self.edit(Edit("", top, bottom, maintain_selection_offset)) def clear(self) -> None: """Clear the document.""" document = self._document last_line = document[-1] document_end_location = (document.line_count, len(last_line)) - self.delete_range((0, 0), document_end_location) + self.delete_range( + (0, 0), document_end_location, maintain_selection_offset=False + ) def action_delete_left(self) -> None: """Deletes the character to the left of the cursor and updates the cursor location. @@ -1091,7 +1059,7 @@ def action_delete_left(self) -> None: else: end = (end_row, end_column - 1) - self.delete_range(start, end) + self.delete_range(start, end, maintain_selection_offset=False) def action_delete_right(self) -> None: """Deletes the character to the right of the cursor and keeps the cursor at the same location. @@ -1110,7 +1078,7 @@ def action_delete_right(self) -> None: else: end = (end_row, end_column + 1) - self.delete_range(start, end) + self.delete_range(start, end, maintain_selection_offset=False) def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" @@ -1121,21 +1089,21 @@ def action_delete_line(self) -> None: from_location = (start_row, 0) to_location = (end_row + 1, 0) - self.delete_range(from_location, to_location) + self.delete_range(from_location, to_location, maintain_selection_offset=False) def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor location to the start of the line.""" from_location = self.selection.end cursor_row, cursor_column = from_location to_location = (cursor_row, 0) - self.delete_range(from_location, to_location) + self.delete_range(from_location, to_location, maintain_selection_offset=False) def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor location to the end of the line.""" from_location = self.selection.end cursor_row, cursor_column = from_location to_location = (cursor_row, len(self._document[cursor_row])) - self.delete_range(from_location, to_location) + self.delete_range(from_location, to_location, maintain_selection_offset=False) def action_delete_word_left(self) -> None: """Deletes the word to the left of the cursor and updates the cursor location.""" @@ -1146,7 +1114,7 @@ def action_delete_word_left(self) -> None: # deletes the characters within the selection range, ignoring word boundaries. start, end = self.selection if start != end: - self.delete_range(start, end) + self.delete_range(start, end, maintain_selection_offset=False) cursor_row, cursor_column = end @@ -1164,7 +1132,9 @@ def action_delete_word_left(self) -> None: # If we're already on the first line and no word boundary is found, delete to the start of the line from_location = (cursor_row, 0) - self.delete_range(from_location, self.selection.end) + self.delete_range( + from_location, self.selection.end, maintain_selection_offset=False + ) def action_delete_word_right(self) -> None: """Deletes the word to the right of the cursor and keeps the cursor at the same location.""" @@ -1173,7 +1143,7 @@ def action_delete_word_right(self) -> None: start, end = self.selection if start != end: - self.delete_range(start, end) + self.delete_range(start, end, maintain_selection_offset=False) cursor_row, cursor_column = end @@ -1191,4 +1161,4 @@ def action_delete_word_right(self) -> None: # If we're already on the last line and no word boundary is found, delete to the end of the line to_location = (cursor_row, len(self._document[cursor_row])) - self.delete_range(end, to_location) + self.delete_range(end, to_location, maintain_selection_offset=False) diff --git a/tests/document/test_document_delete.py b/tests/document/test_document_delete.py index 9bd797c6c8..3007ac5df3 100644 --- a/tests/document/test_document_delete.py +++ b/tests/document/test_document_delete.py @@ -1,6 +1,6 @@ import pytest -from textual.document import Document +from textual.document import Document, EditResult TEXT = """I must not fear. Fear is the mind-killer. @@ -15,8 +15,8 @@ def document(): def test_delete_single_character(document): - deleted_text = document.delete_range((0, 0), (0, 1)) - assert deleted_text == "I" + replace_result = document.replace_range((0, 0), (0, 1), "") + assert replace_result == EditResult(end_location=(0, 0), replaced_text="I") assert document.lines == [ " must not fear.", "Fear is the mind-killer.", @@ -27,8 +27,8 @@ def test_delete_single_character(document): def test_delete_single_newline(document): """Testing deleting newline from right to left""" - deleted_text = document.delete_range((1, 0), (0, 16)) - assert deleted_text == "\n" + replace_result = document.replace_range((1, 0), (0, 16), "") + assert replace_result == EditResult(end_location=(0, 16), replaced_text="\n") assert document.lines == [ "I must not fear.Fear is the mind-killer.", "I forgot the rest of the quote.", @@ -38,9 +38,12 @@ def test_delete_single_newline(document): def test_delete_near_end_of_document(document): """Test deleting a range near the end of a document.""" - deleted_text = document.delete_range((1, 0), (3, 11)) - assert deleted_text == ( - "Fear is the mind-killer.\n" "I forgot the rest of the quote.\n" "Sorry Will." + replace_result = document.replace_range((1, 0), (3, 11), "") + assert replace_result == EditResult( + end_location=(1, 0), + replaced_text="Fear is the mind-killer.\n" + "I forgot the rest of the quote.\n" + "Sorry Will.", ) assert document.lines == [ "I must not fear.", @@ -49,14 +52,20 @@ def test_delete_near_end_of_document(document): def test_delete_clearing_the_document(document): - deleted_text = document.delete_range((0, 0), (4, 0)) - assert deleted_text == TEXT + replace_result = document.replace_range((0, 0), (4, 0), "") + assert replace_result == EditResult( + end_location=(0, 0), + replaced_text=TEXT, + ) assert document.lines == [""] def test_delete_multiple_characters_on_one_line(document): - deleted_text = document.delete_range((0, 2), (0, 7)) - assert deleted_text == "must " + replace_result = document.replace_range((0, 2), (0, 7), "") + assert replace_result == EditResult( + end_location=(0, 2), + replaced_text="must ", + ) assert document.lines == [ "I not fear.", "Fear is the mind-killer.", @@ -67,8 +76,11 @@ def test_delete_multiple_characters_on_one_line(document): def test_delete_multiple_lines_partially_spanned(document): """Deleting a selection that partially spans the first and final lines of the selection.""" - deleted_text = document.delete_range((0, 2), (2, 2)) - assert deleted_text == "must not fear.\nFear is the mind-killer.\nI " + replace_result = document.replace_range((0, 2), (2, 2), "") + assert replace_result == EditResult( + end_location=(0, 2), + replaced_text="must not fear.\nFear is the mind-killer.\nI ", + ) assert document.lines == [ "I forgot the rest of the quote.", "Sorry Will.", @@ -77,8 +89,11 @@ def test_delete_multiple_lines_partially_spanned(document): def test_delete_end_of_line(document): """Testing deleting newline from left to right""" - deleted_text = document.delete_range((0, 16), (1, 0)) - assert deleted_text == "\n" + replace_result = document.replace_range((0, 16), (1, 0), "") + assert replace_result == EditResult( + end_location=(0, 16), + replaced_text="\n", + ) assert document.lines == [ "I must not fear.Fear is the mind-killer.", "I forgot the rest of the quote.", @@ -88,8 +103,11 @@ def test_delete_end_of_line(document): def test_delete_single_line_excluding_newline(document): """Delete from the start to the end of the line.""" - deleted_text = document.delete_range((2, 0), (2, 31)) - assert deleted_text == "I forgot the rest of the quote." + replace_result = document.replace_range((2, 0), (2, 31), "") + assert replace_result == EditResult( + end_location=(2, 0), + replaced_text="I forgot the rest of the quote.", + ) assert document.lines == [ "I must not fear.", "Fear is the mind-killer.", @@ -100,8 +118,11 @@ def test_delete_single_line_excluding_newline(document): def test_delete_single_line_including_newline(document): """Delete from the start of a line to the start of the line below.""" - deleted_text = document.delete_range((2, 0), (3, 0)) - assert deleted_text == "I forgot the rest of the quote.\n" + replace_result = document.replace_range((2, 0), (3, 0), "") + assert replace_result == EditResult( + end_location=(2, 0), + replaced_text="I forgot the rest of the quote.\n", + ) assert document.lines == [ "I must not fear.", "Fear is the mind-killer.", @@ -117,8 +138,8 @@ def test_delete_single_line_including_newline(document): def test_delete_end_of_file_newline(): document = Document(TEXT_NEWLINE_EOF) - deleted_text = document.delete_range((2, 0), (1, 24)) - assert deleted_text == "\n" + replace_result = document.replace_range((2, 0), (1, 24), "") + assert replace_result == EditResult(end_location=(1, 24), replaced_text="\n") assert document.lines == [ "I must not fear.", "Fear is the mind-killer.", diff --git a/tests/document/test_document_insert.py b/tests/document/test_document_insert.py index 39b8d77529..713d811a23 100644 --- a/tests/document/test_document_insert.py +++ b/tests/document/test_document_insert.py @@ -1,6 +1,6 @@ import pytest -from textual.document._document import Document +from textual.document import Document TEXT = """I must not fear. Fear is the mind-killer.""" @@ -8,8 +8,8 @@ def test_insert_no_newlines(): document = Document(TEXT) - document.insert_range((0, 1), (0, 1), " really") - assert document._lines == [ + document.replace_range((0, 1), (0, 1), " really") + assert document.lines == [ "I really must not fear.", "Fear is the mind-killer.", ] @@ -17,8 +17,8 @@ def test_insert_no_newlines(): def test_insert_empty_string(): document = Document(TEXT) - document.insert_range((0, 1), (0, 1), "") - assert document._lines == ["I must not fear.", "Fear is the mind-killer."] + document.replace_range((0, 1), (0, 1), "") + assert document.lines == ["I must not fear.", "Fear is the mind-killer."] @pytest.mark.xfail(reason="undecided on behaviour") @@ -26,40 +26,38 @@ def test_insert_invalid_column(): # TODO - what is the correct behaviour here? # right now it appends to the end of the line if the column is too large. document = Document(TEXT) - document.insert_range((0, 999), (0, 999), " really") - assert document._lines == ["I must not fear.", "Fear is the mind-killer."] + document.replace_range((0, 999), (0, 999), " really") + assert document.lines == ["I must not fear.", "Fear is the mind-killer."] -@pytest.mark.xfail(reason="undecided on behaviour") -def test_insert_invalid_row(): - # TODO - this raises an IndexError for list index out of range +def test_insert_invalid_row_and_column(): document = Document(TEXT) - document.insert_range((999, 0), (999, 0), " really") - assert document._lines == ["I must not fear.", "Fear is the mind-killer."] + document.replace_range((999, 0), (999, 0), " really") + assert document.lines == ["I must not fear.", "Fear is the mind-killer.", " really"] def test_insert_range_newline_file_start(): document = Document(TEXT) - document.insert_range((0, 0), (0, 0), "\n") - assert document._lines == ["", "I must not fear.", "Fear is the mind-killer."] + document.replace_range((0, 0), (0, 0), "\n") + assert document.lines == ["", "I must not fear.", "Fear is the mind-killer."] def test_insert_newline_splits_line(): document = Document(TEXT) - document.insert_range((0, 1), (0, 1), "\n") - assert document._lines == ["I", " must not fear.", "Fear is the mind-killer."] + document.replace_range((0, 1), (0, 1), "\n") + assert document.lines == ["I", " must not fear.", "Fear is the mind-killer."] def test_insert_newline_splits_line_selection(): document = Document(TEXT) - document.insert_range((0, 1), (0, 6), "\n") - assert document._lines == ["I", " not fear.", "Fear is the mind-killer."] + document.replace_range((0, 1), (0, 6), "\n") + assert document.lines == ["I", " not fear.", "Fear is the mind-killer."] def test_insert_multiple_lines_ends_with_newline(): document = Document(TEXT) - document.insert_range((0, 1), (0, 1), "Hello,\nworld!\n") - assert document._lines == [ + document.replace_range((0, 1), (0, 1), "Hello,\nworld!\n") + assert document.lines == [ "IHello,", "world!", " must not fear.", @@ -69,8 +67,8 @@ def test_insert_multiple_lines_ends_with_newline(): def test_insert_multiple_lines_ends_with_no_newline(): document = Document(TEXT) - document.insert_range((0, 1), (0, 1), "Hello,\nworld!") - assert document._lines == [ + document.replace_range((0, 1), (0, 1), "Hello,\nworld!") + assert document.lines == [ "IHello,", "world! must not fear.", "Fear is the mind-killer.", @@ -79,8 +77,8 @@ def test_insert_multiple_lines_ends_with_no_newline(): def test_insert_multiple_lines_starts_with_newline(): document = Document(TEXT) - document.insert_range((0, 1), (0, 1), "\nHello,\nworld!\n") - assert document._lines == [ + document.replace_range((0, 1), (0, 1), "\nHello,\nworld!\n") + assert document.lines == [ "I", "Hello,", "world!", @@ -92,8 +90,8 @@ def test_insert_multiple_lines_starts_with_newline(): def test_insert_range_text_no_newlines(): """Ensuring we can do a simple replacement of text.""" document = Document(TEXT) - document.insert_range((0, 2), (0, 6), "MUST") - assert document._lines == [ + document.replace_range((0, 2), (0, 6), "MUST") + assert document.lines == [ "I MUST not fear.", "Fear is the mind-killer.", ] @@ -107,7 +105,7 @@ def test_insert_range_text_no_newlines(): def test_newline_eof(): document = Document(TEXT_NEWLINE_EOF) - assert document._lines == [ + assert document.lines == [ "I must not fear.", "Fear is the mind-killer.", "", diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index 4a2ac5a0b1..538a436f4f 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -7,10 +7,11 @@ """ from textual.app import App, ComposeResult -from textual.document import Selection +from textual.document import EditResult, Selection from textual.widgets import TextArea -TEXT = """I must not fear. +TEXT = """\ +I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. @@ -24,29 +25,28 @@ def compose(self) -> ComposeResult: yield text_area -async def test_insert_text_start(): - """The cursor is in the middle of the line, and we programmatically insert - some text at the start of the document -> the cursor location should shift - such that it stays above the same character.""" +async def test_insert_text_start_maintain_selection_offset(): + """Ensure that we can maintain the offset between the location + an insert happens and the location of the selection.""" app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) text_area.move_cursor((0, 5)) text_area.insert_text("Hello", location=(0, 0)) assert text_area.text == "Hello" + TEXT - assert text_area.cursor_location == (0, 10) + assert text_area.selection == Selection.cursor((0, 10)) -async def test_insert_text_start_move_cursor(): - """When move_cursor=True, the cursor will automatically jump to the end - location of the edit operation.""" +async def test_insert_text_start(): + """If we don't maintain the selection offset, the cursor jumps + to the end of the edit and the selection is empty.""" app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) text_area.move_cursor((0, 5)) - text_area.insert_text("Hello", location=(0, 0), sticky_cursor=True) + text_area.insert_text("Hello", location=(0, 0), maintain_selection_offset=False) assert text_area.text == "Hello" + TEXT - assert text_area.cursor_location == (0, 5) + assert text_area.selection == Selection.cursor((0, 5)) async def test_insert_newlines_start(): @@ -94,11 +94,11 @@ async def test_insert_text_non_cursor_location(): assert text_area.selection == Selection.cursor((0, 0)) -async def test_insert_text_non_cursor_location_move_cursor(): +async def test_insert_text_non_cursor_location_dont_maintain_offset(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.insert_text("Hello", location=(4, 0), sticky_cursor=True) + text_area.insert_text("Hello", location=(4, 0), maintain_selection_offset=False) assert text_area.text == TEXT + "Hello" assert text_area.selection == Selection.cursor((4, 5)) @@ -108,7 +108,7 @@ async def test_insert_multiline_text(): async with app.run_test(): text_area = app.query_one(TextArea) text_area.move_cursor((2, 5)) - text_area.insert_text("Hello,\nworld!") + text_area.insert_text("Hello,\nworld!", maintain_selection_offset=False) expected_content = """\ I must not fear. Fear is the mind-killer. @@ -116,16 +116,16 @@ async def test_insert_multiline_text(): world!is the little-death that brings total obliteration. I will face my fear. """ - assert text_area.cursor_location == (2, 5) # Cursor didn't move + assert text_area.cursor_location == (3, 6) # Cursor moved to end of insert assert text_area.text == expected_content -async def test_insert_multiline_text_move_cursor(): +async def test_insert_multiline_text_maintain_offset(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) text_area.move_cursor((2, 5)) - text_area.insert_text("Hello,\nworld!", sticky_cursor=True) + text_area.insert_text("Hello,\nworld!") expected_content = """\ I must not fear. Fear is the mind-killer. @@ -133,7 +133,10 @@ async def test_insert_multiline_text_move_cursor(): world!is the little-death that brings total obliteration. I will face my fear. """ - assert text_area.cursor_location == (3, 6) # Cursor moved to end of insert + # The insert happens at the cursor (default location) + # Offset is maintained - we inserted 1 line so cursor shifts + # down 1 line, and along by the length of the last insert line. + assert text_area.cursor_location == (3, 6) assert text_area.text == expected_content @@ -156,26 +159,37 @@ async def test_insert_range_multiline_text(): assert text_area.text == expected_content -async def test_insert_range_multiline_text_move_cursor(): +async def test_insert_range_multiline_text_maintain_selection(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - # replace "Fear is the mind-killer\nFear is the little death..." + + # To begin with, the user selects the word "face" + text_area.selection = Selection((3, 7), (3, 11)) + assert text_area.selected_text == "face" + + # Text is inserted via the API in a way that shifts + # the start and end locations of the word "face" in + # both the horizontal and vertical directions. text_area.insert_text_range( - "Hello,\nworld!\n", + "Hello,\nworld!\n123\n456", from_location=(1, 0), to_location=(3, 0), - sticky_cursor=True, ) expected_content = """\ I must not fear. Hello, world! -I will face my fear. +123 +456I will face my fear. """ - assert text_area.selection == Selection.cursor( - (3, 0) - ) # cursor moves to end of insert + # Despite this insert, the selection locations are updated + # and the word face is still highlighted. This ensures that + # if text is insert programmatically, a user that is typing + # won't lose their place - the cursor will maintain the same + # relative position in the document as before. + assert text_area.selected_text == "face" + assert text_area.selection == Selection((4, 10), (4, 14)) assert text_area.text == expected_content @@ -183,52 +197,78 @@ async def test_delete_range_within_line(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - deleted_text = text_area.delete_range((0, 6), (0, 10)) - assert deleted_text == " not" + text_area.selection = Selection((0, 11), (0, 15)) + assert text_area.selected_text == "fear" + + # Delete some text before the selection location. + result = text_area.delete_range((0, 6), (0, 10)) + + # Even though the word has 'shifted' left, it's still selected. + assert text_area.selection == Selection((0, 7), (0, 11)) + assert text_area.selected_text == "fear" + + # We've recorded exactly what text was replaced in the EditResult + assert result == EditResult( + end_location=(0, 6), + replaced_text=" not", + ) + expected_text = """\ I must fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. """ - assert text_area.selection == Selection.cursor((0, 0)) # cursor didnt move assert text_area.text == expected_text -async def test_delete_range_within_line_move_cursor(): +async def test_delete_range_within_line_dont_maintain_offset(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - deleted_text = text_area.delete_range((0, 6), (0, 10), sticky_cursor=True) - assert deleted_text == " not" - expected_text = """\ + text_area.delete_range((0, 6), (0, 10), maintain_selection_offset=False) + expected_text = """\ I must fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. """ - assert text_area.selection == Selection.cursor((0, 6)) # cursor moved - assert text_area.text == expected_text + assert text_area.selection == Selection.cursor((0, 6)) # cursor moved + assert text_area.text == expected_text -async def test_delete_range_multiple_lines(): +async def test_delete_range_multiple_lines_selection_above(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - deleted_text = text_area.delete_range((1, 0), (3, 0)) - assert text_area.selection == Selection.cursor((0, 0)) + + # User has selected text on the first line... + text_area.selection = Selection((0, 2), (0, 6)) + assert text_area.selected_text == "must" + + # Some lines below are deleted... + result = text_area.delete_range((1, 0), (3, 0)) + + # The selection is not affected at all. + assert text_area.selection == Selection((0, 2), (0, 6)) + + # We've recorded the text that was deleted in the ReplaceResult. + # Lines of index 1 and 2 were deleted. Since the end + # location of the selection is (3, 0), the newline + # marker is included in the deletion. + expected_replaced_text = """\ +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +""" + assert result == EditResult( + end_location=(1, 0), + replaced_text=expected_replaced_text, + ) assert ( text_area.text == """\ I must not fear. I will face my fear. -""" - ) - assert ( - deleted_text - == """\ -Fear is the mind-killer. -Fear is the little-death that brings total obliteration. """ ) @@ -238,8 +278,8 @@ async def test_delete_range_empty_document(): async with app.run_test(): text_area = app.query_one(TextArea) text_area.load_text("") - deleted_text = text_area.delete_range((0, 0), (1, 0)) - assert deleted_text == "" + result = text_area.delete_range((0, 0), (1, 0)) + assert result.replaced_text == "" assert text_area.text == "" From 53b38d1b5c50cb2cb80d0314ed3ac124082d827b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 16:56:48 +0100 Subject: [PATCH 204/366] Dunder all, docstring fix --- src/textual/widgets/_text_area.py | 4 ++-- src/textual/widgets/text_area.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index ff15b66f02..d46533e247 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -60,10 +60,10 @@ class Edit: """Where the selection should move to after the replace happens.""" def do(self, text_area: TextArea) -> EditResult: - """Perform the Insert operation. + """Perform the edit operation. Args: - text_area: The TextArea to perform the insert on. + text_area: The TextArea to perform the edit on. Returns: An EditResult containing information about the replace operation. diff --git a/src/textual/widgets/text_area.py b/src/textual/widgets/text_area.py index c40af33762..c25f0bd6dc 100644 --- a/src/textual/widgets/text_area.py +++ b/src/textual/widgets/text_area.py @@ -1,3 +1,6 @@ -from ._text_area import Highlight +from textual.widgets._text_area import Edit, Undoable -__all__ = ["Highlight"] +__all__ = [ + "Edit", + "Undoable", +] From e3fb1277ed08dd5a99f6887d9090d3b326f8e3a1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 17:00:52 +0100 Subject: [PATCH 205/366] Fix XFAIL --- tests/document/test_document_insert.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/document/test_document_insert.py b/tests/document/test_document_insert.py index 713d811a23..f222a4d958 100644 --- a/tests/document/test_document_insert.py +++ b/tests/document/test_document_insert.py @@ -21,13 +21,10 @@ def test_insert_empty_string(): assert document.lines == ["I must not fear.", "Fear is the mind-killer."] -@pytest.mark.xfail(reason="undecided on behaviour") def test_insert_invalid_column(): - # TODO - what is the correct behaviour here? - # right now it appends to the end of the line if the column is too large. document = Document(TEXT) document.replace_range((0, 999), (0, 999), " really") - assert document.lines == ["I must not fear.", "Fear is the mind-killer."] + assert document.lines == ["I must not fear. really", "Fear is the mind-killer."] def test_insert_invalid_row_and_column(): From 8d1316127ca4c61ff9bcd1e74f943e3602d4b10a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 17:01:05 +0100 Subject: [PATCH 206/366] Remove unused import --- tests/document/test_document_insert.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/document/test_document_insert.py b/tests/document/test_document_insert.py index f222a4d958..cba3aed275 100644 --- a/tests/document/test_document_insert.py +++ b/tests/document/test_document_insert.py @@ -1,5 +1,3 @@ -import pytest - from textual.document import Document TEXT = """I must not fear. From acdaefbd85512dc8211115be88ed7adb7005270a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 18:37:57 +0100 Subject: [PATCH 207/366] More tests, tidying up --- src/textual/widgets/_text_area.py | 25 ++-- tests/text_area/test_edit_via_api.py | 175 +++++++++++++++++++++++++-- 2 files changed, 186 insertions(+), 14 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index d46533e247..f45814f174 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -38,7 +38,7 @@ def do(self, text_area: TextArea) -> object | None: def undo(self, text_area: TextArea) -> object | None: """Undo the action.""" - def post_edit(self, text_area: TextArea) -> None: + def after(self, text_area: TextArea) -> None: """Code to execute after content size recalculated and repainted.""" @@ -73,6 +73,12 @@ def do(self, text_area: TextArea) -> EditResult: edit_from = self.from_location edit_to = self.to_location + # This code is mostly handling how we adjust TextArea.selection + # when an edit is made to the document programmatically. + # We want a user who is typing away to maintain their relative + # position in the document even if an insert happens before + # their cursor position. + edit_top, edit_bottom = _sort_ascending(edit_from, edit_to) edit_bottom_row, edit_bottom_column = edit_bottom @@ -82,8 +88,11 @@ def do(self, text_area: TextArea) -> EditResult: replace_result = text_area._document.replace_range(edit_from, edit_to, text) self._replace_result = replace_result + new_edit_to_row, new_edit_to_column = replace_result.end_location + # TODO: We could maybe improve the situation where the selection + # and the edit range overlap with each other. column_offset = new_edit_to_column - edit_bottom_column target_selection_start_column = ( selection_start_column + column_offset @@ -117,7 +126,7 @@ def undo(self, text_area: TextArea) -> EditResult: text_area: The TextArea to undo the insert operation on. """ - def post_edit(self, text_area: TextArea) -> None: + def after(self, text_area: TextArea) -> None: """Possibly update the cursor location after the widget has been refreshed. Args: @@ -509,7 +518,7 @@ def get_text_range(self, start: Location, end: Location) -> str: start, end = _sort_ascending(start, end) return self._document.get_text_range(start, end) - def edit(self, edit: Undoable) -> Any: + def perform_action(self, edit: Undoable) -> Any: """Perform an Edit. Args: @@ -526,7 +535,7 @@ def edit(self, edit: Undoable) -> Any: # self._undo_stack = self._undo_stack[-20:] self._refresh_size() - edit.post_edit(self) + edit.after(self) return result @@ -1008,7 +1017,9 @@ def insert_text( ) -> EditResult: if location is None: location = self.cursor_location - return self.edit(Edit(text, location, location, maintain_selection_offset)) + return self.perform_action( + Edit(text, location, location, maintain_selection_offset) + ) def insert_text_range( self, @@ -1017,7 +1028,7 @@ def insert_text_range( to_location: Location, maintain_selection_offset: bool = True, ) -> EditResult: - return self.edit( + return self.perform_action( Edit(text, from_location, to_location, maintain_selection_offset) ) @@ -1029,7 +1040,7 @@ def delete_range( ) -> EditResult: """Delete text between from_location and to_location.""" top, bottom = _sort_ascending(from_location, to_location) - return self.edit(Edit("", top, bottom, maintain_selection_offset)) + return self.perform_action(Edit("", top, bottom, maintain_selection_offset)) def clear(self) -> None: """Clear the document.""" diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index 538a436f4f..0e2d8eee2a 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -5,6 +5,7 @@ Note that more extensive testing for editing is done at the Document level. """ +import pytest from textual.app import App, ComposeResult from textual.document import EditResult, Selection @@ -17,6 +18,15 @@ I will face my fear. """ +SIMPLE_TEXT = """\ +ABCDE +FGHIJ +KLMNO +PQRST +UVWXY +Z +""" + class TextAreaApp(App): def compose(self) -> ComposeResult: @@ -98,8 +108,22 @@ async def test_insert_text_non_cursor_location_dont_maintain_offset(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.insert_text("Hello", location=(4, 0), maintain_selection_offset=False) + text_area.selection = Selection((2, 3), (3, 5)) + + result = text_area.insert_text( + "Hello", + location=(4, 0), + maintain_selection_offset=False, + ) + + assert result == EditResult( + end_location=(4, 5), + replaced_text="", + ) assert text_area.text == TEXT + "Hello" + + # Since maintain_selection_offset is False, the selection + # is reset to a cursor and goes to the end of the insert. assert text_area.selection == Selection.cursor((4, 5)) @@ -125,7 +149,17 @@ async def test_insert_multiline_text_maintain_offset(): async with app.run_test(): text_area = app.query_one(TextArea) text_area.move_cursor((2, 5)) - text_area.insert_text("Hello,\nworld!") + result = text_area.insert_text("Hello,\nworld!") + + assert result == EditResult( + end_location=(3, 6), + replaced_text="", + ) + + # The insert happens at the cursor (default location) + # Offset is maintained - we inserted 1 line so cursor shifts + # down 1 line, and along by the length of the last insert line. + assert text_area.cursor_location == (3, 6) expected_content = """\ I must not fear. Fear is the mind-killer. @@ -133,10 +167,6 @@ async def test_insert_multiline_text_maintain_offset(): world!is the little-death that brings total obliteration. I will face my fear. """ - # The insert happens at the cursor (default location) - # Offset is maintained - we inserted 1 line so cursor shifts - # down 1 line, and along by the length of the last insert line. - assert text_area.cursor_location == (3, 6) assert text_area.text == expected_content @@ -146,9 +176,18 @@ async def test_insert_range_multiline_text(): text_area = app.query_one(TextArea) # replace "Fear is the mind-killer\nFear is the little death...\n" # with "Hello,\nworld!\n" - text_area.insert_text_range( + result = text_area.insert_text_range( "Hello,\nworld!\n", from_location=(1, 0), to_location=(3, 0) ) + expected_replaced_text = """\ +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +""" + assert result == EditResult( + end_location=(3, 0), + replaced_text=expected_replaced_text, + ) + expected_content = """\ I must not fear. Hello, @@ -296,3 +335,125 @@ async def test_clear_empty_document(): text_area = app.query_one(TextArea) text_area.load_text("") text_area.clear() + + +@pytest.mark.parametrize( + "select_from,select_to", + [ + [(0, 3), (2, 1)], + [(2, 1), (0, 3)], # Ensuring independence from selection direction. + ], +) +async def test_insert_text_multiline_selection_top(select_from, select_to): + """ + An example to attempt to explain what we're testing here... + + X = edit range, * = character in TextArea, S = selection + + *********XX + XXXXX***SSS + SSSSSSSSSSS + SSSS******* + + If an edit happens at XXXX, we need to ensure that the SSS on the + same line is adjusted appropriately so that it's still highlighting + the same characters as before. + """ + app = TextAreaApp() + async with app.run_test(): + # ABCDE + # FGHIJ + # KLMNO + # PQRST + # UVWXY + # Z + text_area = app.query_one(TextArea) + text_area.load_text(SIMPLE_TEXT) + text_area.selection = Selection(select_from, select_to) + + # Check what text is selected. + expected_selected_text = "DE\nFGHIJ\nK" + assert text_area.selected_text == expected_selected_text + + result = text_area.insert_text_range( + "Hello", + from_location=(0, 0), + to_location=(0, 2), + ) + + assert result == EditResult(end_location=(0, 5), replaced_text="AB") + + # The edit range has grown from width 2 to width 5, so the + # top line of the selection was adjusted (column+=3) such that the + # same characters are highlighted: + # ... the selection is not changed after programmatic insert + # ... the same text is selected as before. + assert text_area.selected_text == expected_selected_text + + # The resulting text in the TextArea is correct. + assert text_area.text == "HelloCDE\nFGHIJ\nKLMNO\nPQRST\nUVWXY\nZ\n" + + +@pytest.mark.parametrize( + "select_from,select_to", + [ + [(0, 3), (2, 5)], + [(2, 5), (0, 3)], # Ensuring independence from selection direction. + ], +) +async def test_insert_text_multiline_selection_bottom(select_from, select_to): + """ + The edited text is within the selected text on the bottom line + of the selection. The bottom of the selection should be adjusted + such that any text that was previously selected is still selected. + """ + app = TextAreaApp() + async with app.run_test(): + # ABCDE + # FGHIJ + # KLMNO + # PQRST + # UVWXY + # Z + + text_area = app.query_one(TextArea) + text_area.load_text(SIMPLE_TEXT) + text_area.selection = Selection(select_from, select_to) + + # Check what text is selected. + assert text_area.selected_text == "DE\nFGHIJ\nKLMNO" + + result = text_area.insert_text_range( + "*", + from_location=(2, 0), + to_location=(2, 3), + ) + assert result == EditResult(end_location=(2, 1), replaced_text="KLM") + + # The 'NO' from the selection is still available on the + # bottom selection line, however the 'KLM' is replaced + # with '*'. Since 'NO' is still available, it's maintained + # within the selection. + assert text_area.selected_text == "DE\nFGHIJ\n*NO" + + # The resulting text in the TextArea is correct. + # 'KLM' replaced with '*' + assert text_area.text == "ABCDE\nFGHIJ\n*NO\nPQRST\nUVWXY\nZ\n" + + +async def test_delete_fully_within_selection(): + """User-facing selection should be best-effort adjusted when a programmatic + replacement is made to the document.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + text_area.selection = Selection((0, 2), (0, 7)) + assert text_area.selected_text == "23456" + + result = text_area.delete_range((0, 4), (0, 6)) + assert result == EditResult( + replaced_text="45", + end_location=(0, 4), + ) + assert text_area.selected_text == "01236" From 4b4642b4454f17d04470de497f3b9f654a6d39e5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 18:45:43 +0100 Subject: [PATCH 208/366] Cleaning the API --- src/textual/widgets/_text_area.py | 75 ++++++++++------------------ tests/text_area/test_edit_via_api.py | 52 ++++++++++--------- 2 files changed, 51 insertions(+), 76 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index f45814f174..4c9a6e26a2 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -38,9 +38,6 @@ def do(self, text_area: TextArea) -> object | None: def undo(self, text_area: TextArea) -> object | None: """Undo the action.""" - def after(self, text_area: TextArea) -> None: - """Code to execute after content size recalculated and repainted.""" - @dataclass class Edit: @@ -381,7 +378,6 @@ def load_text(self, text: str) -> None: def _refresh_size(self) -> None: """Calculate the size of the document.""" width, height = self._document.get_size(self.indent_width) - # TODO - this is a prime candidate for optimisation. # +1 width to make space for the cursor resting at the end of the line self.virtual_size = Size(width + self.gutter_width + 1, height) @@ -518,7 +514,7 @@ def get_text_range(self, start: Location, end: Location) -> str: start, end = _sort_ascending(start, end) return self._document.get_text_range(start, end) - def perform_action(self, edit: Undoable) -> Any: + def edit(self, edit: Edit) -> Any: """Perform an Edit. Args: @@ -529,14 +525,8 @@ def perform_action(self, edit: Undoable) -> Any: may be different depending on the edit performed. """ result = edit.do(self) - - # TODO: Think about this... - # self._undo_stack.append(edit) - # self._undo_stack = self._undo_stack[-20:] - self._refresh_size() edit.after(self) - return result async def _on_key(self, event: events.Key) -> None: @@ -551,12 +541,7 @@ async def _on_key(self, event: events.Key) -> None: event.prevent_default() insert = insert_values.get(key, event.character) start, end = self.selection - self.insert_text_range(insert, start, end, False) - - # def undo(self) -> None: - # if self._undo_stack: - # action = self._undo_stack.pop() - # action.undo(self) + self.replace(insert, start, end, False) # --- Lower level event/key handling def get_target_document_location(self, event: MouseEvent) -> Location: @@ -621,7 +606,7 @@ def _on_paste(self, event: events.Paste) -> None: """When a paste occurs, insert the text from the paste event into the document.""" text = event.text if text: - self.insert_text_range(text, *self.selection) + self.replace(text, *self.selection) def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: """Return the column that the cell width corresponds to on the given row. @@ -1009,7 +994,7 @@ def record_cursor_offset(self) -> None: self._last_intentional_cell_width = column_cell_length # --- Editor operations - def insert_text( + def insert( self, text: str, location: Location | None = None, @@ -1017,39 +1002,33 @@ def insert_text( ) -> EditResult: if location is None: location = self.cursor_location - return self.perform_action( - Edit(text, location, location, maintain_selection_offset) - ) + return self.edit(Edit(text, location, location, maintain_selection_offset)) - def insert_text_range( + def delete( self, - text: str, - from_location: Location, - to_location: Location, + start: Location, + end: Location, maintain_selection_offset: bool = True, ) -> EditResult: - return self.perform_action( - Edit(text, from_location, to_location, maintain_selection_offset) - ) + """Delete text between from_location and to_location.""" + top, bottom = _sort_ascending(start, end) + return self.edit(Edit("", top, bottom, maintain_selection_offset)) - def delete_range( + def replace( self, - from_location: Location, - to_location: Location, + insert: str, + start: Location, + end: Location, maintain_selection_offset: bool = True, ) -> EditResult: - """Delete text between from_location and to_location.""" - top, bottom = _sort_ascending(from_location, to_location) - return self.perform_action(Edit("", top, bottom, maintain_selection_offset)) + return self.edit(Edit(insert, start, end, maintain_selection_offset)) def clear(self) -> None: """Clear the document.""" document = self._document last_line = document[-1] document_end_location = (document.line_count, len(last_line)) - self.delete_range( - (0, 0), document_end_location, maintain_selection_offset=False - ) + self.delete((0, 0), document_end_location, maintain_selection_offset=False) def action_delete_left(self) -> None: """Deletes the character to the left of the cursor and updates the cursor location. @@ -1070,7 +1049,7 @@ def action_delete_left(self) -> None: else: end = (end_row, end_column - 1) - self.delete_range(start, end, maintain_selection_offset=False) + self.delete(start, end, maintain_selection_offset=False) def action_delete_right(self) -> None: """Deletes the character to the right of the cursor and keeps the cursor at the same location. @@ -1089,7 +1068,7 @@ def action_delete_right(self) -> None: else: end = (end_row, end_column + 1) - self.delete_range(start, end, maintain_selection_offset=False) + self.delete(start, end, maintain_selection_offset=False) def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" @@ -1100,21 +1079,21 @@ def action_delete_line(self) -> None: from_location = (start_row, 0) to_location = (end_row + 1, 0) - self.delete_range(from_location, to_location, maintain_selection_offset=False) + self.delete(from_location, to_location, maintain_selection_offset=False) def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor location to the start of the line.""" from_location = self.selection.end cursor_row, cursor_column = from_location to_location = (cursor_row, 0) - self.delete_range(from_location, to_location, maintain_selection_offset=False) + self.delete(from_location, to_location, maintain_selection_offset=False) def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor location to the end of the line.""" from_location = self.selection.end cursor_row, cursor_column = from_location to_location = (cursor_row, len(self._document[cursor_row])) - self.delete_range(from_location, to_location, maintain_selection_offset=False) + self.delete(from_location, to_location, maintain_selection_offset=False) def action_delete_word_left(self) -> None: """Deletes the word to the left of the cursor and updates the cursor location.""" @@ -1125,7 +1104,7 @@ def action_delete_word_left(self) -> None: # deletes the characters within the selection range, ignoring word boundaries. start, end = self.selection if start != end: - self.delete_range(start, end, maintain_selection_offset=False) + self.delete(start, end, maintain_selection_offset=False) cursor_row, cursor_column = end @@ -1143,9 +1122,7 @@ def action_delete_word_left(self) -> None: # If we're already on the first line and no word boundary is found, delete to the start of the line from_location = (cursor_row, 0) - self.delete_range( - from_location, self.selection.end, maintain_selection_offset=False - ) + self.delete(from_location, self.selection.end, maintain_selection_offset=False) def action_delete_word_right(self) -> None: """Deletes the word to the right of the cursor and keeps the cursor at the same location.""" @@ -1154,7 +1131,7 @@ def action_delete_word_right(self) -> None: start, end = self.selection if start != end: - self.delete_range(start, end, maintain_selection_offset=False) + self.delete(start, end, maintain_selection_offset=False) cursor_row, cursor_column = end @@ -1172,4 +1149,4 @@ def action_delete_word_right(self) -> None: # If we're already on the last line and no word boundary is found, delete to the end of the line to_location = (cursor_row, len(self._document[cursor_row])) - self.delete_range(end, to_location, maintain_selection_offset=False) + self.delete(end, to_location, maintain_selection_offset=False) diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index 0e2d8eee2a..5fffab70a6 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -42,7 +42,7 @@ async def test_insert_text_start_maintain_selection_offset(): async with app.run_test(): text_area = app.query_one(TextArea) text_area.move_cursor((0, 5)) - text_area.insert_text("Hello", location=(0, 0)) + text_area.insert("Hello", location=(0, 0)) assert text_area.text == "Hello" + TEXT assert text_area.selection == Selection.cursor((0, 10)) @@ -54,7 +54,7 @@ async def test_insert_text_start(): async with app.run_test(): text_area = app.query_one(TextArea) text_area.move_cursor((0, 5)) - text_area.insert_text("Hello", location=(0, 0), maintain_selection_offset=False) + text_area.insert("Hello", location=(0, 0), maintain_selection_offset=False) assert text_area.text == "Hello" + TEXT assert text_area.selection == Selection.cursor((0, 5)) @@ -63,7 +63,7 @@ async def test_insert_newlines_start(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.insert_text("\n\n\n") + text_area.insert("\n\n\n") assert text_area.text == "\n\n\n" + TEXT assert text_area.selection == Selection.cursor((3, 0)) @@ -72,7 +72,7 @@ async def test_insert_newlines_end(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.insert_text("\n\n\n", location=(4, 0)) + text_area.insert("\n\n\n", location=(4, 0)) assert text_area.text == TEXT + "\n\n\n" @@ -83,7 +83,7 @@ async def test_insert_windows_newlines(): # Although we're inserting windows newlines, the configured newline on # the Document inside the TextArea will be "\n", so when we check TextArea.text # we expect to see "\n". - text_area.insert_text("\r\n\r\n\r\n") + text_area.insert("\r\n\r\n\r\n") assert text_area.text == "\n\n\n" + TEXT @@ -91,7 +91,7 @@ async def test_insert_old_mac_newlines(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.insert_text("\r\r\r") + text_area.insert("\r\r\r") assert text_area.text == "\n\n\n" + TEXT @@ -99,7 +99,7 @@ async def test_insert_text_non_cursor_location(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.insert_text("Hello", location=(4, 0)) + text_area.insert("Hello", location=(4, 0)) assert text_area.text == TEXT + "Hello" assert text_area.selection == Selection.cursor((0, 0)) @@ -110,7 +110,7 @@ async def test_insert_text_non_cursor_location_dont_maintain_offset(): text_area = app.query_one(TextArea) text_area.selection = Selection((2, 3), (3, 5)) - result = text_area.insert_text( + result = text_area.insert( "Hello", location=(4, 0), maintain_selection_offset=False, @@ -132,7 +132,7 @@ async def test_insert_multiline_text(): async with app.run_test(): text_area = app.query_one(TextArea) text_area.move_cursor((2, 5)) - text_area.insert_text("Hello,\nworld!", maintain_selection_offset=False) + text_area.insert("Hello,\nworld!", maintain_selection_offset=False) expected_content = """\ I must not fear. Fear is the mind-killer. @@ -149,7 +149,7 @@ async def test_insert_multiline_text_maintain_offset(): async with app.run_test(): text_area = app.query_one(TextArea) text_area.move_cursor((2, 5)) - result = text_area.insert_text("Hello,\nworld!") + result = text_area.insert("Hello,\nworld!") assert result == EditResult( end_location=(3, 6), @@ -176,9 +176,7 @@ async def test_insert_range_multiline_text(): text_area = app.query_one(TextArea) # replace "Fear is the mind-killer\nFear is the little death...\n" # with "Hello,\nworld!\n" - result = text_area.insert_text_range( - "Hello,\nworld!\n", from_location=(1, 0), to_location=(3, 0) - ) + result = text_area.replace("Hello,\nworld!\n", start=(1, 0), end=(3, 0)) expected_replaced_text = """\ Fear is the mind-killer. Fear is the little-death that brings total obliteration. @@ -210,10 +208,10 @@ async def test_insert_range_multiline_text_maintain_selection(): # Text is inserted via the API in a way that shifts # the start and end locations of the word "face" in # both the horizontal and vertical directions. - text_area.insert_text_range( + text_area.replace( "Hello,\nworld!\n123\n456", - from_location=(1, 0), - to_location=(3, 0), + start=(1, 0), + end=(3, 0), ) expected_content = """\ I must not fear. @@ -240,7 +238,7 @@ async def test_delete_range_within_line(): assert text_area.selected_text == "fear" # Delete some text before the selection location. - result = text_area.delete_range((0, 6), (0, 10)) + result = text_area.delete((0, 6), (0, 10)) # Even though the word has 'shifted' left, it's still selected. assert text_area.selection == Selection((0, 7), (0, 11)) @@ -265,7 +263,7 @@ async def test_delete_range_within_line_dont_maintain_offset(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - text_area.delete_range((0, 6), (0, 10), maintain_selection_offset=False) + text_area.delete((0, 6), (0, 10), maintain_selection_offset=False) expected_text = """\ I must fear. Fear is the mind-killer. @@ -286,7 +284,7 @@ async def test_delete_range_multiple_lines_selection_above(): assert text_area.selected_text == "must" # Some lines below are deleted... - result = text_area.delete_range((1, 0), (3, 0)) + result = text_area.delete((1, 0), (3, 0)) # The selection is not affected at all. assert text_area.selection == Selection((0, 2), (0, 6)) @@ -317,7 +315,7 @@ async def test_delete_range_empty_document(): async with app.run_test(): text_area = app.query_one(TextArea) text_area.load_text("") - result = text_area.delete_range((0, 0), (1, 0)) + result = text_area.delete((0, 0), (1, 0)) assert result.replaced_text == "" assert text_area.text == "" @@ -375,10 +373,10 @@ async def test_insert_text_multiline_selection_top(select_from, select_to): expected_selected_text = "DE\nFGHIJ\nK" assert text_area.selected_text == expected_selected_text - result = text_area.insert_text_range( + result = text_area.replace( "Hello", - from_location=(0, 0), - to_location=(0, 2), + start=(0, 0), + end=(0, 2), ) assert result == EditResult(end_location=(0, 5), replaced_text="AB") @@ -423,10 +421,10 @@ async def test_insert_text_multiline_selection_bottom(select_from, select_to): # Check what text is selected. assert text_area.selected_text == "DE\nFGHIJ\nKLMNO" - result = text_area.insert_text_range( + result = text_area.replace( "*", - from_location=(2, 0), - to_location=(2, 3), + start=(2, 0), + end=(2, 3), ) assert result == EditResult(end_location=(2, 1), replaced_text="KLM") @@ -451,7 +449,7 @@ async def test_delete_fully_within_selection(): text_area.selection = Selection((0, 2), (0, 7)) assert text_area.selected_text == "23456" - result = text_area.delete_range((0, 4), (0, 6)) + result = text_area.delete((0, 4), (0, 6)) assert result == EditResult( replaced_text="45", end_location=(0, 4), From 42e34714a53e0c6ee1202e51ec7d54a434a38049 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 18:53:30 +0100 Subject: [PATCH 209/366] Docstrings for TextArea --- src/textual/widgets/_text_area.py | 47 ++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 4c9a6e26a2..a4dec186a2 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1000,6 +1000,19 @@ def insert( location: Location | None = None, maintain_selection_offset: bool = True, ) -> EditResult: + """Insert text into the document. + + Args: + text: The text to insert. + location: The location to insert text, or None to use the cursor location. + maintain_selection_offset: If True, the active Selection will be updated + such that the same text is selected before and after the selection, + if possible. Otherwise, the cursor will jump to the end point of the + edit. + + Returns: + An EditResult containing information about the edit. + """ if location is None: location = self.cursor_location return self.edit(Edit(text, location, location, maintain_selection_offset)) @@ -1010,7 +1023,19 @@ def delete( end: Location, maintain_selection_offset: bool = True, ) -> EditResult: - """Delete text between from_location and to_location.""" + """Delete the text between two locations in the document. + + Args: + start: The start location. + end: The end location. + maintain_selection_offset: If True, the active Selection will be updated + such that the same text is selected before and after the selection, + if possible. Otherwise, the cursor will jump to the end point of the + edit. + + Returns: + An EditResult containing information about the edit. + """ top, bottom = _sort_ascending(start, end) return self.edit(Edit("", top, bottom, maintain_selection_offset)) @@ -1021,14 +1046,28 @@ def replace( end: Location, maintain_selection_offset: bool = True, ) -> EditResult: + """Replace text in the document with new text. + + Args: + insert: The text to insert. + start: The start location + end: The end location. + maintain_selection_offset: If True, the active Selection will be updated + such that the same text is selected before and after the selection, + if possible. Otherwise, the cursor will jump to the end point of the + edit. + + Returns: + An EditResult containing information about the edit. + """ return self.edit(Edit(insert, start, end, maintain_selection_offset)) def clear(self) -> None: - """Clear the document.""" + """Delete all text from the document.""" document = self._document last_line = document[-1] - document_end_location = (document.line_count, len(last_line)) - self.delete((0, 0), document_end_location, maintain_selection_offset=False) + document_end = (document.line_count, len(last_line)) + self.delete((0, 0), document_end, maintain_selection_offset=False) def action_delete_left(self) -> None: """Deletes the character to the left of the cursor and updates the cursor location. From affc8a14d09a481e43e10692fe1f60bee1c36a85 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 19:55:56 +0100 Subject: [PATCH 210/366] A bunch of docstrings, delete unused code --- src/textual/document/_document.py | 9 -- .../document/_syntax_aware_document.py | 66 +++++++---- src/textual/widgets/_text_area.py | 107 +++++++++++------- 3 files changed, 112 insertions(+), 70 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 8bbfbbbb76..4b6b9366d4 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -121,12 +121,6 @@ def get_size(self, indent_width: int) -> Size: def line_count(self) -> int: """Returns the number of lines in the document.""" - @abstractmethod - def tree_query(self, tree_query: str) -> list[object]: - """Query the syntax tree, if available for the current - document. If no syntax tree is available, the returned - list will be empty.""" - class Document(DocumentBase): """A document which can be opened in a TextArea.""" @@ -282,9 +276,6 @@ def get_line_text(self, index: int) -> Text: line_string = line_string.replace("\n", "").replace("\r", "") return Text(line_string, end="") - def tree_query(self, tree_query: str) -> list[object]: - return [] - def __getitem__(self, line_index: SupportsIndex | slice) -> str | list[str]: """Return the content of a line as a string, excluding newline characters. diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 1099017e77..e42a8dab97 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -50,6 +50,9 @@ class SyntaxAwareDocument(Document): def __init__( self, text: str, language: str | Language, syntax_theme: str | SyntaxTheme ): + if not TREE_SITTER: + raise RuntimeError("SyntaxAwareDocument is unavailable on Python 3.7.") + super().__init__(text) self._language: Language | None = None """The tree-sitter Language or None if tree-sitter is unavailable.""" @@ -92,10 +95,8 @@ def __init__( ) self._prepare_highlights() - def replace_range( - self, start: tuple[int, int], end: tuple[int, int], text: str - ) -> EditResult: - """Insert text at the given range. + def replace_range(self, start: Location, end: Location, text: str) -> EditResult: + """Replace text at the given range. Args: start: A tuple (row, column) where the edit starts. @@ -158,15 +159,7 @@ def get_line_text(self, line_index: int) -> Text: return line - def tree_query(self, tree_query: str) -> list[object]: - """Query the syntax tree.""" - query = self._language.query(tree_query) - - captures = query.captures(self._syntax_tree.root_node) - - return list(captures) - - def _location_to_byte_offset(self, location: tuple[int, int]) -> int: + def _location_to_byte_offset(self, location: Location) -> int: """Given a document coordinate, return the byte offset of that coordinate. This method only does work if tree-sitter was imported, otherwise it returns 0. @@ -195,7 +188,15 @@ def _location_to_byte_offset(self, location: tuple[int, int]) -> int: def _location_to_point(self, location: Location) -> tuple[int, int]: """Convert a document location (row_index, column_index) to a tree-sitter - point (row_index, byte_offset_from_start_of_row).""" + point (row_index, byte_offset_from_start_of_row). + + + Args: + location: A location (row index, column codepoint offset) + + Returns: + The point corresponding to that location (row index, column byte offset). + """ lines = self._lines row, column = location if row < len(lines): @@ -259,17 +260,36 @@ def _prepare_highlights( def _build_ast( self, parser: Parser, - ) -> Tree | None: + ) -> Tree: """Fully parse the document and build the abstract syntax tree for it. Returns None if there's no parser available (e.g. when no language is selected). + + Args: + parser: The tree-sitter Parser to parse the document with. + + Returns: + A tree-sitter concrete syntax Tree representing the document, or None + if there's no """ - if parser: - return parser.parse(self._read_callable) - else: - return None + return parser.parse(self._read_callable) def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | None: + """A callable which informs tree-sitter about the document content. + + This is passed to tree-sitter which will call it frequently to retrieve + the bytes from the document. + + Args: + byte_offset: The number of (utf-8) bytes from the start of the document. + point: A tuple (row index, column *byte* offset). Note that this differs + from our Location tuple which is (row_index, column codepoint offset). + + Returns: + All the utf-8 bytes between the byte_offset/point and the end of the current + line _including_ the line separator character(s). Returns None if the + offset/point requested by tree-sitter doesn't correspond to a byte. + """ row, column = point lines = self._lines newline = self.newline @@ -296,6 +316,14 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No @lru_cache(maxsize=128) def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]: + """Build a mapping of utf-8 byte offsets to codepoint offsets for the given data. + + Args: + data: utf-8 bytes. + + Returns: + A `dict[int, int]` mapping byte indices to codepoint indices within `data`. + """ byte_to_codepoint = {} current_byte_offset = 0 code_point_offset = 0 diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index a4dec186a2..6c0b042701 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -7,6 +7,8 @@ from rich.style import Style from rich.text import Text +from textual.document._document import DocumentBase + if TYPE_CHECKING: from tree_sitter import Language @@ -83,7 +85,7 @@ def do(self, text_area: TextArea) -> EditResult: selection_start_row, selection_start_column = selection_start selection_end_row, selection_end_column = selection_end - replace_result = text_area._document.replace_range(edit_from, edit_to, text) + replace_result = text_area.document.replace_range(edit_from, edit_to, text) self._replace_result = replace_result new_edit_to_row, new_edit_to_column = replace_result.end_location @@ -301,7 +303,7 @@ def __init__( ) -> None: super().__init__(name=name, id=id, classes=classes, disabled=disabled) - self._document = Document("") + self.document = Document("") """The document this widget is currently editing.""" self.indent_type: Literal["tabs", "spaces"] = "spaces" @@ -350,47 +352,67 @@ def _watch_indent_width(self) -> None: def _reload_document(self) -> None: """Recreate the document based on the language and theme currently set.""" language = self.language - text = self._document.text + text = self.document.text if not language: # If there's no language set, we don't need to use a SyntaxAwareDocument. - self._document = Document(text) + self.document = Document(text) else: try: from textual.document._syntax_aware_document import SyntaxAwareDocument - self._document = SyntaxAwareDocument(text, language, self.theme) + self.document = SyntaxAwareDocument(text, language, self.theme) except ImportError: # SyntaxAwareDocument isn't available on Python 3.7. # Fall back to the standard document. log.warning("Syntax highlighting isn't available on Python 3.7.") - self._document = Document(text) + self.document = Document(text) def load_text(self, text: str) -> None: - """Load text from a string into the editor. + """Load text from a string into the TextArea. Args: - text: The text to load into the editor. + text: The text to load into the TextArea. """ - self._document = Document(text) + self.document = Document(text) self._reload_document() self._refresh_size() + def load_document(self, document: DocumentBase) -> None: + """Load a document into the TextArea. + + Args: + document: The document to load into the TextArea. + """ + self.document = document + self._refresh_size() + def _refresh_size(self) -> None: - """Calculate the size of the document.""" - width, height = self._document.get_size(self.indent_width) + """Update the virtual size of the TextArea.""" + width, height = self.document.get_size(self.indent_width) # +1 width to make space for the cursor resting at the end of the line self.virtual_size = Size(width + self.gutter_width + 1, height) def render_line(self, widget_y: int) -> Strip: - document = self._document + """Render a single line of the TextArea. Called by Textual. + Args: + widget_y: Y Coordinate of line relative to the widget region. + + Returns: + A rendered line. + """ + document = self.document scroll_x, scroll_y = self.scroll_offset + # Account for how much the TextArea is scrolled. line_index = widget_y + scroll_y + + # Render the lines beyond the valid line numbers out_of_bounds = line_index >= document.line_count if out_of_bounds: return Strip.blank(self.size.width) + # Get the (possibly highlighted) line from the Document. line = document.get_line_text(line_index) line_character_count = len(line) line.tab_size = self.indent_width @@ -406,7 +428,8 @@ def render_line(self, widget_y: int) -> Strip: if start != end and selection_top_row <= line_index <= selection_bottom_row: # If this row intersects with the selection range selection_style = self.get_component_rich_style("text-area--selection") - if line_character_count == 0 and line_index != end: + cursor_row, _ = end + if line_character_count == 0 and line_index != cursor_row: # A simple highlight to show empty lines are included in the selection line = Text("▌", end="", style=Style(color=selection_style.bgcolor)) else: @@ -493,7 +516,7 @@ def render_line(self, widget_y: int) -> Strip: @property def text(self) -> str: """The entire text content of the document.""" - return self._document.text + return self.document.text @property def selected_text(self) -> str: @@ -512,7 +535,7 @@ def get_text_range(self, start: Location, end: Location) -> str: The text between start and end. """ start, end = _sort_ascending(start, end) - return self._document.get_text_range(start, end) + return self.document.get_text_range(start, end) def edit(self, edit: Edit) -> Any: """Perform an Edit. @@ -559,7 +582,7 @@ def get_target_document_location(self, event: MouseEvent) -> Location: target_row = clamp( event.y + scroll_y - self.gutter.top, 0, - self._document.line_count - 1, + self.document.line_count - 1, ) target_column = self.cell_width_to_column_index(target_x, target_row) return target_row, target_column @@ -574,7 +597,7 @@ def gutter_width(self) -> int: # The longest number in the gutter plus two extra characters: `│ `. gutter_margin = 2 gutter_width = ( - len(str(self._document.line_count + 1)) + gutter_margin + len(str(self.document.line_count + 1)) + gutter_margin if self.show_line_numbers else 0 ) @@ -620,7 +643,7 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: """ tab_width = self.indent_width total_cell_offset = 0 - line = self._document[row_index] + line = self.document[row_index] for column_index, character in enumerate(line): total_cell_offset += cell_len(character.expandtabs(tab_width)) if total_cell_offset >= cell_width + 1: @@ -632,7 +655,7 @@ def clamp_visitable(self, location: Location) -> Location: Clamps the given location the nearest location which could be navigated to """ - document = self._document + document = self.document row, column = location try: @@ -740,7 +763,7 @@ def cursor_at_first_row(self) -> bool: @property def cursor_at_last_row(self) -> bool: - return self.selection.end[0] == self._document.line_count - 1 + return self.selection.end[0] == self.document.line_count - 1 @property def cursor_at_start_of_row(self) -> bool: @@ -749,7 +772,7 @@ def cursor_at_start_of_row(self) -> bool: @property def cursor_at_end_of_row(self) -> bool: cursor_row, cursor_column = self.selection.end - row_length = len(self._document[cursor_row]) + row_length = len(self.document[cursor_row]) cursor_at_end = cursor_column == row_length return cursor_at_end @@ -792,7 +815,7 @@ def get_cursor_left_location(self) -> Location: if self.cursor_at_start_of_document: return 0, 0 cursor_row, cursor_column = self.selection.end - length_of_row_above = len(self._document[cursor_row - 1]) + length_of_row_above = len(self.document[cursor_row - 1]) target_row = cursor_row if cursor_column != 0 else cursor_row - 1 target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above return target_row, target_column @@ -837,14 +860,14 @@ def get_cursor_down_location(self): """Get the location the cursor will move to if it moves down.""" cursor_row, cursor_column = self.selection.end if self.cursor_at_last_row: - return cursor_row, len(self._document[cursor_row]) + return cursor_row, len(self.document[cursor_row]) - target_row = min(self._document.line_count - 1, cursor_row + 1) + target_row = min(self.document.line_count - 1, cursor_row + 1) # Attempt to snap last intentional cell length target_column = self.cell_width_to_column_index( self._last_intentional_cell_width, target_row ) - target_column = clamp(target_column, 0, len(self._document[target_row])) + target_column = clamp(target_column, 0, len(self.document[target_row])) return target_row, target_column def action_cursor_up(self) -> None: @@ -867,7 +890,7 @@ def get_cursor_up_location(self) -> Location: target_column = self.cell_width_to_column_index( self._last_intentional_cell_width, target_row ) - target_column = clamp(target_column, 0, len(self._document[target_row])) + target_column = clamp(target_column, 0, len(self.document[target_row])) return target_row, target_column def action_cursor_line_end(self) -> None: @@ -883,7 +906,7 @@ def get_cursor_line_end_location(self) -> Location: """ start, end = self.selection cursor_row, cursor_column = end - target_column = len(self._document[cursor_row]) + target_column = len(self.document[cursor_row]) return cursor_row, target_column def action_cursor_line_start(self) -> None: @@ -916,7 +939,7 @@ def get_cursor_left_word_location(self) -> Location: """ cursor_row, cursor_column = self.cursor_location # Check the current line for a word boundary - line = self._document[cursor_row][:cursor_column] + line = self.document[cursor_row][:cursor_column] matches = list(re.finditer(self.word_pattern, line)) if matches: # If a word boundary is found, move the cursor there @@ -924,7 +947,7 @@ def get_cursor_left_word_location(self) -> Location: elif cursor_row > 0: # If no word boundary is found, and we're not on the first line, move to the end of the previous line cursor_row -= 1 - cursor_column = len(self._document[cursor_row]) + cursor_column = len(self.document[cursor_row]) else: # If we're already on the first line and no word boundary is found, move to the start of the line cursor_column = 0 @@ -947,18 +970,18 @@ def get_cursor_right_word_location(self): """ cursor_row, cursor_column = self.selection.end # Check the current line for a word boundary - line = self._document[cursor_row][cursor_column:] + line = self.document[cursor_row][cursor_column:] matches = list(re.finditer(self.word_pattern, line)) if matches: # If a word boundary is found, move the cursor there cursor_column += matches[0].end() - elif cursor_row < self._document.line_count - 1: + elif cursor_row < self.document.line_count - 1: # If no word boundary is found and we're not on the last line, move to the start of the next line cursor_row += 1 cursor_column = 0 else: # If we're already on the last line and no word boundary is found, move to the end of the line - cursor_column = len(self._document[cursor_row]) + cursor_column = len(self.document[cursor_row]) return cursor_row, cursor_column def action_cursor_page_up(self) -> None: @@ -979,13 +1002,13 @@ def action_cursor_page_down(self) -> None: @property def cursor_line_text(self) -> str: - return self._document[self.selection.end[0]] + return self.document[self.selection.end[0]] def get_column_cell_width(self, row: int, column: int) -> int: """Given a row and column index within the editor, return the cell offset of the column from the start of the row (the left edge of the editor content area). """ - line = self._document[row] + line = self.document[row] return cell_len(line[:column].expandtabs(self.indent_width)) def record_cursor_offset(self) -> None: @@ -1064,7 +1087,7 @@ def replace( def clear(self) -> None: """Delete all text from the document.""" - document = self._document + document = self.document last_line = document[-1] document_end = (document.line_count, len(last_line)) self.delete((0, 0), document_end, maintain_selection_offset=False) @@ -1084,7 +1107,7 @@ def action_delete_left(self) -> None: return if self.cursor_at_start_of_row: - end = (end_row - 1, len(self._document[end_row - 1])) + end = (end_row - 1, len(self.document[end_row - 1])) else: end = (end_row, end_column - 1) @@ -1131,7 +1154,7 @@ def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor location to the end of the line.""" from_location = self.selection.end cursor_row, cursor_column = from_location - to_location = (cursor_row, len(self._document[cursor_row])) + to_location = (cursor_row, len(self.document[cursor_row])) self.delete(from_location, to_location, maintain_selection_offset=False) def action_delete_word_left(self) -> None: @@ -1148,7 +1171,7 @@ def action_delete_word_left(self) -> None: cursor_row, cursor_column = end # Check the current line for a word boundary - line = self._document[cursor_row][:cursor_column] + line = self.document[cursor_row][:cursor_column] matches = list(re.finditer(self.word_pattern, line)) if matches: @@ -1156,7 +1179,7 @@ def action_delete_word_left(self) -> None: from_location = (cursor_row, matches[-1].start()) elif cursor_row > 0: # If no word boundary is found, and we're not on the first line, delete to the end of the previous line - from_location = (cursor_row - 1, len(self._document[cursor_row - 1])) + from_location = (cursor_row - 1, len(self.document[cursor_row - 1])) else: # If we're already on the first line and no word boundary is found, delete to the start of the line from_location = (cursor_row, 0) @@ -1175,17 +1198,17 @@ def action_delete_word_right(self) -> None: cursor_row, cursor_column = end # Check the current line for a word boundary - line = self._document[cursor_row][cursor_column:] + line = self.document[cursor_row][cursor_column:] matches = list(re.finditer(self.word_pattern, line)) if matches: # If a word boundary is found, delete the word to_location = (cursor_row, cursor_column + matches[0].end()) - elif cursor_row < self._document.line_count - 1: + elif cursor_row < self.document.line_count - 1: # If no word boundary is found, and we're not on the last line, delete to the start of the next line to_location = (cursor_row + 1, 0) else: # If we're already on the last line and no word boundary is found, delete to the end of the line - to_location = (cursor_row, len(self._document[cursor_row])) + to_location = (cursor_row, len(self.document[cursor_row])) self.delete(end, to_location, maintain_selection_offset=False) From 232a9cd9d9b34c080c5d09c1f4f70896e1b4084d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 20:27:14 +0100 Subject: [PATCH 211/366] More tidying and docstrings --- .../document/_syntax_aware_document.py | 35 ++---- src/textual/widgets/_text_area.py | 105 ++++++++++++------ tests/text_area/test_selection.py | 4 +- 3 files changed, 80 insertions(+), 64 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index e42a8dab97..9aacdbfdd3 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -31,6 +31,7 @@ EndColumn = Optional[int] HighlightName = Optional[str] Highlight = Tuple[StartColumn, EndColumn, HighlightName] +"""A tuple representing a syntax highlight within one line.""" class SyntaxAwareDocument(Document): @@ -89,7 +90,7 @@ def __init__( self._syntax_theme = SyntaxTheme.get_theme(syntax_theme) self._syntax_theme.highlight_query = highlight_query_path.read_text() - self._syntax_tree = self._build_ast(self._parser) + self._syntax_tree = self._parser.parse(self._read_callable) self._query: Query = self._language.query( self._syntax_theme.highlight_query ) @@ -128,6 +129,7 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult old_end_point=old_end_point, new_end_point=self._location_to_point(end_location), ) + # Incrementally parse the document. self._syntax_tree = self._parser.parse( self._read_callable, self._syntax_tree ) @@ -147,16 +149,17 @@ def get_line_text(self, line_index: int) -> Text: line_bytes = _utf8_encode(self[line_index]) byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) line = Text(self[line_index], end="") - if self._highlights: - highlights = self._highlights[line_index] + highlights = self._highlights + get_highlight_from_theme = self._syntax_theme.get_highlight + if highlights: + highlights = highlights[line_index] for start, end, highlight_name in highlights: - node_style = self._syntax_theme.get_highlight(highlight_name) + node_style = get_highlight_from_theme(highlight_name) line.stylize( node_style, byte_to_codepoint.get(start, 0), byte_to_codepoint.get(end), ) - return line def _location_to_byte_offset(self, location: Location) -> int: @@ -190,7 +193,6 @@ def _location_to_point(self, location: Location) -> tuple[int, int]: """Convert a document location (row_index, column_index) to a tree-sitter point (row_index, byte_offset_from_start_of_row). - Args: location: A location (row index, column codepoint offset) @@ -257,23 +259,6 @@ def _prepare_highlights( for line_index, updated_highlights in highlight_updates.items(): highlights[line_index] = updated_highlights - def _build_ast( - self, - parser: Parser, - ) -> Tree: - """Fully parse the document and build the abstract syntax tree for it. - - Returns None if there's no parser available (e.g. when no language is selected). - - Args: - parser: The tree-sitter Parser to parse the document with. - - Returns: - A tree-sitter concrete syntax Tree representing the document, or None - if there's no - """ - return parser.parse(self._read_callable) - def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | None: """A callable which informs tree-sitter about the document content. @@ -329,9 +314,7 @@ def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]: code_point_offset = 0 while current_byte_offset < len(data): - # Save the mapping before incrementing the byte offset byte_to_codepoint[current_byte_offset] = code_point_offset - first_byte = data[current_byte_offset] # Single-byte character @@ -349,10 +332,8 @@ def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]: else: raise ValueError(f"Invalid UTF-8 byte: {first_byte}") - # Increment the code-point counter code_point_offset += 1 # Mapping for the end of the string byte_to_codepoint[current_byte_offset] = code_point_offset - return byte_to_codepoint diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 6c0b042701..d790963972 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -623,7 +623,7 @@ def _on_mouse_up(self, event: events.MouseUp) -> None: """Finalise the selection that has been made using the mouse.""" self._selecting = False self.release_mouse() - self.record_cursor_offset() + self.record_cursor_width() def _on_paste(self, event: events.Paste) -> None: """When a paste occurs, insert the text from the paste event into the document.""" @@ -653,7 +653,11 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: def clamp_visitable(self, location: Location) -> Location: """Clamp the given location to the nearest visitable location. - Clamps the given location the nearest location which could be navigated to + Args: + location: The location to clamp. + + Returns: + The nearest location that we could conceivably navigate to using the cursor. """ document = self.document @@ -669,22 +673,25 @@ def clamp_visitable(self, location: Location) -> Location: return row, column # --- Cursor/selection utilities - def scroll_cursor_visible(self, center: bool = False) -> Offset: + def scroll_cursor_visible( + self, center: bool = False, animate: bool = False + ) -> Offset: """Scroll the `TextArea` such that the cursor is visible on screen. Args: center: True if the cursor should be scrolled to the center. + animate: True if we should animate while scrolling. Returns: The offset that was scrolled to bring the cursor into view. """ row, column = self.selection.end - text = self.cursor_line_text[:column] + text = self.document[row][:column] column_offset = cell_len(text.expandtabs(self.indent_width)) scroll_offset = self.scroll_to_region( Region(x=column_offset, y=row, width=3, height=1), spacing=Spacing(right=self.gutter_width), - animate=False, + animate=animate, force=True, center=center, ) @@ -703,8 +710,9 @@ def move_cursor( location: The location to move the cursor to. select: If True, select text between the old and new location. center: If True, scroll such that the cursor is centered. - record_width: If True, record the cursor column cell width so that we - jump to a corresponding location when moving between rows. + record_width: If True, record the cursor column cell width after navigating + so that we jump back to the same width the next time we move to a row + that is wide enough. """ if select: start, end = self.selection @@ -713,7 +721,7 @@ def move_cursor( self.selection = Selection.cursor(location) if record_width: - self.record_cursor_offset() + self.record_cursor_width() if center: self.scroll_cursor_visible(center) @@ -725,15 +733,17 @@ def move_cursor_relative( select: bool = False, center: bool = False, record_width: bool = True, - ): + ) -> None: """Move the cursor relative to its current location. Args: - location: The location to move the cursor to. + rows: The number of rows to move down by (negative to move up) + columns: The number of columns to move right by (negative to move left) select: If True, select text between the old and new location. center: If True, scroll such that the cursor is centered. - record_width: If True, record the cursor column cell width so that we - jump to a corresponding location when moving between rows. + record_width: If True, record the cursor column cell width after navigating + so that we jump back to the same width the next time we move to a row + that is wide enough. """ clamp_visitable = self.clamp_visitable start, end = self.selection @@ -759,18 +769,22 @@ def cursor_location(self, location: Location) -> None: @property def cursor_at_first_row(self) -> bool: + """True if and only if the cursor is on the first row.""" return self.selection.end[0] == 0 @property def cursor_at_last_row(self) -> bool: + """True if and only if the cursor is on the last row.""" return self.selection.end[0] == self.document.line_count - 1 @property def cursor_at_start_of_row(self) -> bool: + """True if and only if the cursor is at column 0.""" return self.selection.end[1] == 0 @property def cursor_at_end_of_row(self) -> bool: + """True if and only if the cursor is at the end of a row.""" cursor_row, cursor_column = self.selection.end row_length = len(self.document[cursor_row]) cursor_at_end = cursor_column == row_length @@ -778,11 +792,12 @@ def cursor_at_end_of_row(self) -> bool: @property def cursor_at_start_of_document(self) -> bool: - return self.cursor_at_first_row and self.cursor_at_start_of_row + """True if and only if the cursor is at location (0, 0)""" + return self.selection.end == (0, 0) @property def cursor_at_end_of_document(self) -> bool: - """True if the cursor is at the very end of the document.""" + """True if and only if the cursor is at the very end of the document.""" return self.cursor_at_last_row and self.cursor_at_end_of_row # ------ Cursor movement actions @@ -794,7 +809,7 @@ def action_cursor_left(self) -> None: """ target = self.get_cursor_left_location() self.selection = Selection.cursor(target) - self.record_cursor_offset() + self.record_cursor_width() def action_cursor_left_select(self): """Move the end of the selection one location to the left. @@ -804,7 +819,7 @@ def action_cursor_left_select(self): new_cursor_location = self.get_cursor_left_location() selection_start, selection_end = self.selection self.selection = Selection(selection_start, new_cursor_location) - self.record_cursor_offset() + self.record_cursor_width() def get_cursor_left_location(self) -> Location: """Get the location the cursor will move to if it moves left. @@ -827,18 +842,19 @@ def action_cursor_right(self) -> None: """ target = self.get_cursor_right_location() self.move_cursor(target) - self.record_cursor_offset() + self.record_cursor_width() def action_cursor_right_select(self): - """Move the end of the selection one location to the right. - - This will expand or contract the selection. - """ + """Move the end of the selection one location to the right.""" target = self.get_cursor_right_location() self.move_cursor(target, select=True) def get_cursor_right_location(self) -> Location: - """Get the location the cursor will move to if it moves right.""" + """Get the location the cursor will move to if it moves right. + + Returns: + the location the cursor will move to if it moves right. + """ if self.cursor_at_end_of_document: return self.selection.end cursor_row, cursor_column = self.selection.end @@ -856,8 +872,12 @@ def action_cursor_down_select(self) -> None: target = self.get_cursor_down_location() self.move_cursor(target, select=True, record_width=False) - def get_cursor_down_location(self): - """Get the location the cursor will move to if it moves down.""" + def get_cursor_down_location(self) -> Location: + """Get the location the cursor will move to if it moves down. + + Returns: + the location the cursor will move to if it moves down. + """ cursor_row, cursor_column = self.selection.end if self.cursor_at_last_row: return cursor_row, len(self.document[cursor_row]) @@ -881,7 +901,11 @@ def action_cursor_up_select(self) -> None: self.move_cursor(target, select=True, record_width=False) def get_cursor_up_location(self) -> Location: - """Get the location the cursor will move to if it moves up.""" + """Get the location the cursor will move to if it moves up. + + Returns: + the location the cursor will move to if it moves up. + """ if self.cursor_at_first_row: return 0, 0 cursor_row, cursor_column = self.selection.end @@ -915,10 +939,10 @@ def action_cursor_line_start(self) -> None: self.move_cursor(target) def get_cursor_line_start_location(self) -> Location: - """Get the location of the end of the current line. + """Get the location of the start of the current line. Returns: - The (row, column) location of the end of the cursors current line. + The (row, column) location of the start of the cursors current line. """ _start, end = self.selection cursor_row, _cursor_column = end @@ -985,6 +1009,7 @@ def get_cursor_right_word_location(self): return cursor_row, cursor_column def action_cursor_page_up(self) -> None: + """Move the cursor and scroll up one page.""" height = self.content_size.height _, cursor_location = self.selection row, column = cursor_location @@ -993,6 +1018,7 @@ def action_cursor_page_up(self) -> None: self.move_cursor(target) def action_cursor_page_down(self) -> None: + """Move the cursor and scroll down one page.""" height = self.content_size.height _, cursor_location = self.selection row, column = cursor_location @@ -1000,20 +1026,29 @@ def action_cursor_page_down(self) -> None: self.scroll_relative(y=height, animate=False) self.move_cursor(target) - @property - def cursor_line_text(self) -> str: - return self.document[self.selection.end[0]] + def get_column_width(self, row: int, column: int) -> int: + """Get the cell offset of the column from the start of the row. + + Args: + row: The row index. + column: The column index (codepoint offset from start of row). - def get_column_cell_width(self, row: int, column: int) -> int: - """Given a row and column index within the editor, return the cell offset - of the column from the start of the row (the left edge of the editor content area). + Returns: + The cell width of the column relative to the start of the row. """ line = self.document[row] return cell_len(line[:column].expandtabs(self.indent_width)) - def record_cursor_offset(self) -> None: + def record_cursor_width(self) -> None: + """Record the current cell width of the cursor. + + This is used where we navigate up and down through rows. + If we're in the middle of a row, and go down to a row with no + content, then we go down to another row, we want our cursor to + jump back to the same offset that we were originally at. + """ row, column = self.selection.end - column_cell_length = self.get_column_cell_width(row, column) + column_cell_length = self.get_column_width(row, column) self._last_intentional_cell_width = column_cell_length # --- Editor operations diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 3a72e3b611..8a33b6ee65 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -223,7 +223,7 @@ async def test_get_cursor_up_location(start, end): text_area.move_cursor(start) # This is required otherwise the cursor will snap back to the # last location navigated to (0, 0) - text_area.record_cursor_offset() + text_area.record_cursor_width() assert text_area.get_cursor_up_location() == end @@ -242,7 +242,7 @@ async def test_get_cursor_down_location(start, end): text_area.move_cursor(start) # This is required otherwise the cursor will snap back to the # last location navigated to (0, 0) - text_area.record_cursor_offset() + text_area.record_cursor_width() assert text_area.get_cursor_down_location() == end From 91a6ce9b67458ae3e8d363f6d485aacaa2c1ba46 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 20:45:30 +0100 Subject: [PATCH 212/366] Cursor origin on document load, correctly handle delete word left/right when selection is non-empty, fix delete_line when selection spans multiple lines and is in reverse direction --- src/textual/widgets/_text_area.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index d790963972..2ed28ed75f 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -375,6 +375,7 @@ def load_text(self, text: str) -> None: """ self.document = Document(text) self._reload_document() + self.move_cursor((0, 0)) self._refresh_size() def load_document(self, document: DocumentBase) -> None: @@ -384,6 +385,7 @@ def load_document(self, document: DocumentBase) -> None: document: The document to load into the TextArea. """ self.document = document + self.move_cursor((0, 0)) self._refresh_size() def _refresh_size(self) -> None: @@ -1170,6 +1172,7 @@ def action_delete_right(self) -> None: def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" start, end = self.selection + start, end = _sort_ascending(start, end) start_row, start_column = start end_row, end_column = end @@ -1202,6 +1205,7 @@ def action_delete_word_left(self) -> None: start, end = self.selection if start != end: self.delete(start, end, maintain_selection_offset=False) + return cursor_row, cursor_column = end @@ -1229,6 +1233,7 @@ def action_delete_word_right(self) -> None: start, end = self.selection if start != end: self.delete(start, end, maintain_selection_offset=False) + return cursor_row, cursor_column = end From e2779540fd474ae787f38d6b6d1b0a9e565ffa9a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 20:49:21 +0100 Subject: [PATCH 213/366] Moving things around --- src/textual/widgets/_text_area.py | 218 +++++++++++++++--------------- src/textual/widgets/text_area.py | 3 +- 2 files changed, 110 insertions(+), 111 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 2ed28ed75f..43c3b02bdf 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -25,115 +25,6 @@ from textual.strip import Strip -@runtime_checkable -class Undoable(Protocol): - """Protocol for actions performed in the text editor which can be done and undone. - - These are typically actions which affect the document (e.g. inserting and deleting - text), but they can really be anything. - - To perform an edit operation, pass the Edit to `TextArea.edit()`""" - - def do(self, text_area: TextArea) -> object | None: - """Do the action.""" - - def undo(self, text_area: TextArea) -> object | None: - """Undo the action.""" - - -@dataclass -class Edit: - """Implements the Undoable protocol to replace text at some range within a document.""" - - text: str - """The text to insert. An empty string is equivalent to deletion.""" - from_location: Location - """The start location of the insert.""" - to_location: Location - """The end location of the insert""" - maintain_selection_offset: bool - """If True, the selection will maintain its offset to the replacement range.""" - _replace_result: EditResult | None = field(init=False, default=None) - """Contains data relating to the replace operation.""" - _updated_selection: Location | None = field(init=False, default=None) - """Where the selection should move to after the replace happens.""" - - def do(self, text_area: TextArea) -> EditResult: - """Perform the edit operation. - - Args: - text_area: The TextArea to perform the edit on. - - Returns: - An EditResult containing information about the replace operation. - """ - text = self.text - - edit_from = self.from_location - edit_to = self.to_location - - # This code is mostly handling how we adjust TextArea.selection - # when an edit is made to the document programmatically. - # We want a user who is typing away to maintain their relative - # position in the document even if an insert happens before - # their cursor position. - - edit_top, edit_bottom = _sort_ascending(edit_from, edit_to) - edit_bottom_row, edit_bottom_column = edit_bottom - - selection_start, selection_end = text_area.selection - selection_start_row, selection_start_column = selection_start - selection_end_row, selection_end_column = selection_end - - replace_result = text_area.document.replace_range(edit_from, edit_to, text) - self._replace_result = replace_result - - new_edit_to_row, new_edit_to_column = replace_result.end_location - - # TODO: We could maybe improve the situation where the selection - # and the edit range overlap with each other. - column_offset = new_edit_to_column - edit_bottom_column - target_selection_start_column = ( - selection_start_column + column_offset - if edit_bottom_row == selection_start_row - else selection_start_column - ) - target_selection_end_column = ( - selection_end_column + column_offset - if edit_bottom_row == selection_end_row - else selection_end_column - ) - - row_offset = new_edit_to_row - edit_bottom_row - target_selection_start_row = selection_start_row + row_offset - target_selection_end_row = selection_end_row + row_offset - - if self.maintain_selection_offset: - self._updated_selection = Selection( - start=(target_selection_start_row, target_selection_start_column), - end=(target_selection_end_row, target_selection_end_column), - ) - else: - self._updated_selection = Selection.cursor(replace_result.end_location) - - return replace_result - - def undo(self, text_area: TextArea) -> EditResult: - """Undo the Replace operation. - - Args: - text_area: The TextArea to undo the insert operation on. - """ - - def after(self, text_area: TextArea) -> None: - """Possibly update the cursor location after the widget has been refreshed. - - Args: - text_area: The TextArea this operation was performed on. - """ - text_area.selection = self._updated_selection - - class TextArea(ScrollView, can_focus=True): DEFAULT_CSS = """\ $text-area-active-line-bg: white 8%; @@ -1252,3 +1143,112 @@ def action_delete_word_right(self) -> None: to_location = (cursor_row, len(self.document[cursor_row])) self.delete(end, to_location, maintain_selection_offset=False) + + +@dataclass +class Edit: + """Implements the Undoable protocol to replace text at some range within a document.""" + + text: str + """The text to insert. An empty string is equivalent to deletion.""" + from_location: Location + """The start location of the insert.""" + to_location: Location + """The end location of the insert""" + maintain_selection_offset: bool + """If True, the selection will maintain its offset to the replacement range.""" + _replace_result: EditResult | None = field(init=False, default=None) + """Contains data relating to the replace operation.""" + _updated_selection: Location | None = field(init=False, default=None) + """Where the selection should move to after the replace happens.""" + + def do(self, text_area: TextArea) -> EditResult: + """Perform the edit operation. + + Args: + text_area: The TextArea to perform the edit on. + + Returns: + An EditResult containing information about the replace operation. + """ + text = self.text + + edit_from = self.from_location + edit_to = self.to_location + + # This code is mostly handling how we adjust TextArea.selection + # when an edit is made to the document programmatically. + # We want a user who is typing away to maintain their relative + # position in the document even if an insert happens before + # their cursor position. + + edit_top, edit_bottom = _sort_ascending(edit_from, edit_to) + edit_bottom_row, edit_bottom_column = edit_bottom + + selection_start, selection_end = text_area.selection + selection_start_row, selection_start_column = selection_start + selection_end_row, selection_end_column = selection_end + + replace_result = text_area.document.replace_range(edit_from, edit_to, text) + self._replace_result = replace_result + + new_edit_to_row, new_edit_to_column = replace_result.end_location + + # TODO: We could maybe improve the situation where the selection + # and the edit range overlap with each other. + column_offset = new_edit_to_column - edit_bottom_column + target_selection_start_column = ( + selection_start_column + column_offset + if edit_bottom_row == selection_start_row + else selection_start_column + ) + target_selection_end_column = ( + selection_end_column + column_offset + if edit_bottom_row == selection_end_row + else selection_end_column + ) + + row_offset = new_edit_to_row - edit_bottom_row + target_selection_start_row = selection_start_row + row_offset + target_selection_end_row = selection_end_row + row_offset + + if self.maintain_selection_offset: + self._updated_selection = Selection( + start=(target_selection_start_row, target_selection_start_column), + end=(target_selection_end_row, target_selection_end_column), + ) + else: + self._updated_selection = Selection.cursor(replace_result.end_location) + + return replace_result + + def undo(self, text_area: TextArea) -> EditResult: + """Undo the Replace operation. + + Args: + text_area: The TextArea to undo the insert operation on. + """ + + def after(self, text_area: TextArea) -> None: + """Possibly update the cursor location after the widget has been refreshed. + + Args: + text_area: The TextArea this operation was performed on. + """ + text_area.selection = self._updated_selection + + +@runtime_checkable +class Undoable(Protocol): + """Protocol for actions performed in the text editor which can be done and undone. + + These are typically actions which affect the document (e.g. inserting and deleting + text), but they can really be anything. + + To perform an edit operation, pass the Edit to `TextArea.edit()`""" + + def do(self, text_area: TextArea) -> object | None: + """Do the action.""" + + def undo(self, text_area: TextArea) -> object | None: + """Undo the action.""" diff --git a/src/textual/widgets/text_area.py b/src/textual/widgets/text_area.py index c25f0bd6dc..b67dc051c9 100644 --- a/src/textual/widgets/text_area.py +++ b/src/textual/widgets/text_area.py @@ -1,6 +1,5 @@ -from textual.widgets._text_area import Edit, Undoable +from textual.widgets._text_area import Edit __all__ = [ "Edit", - "Undoable", ] From 438125cda7129af99faaea9bb78f0a3e21a7d89b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 20:50:19 +0100 Subject: [PATCH 214/366] Fixing dunder all to export DocumentBase --- src/textual/document/__init__.py | 3 ++- src/textual/widgets/_text_area.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/textual/document/__init__.py b/src/textual/document/__init__.py index 5e8346111b..c395a77ff1 100644 --- a/src/textual/document/__init__.py +++ b/src/textual/document/__init__.py @@ -1,4 +1,4 @@ -from ._document import Document, EditResult, Location, Selection +from ._document import Document, DocumentBase, EditResult, Location, Selection from ._languages import VALID_LANGUAGES from ._syntax_aware_document import ( EndColumn, @@ -11,6 +11,7 @@ __all__ = [ "Document", + "DocumentBase", "EndColumn", "Highlight", "HighlightName", diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 43c3b02bdf..5a00cc79b0 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -7,8 +7,6 @@ from rich.style import Style from rich.text import Text -from textual.document._document import DocumentBase - if TYPE_CHECKING: from tree_sitter import Language @@ -17,7 +15,14 @@ from textual._fix_direction import _sort_ascending from textual._types import Literal, Protocol, runtime_checkable from textual.binding import Binding -from textual.document import Document, EditResult, Location, Selection, SyntaxTheme +from textual.document import ( + Document, + DocumentBase, + EditResult, + Location, + Selection, + SyntaxTheme, +) from textual.events import MouseEvent from textual.geometry import Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive From 9f33e65e0742f6729112bb74907a4df4af05e60f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 20:52:02 +0100 Subject: [PATCH 215/366] Add docstring --- src/textual/_fix_direction.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/textual/_fix_direction.py b/src/textual/_fix_direction.py index af1609a20e..26dd01fea8 100644 --- a/src/textual/_fix_direction.py +++ b/src/textual/_fix_direction.py @@ -5,7 +5,15 @@ def _sort_ascending( start: tuple[int, int], end: tuple[int, int] ) -> tuple[tuple[int, int], tuple[int, int]]: """Given a range, return a new range (x, y) such - that x <= y which covers the same characters.""" + that x <= y which covers the same characters. + + Args: + start: The start of the range. + end: The end of the range. + + Returns: + A tuple (lesser_point, greater_point). + """ if start > end: return end, start return start, end From 360062eb51b2c4e6393904ae864de113b99cf0c6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 21:20:16 +0100 Subject: [PATCH 216/366] Record cursor width on programmatic insert since it can result in the cursor moving --- src/textual/widgets/_text_area.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 5a00cc79b0..e46b6ec143 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -802,7 +802,7 @@ def get_cursor_up_location(self) -> Location: """Get the location the cursor will move to if it moves up. Returns: - the location the cursor will move to if it moves up. + The location the cursor will move to if it moves up. """ if self.cursor_at_first_row: return 0, 0 @@ -1222,6 +1222,7 @@ def do(self, text_area: TextArea) -> EditResult: start=(target_selection_start_row, target_selection_start_column), end=(target_selection_end_row, target_selection_end_column), ) + text_area.record_cursor_width() else: self._updated_selection = Selection.cursor(replace_result.end_location) From 65cfe12a99e8ca511ab6e7d4b217093b6258f18f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 17 Aug 2023 21:50:30 +0100 Subject: [PATCH 217/366] Typing fixes --- src/textual/document/_document.py | 11 +++++++++++ src/textual/widgets/_text_area.py | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 4b6b9366d4..df37f0c0bf 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -121,6 +121,17 @@ def get_size(self, indent_width: int) -> Size: def line_count(self) -> int: """Returns the number of lines in the document.""" + @abstractmethod + def __getitem__(self, line_index: SupportsIndex | slice) -> str | list[str]: + """Return the content of a line as a string, excluding newline characters. + + Args: + line_index: The index or slice of the line(s) to retrieve. + + Returns: + The line or list of lines requested. + """ + class Document(DocumentBase): """A document which can be opened in a TextArea.""" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index e46b6ec143..0e560386a2 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -199,7 +199,7 @@ def __init__( ) -> None: super().__init__(name=name, id=id, classes=classes, disabled=disabled) - self.document = Document("") + self.document: DocumentBase = Document("") """The document this widget is currently editing.""" self.indent_type: Literal["tabs", "spaces"] = "spaces" @@ -461,6 +461,9 @@ async def _on_key(self, event: events.Key) -> None: event.stop() event.prevent_default() insert = insert_values.get(key, event.character) + # `insert` is not None because event.character cannot be + # None because we've checked that it's printable. + assert insert is not None start, end = self.selection self.replace(insert, start, end, False) From e166afcbfdded0e88d77bc9918f95ad1c11c37d8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 21 Aug 2023 13:12:31 +0100 Subject: [PATCH 218/366] Fixing remaining typing issues with TextArea --- poetry.lock | 14 +++++++++++++- pyproject.toml | 1 + src/textual/document/_document.py | 14 +++++++++++--- .../document/_syntax_aware_document.py | 1 + src/textual/widgets/_text_area.py | 19 +++++++++---------- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3689839759..e7e39cbc46 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1047,6 +1047,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "types-tree-sitter" +version = "0.20.1.4" +description = "Typing stubs for tree-sitter" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "typing-extensions" version = "4.7.1" @@ -1145,7 +1153,7 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "6310dfc187fa28cd8b2b686f02eb0167c1d5cc3c31a7d5445410b546c3f853d3" +content-hash = "1bdcf7f05a06c9d172aa29ac9b7ee47213a00d101ff3ac1bf61451bebabfe5b9" [metadata.files] aiohttp = [ @@ -2257,6 +2265,10 @@ types-setuptools = [ {file = "types-setuptools-67.8.0.0.tar.gz", hash = "sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f"}, {file = "types_setuptools-67.8.0.0-py3-none-any.whl", hash = "sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff"}, ] +types-tree-sitter = [ + {file = "types-tree-sitter-0.20.1.4.tar.gz", hash = "sha256:673730dcc2efe09be6cdbd9795cdc5243c164262b7a539e6d7e7980fd06c0907"}, + {file = "types_tree_sitter-0.20.1.4-py3-none-any.whl", hash = "sha256:9a38efd62a3cf66f9751c612588b7dbc72340fd6c81fd089c8a0f5877f86b58c"}, +] typing-extensions = [ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, diff --git a/pyproject.toml b/pyproject.toml index 21f62bfc74..1833cb4e24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ types-setuptools = "^67.2.0.1" textual-dev = "^1.1.0" pytest-asyncio = "*" pytest-textual-snapshot = "0.2.0" # TODO: pinned while while we resolve issue #3042 +types-tree-sitter = "^0.20.1.4" [tool.black] includes = "src" diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index df37f0c0bf..3827e7fe14 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from functools import lru_cache -from typing import NamedTuple, Tuple +from typing import NamedTuple, Tuple, overload from rich.text import Text @@ -65,7 +65,7 @@ class DocumentBase(ABC): provide in order to be used by the TextArea widget.""" @abstractmethod - def replace_range(self, start: Location, end: Location, text: str) -> Location: + def replace_range(self, start: Location, end: Location, text: str) -> EditResult: """Replace the text at the given range. Args: @@ -121,6 +121,14 @@ def get_size(self, indent_width: int) -> Size: def line_count(self) -> int: """Returns the number of lines in the document.""" + @overload + def __getitem__(self, line_index: SupportsIndex) -> str: + ... + + @overload + def __getitem__(self, line_index: slice) -> list[str]: + ... + @abstractmethod def __getitem__(self, line_index: SupportsIndex | slice) -> str | list[str]: """Return the content of a line as a string, excluding newline characters. @@ -287,7 +295,7 @@ def get_line_text(self, index: int) -> Text: line_string = line_string.replace("\n", "").replace("\r", "") return Text(line_string, end="") - def __getitem__(self, line_index: SupportsIndex | slice) -> str | list[str]: + def __getitem__(self, line_index: int | slice) -> str | list[str]: """Return the content of a line as a string, excluding newline characters. Args: diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 9aacdbfdd3..963b30a703 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -222,6 +222,7 @@ def _prepare_highlights( return None highlights = self._highlights + highlights.clear() captures_kwargs = {} if start_point is not None: diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 0e560386a2..3c7c0c73e8 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -504,7 +504,7 @@ def gutter_width(self) -> int: ) return gutter_width - def _on_mouse_down(self, event: events.MouseDown) -> None: + async def _on_mouse_down(self, event: events.MouseDown) -> None: """Update the cursor position, and begin a selection using the mouse.""" target = self.get_target_document_location(event) self.selection = Selection.cursor(target) @@ -513,20 +513,20 @@ def _on_mouse_down(self, event: events.MouseDown) -> None: # TextArea widget while selecting, the widget still scrolls. self.capture_mouse() - def _on_mouse_move(self, event: events.MouseMove) -> None: + async def _on_mouse_move(self, event: events.MouseMove) -> None: """Handles click and drag to expand and contract the selection.""" if self._selecting: target = self.get_target_document_location(event) selection_start, _ = self.selection self.selection = Selection(selection_start, target) - def _on_mouse_up(self, event: events.MouseUp) -> None: + async def _on_mouse_up(self, event: events.MouseUp) -> None: """Finalise the selection that has been made using the mouse.""" self._selecting = False self.release_mouse() self.record_cursor_width() - def _on_paste(self, event: events.Paste) -> None: + async def _on_paste(self, event: events.Paste) -> None: """When a paste occurs, insert the text from the paste event into the document.""" text = event.text if text: @@ -649,7 +649,7 @@ def move_cursor_relative( clamp_visitable = self.clamp_visitable start, end = self.selection current_row, current_column = end - target = clamp_visitable(Location(current_row + rows, current_column + columns)) + target = clamp_visitable((current_row + rows, current_column + columns)) self.move_cursor(target, select, center, record_width) @property @@ -1165,9 +1165,7 @@ class Edit: """The end location of the insert""" maintain_selection_offset: bool """If True, the selection will maintain its offset to the replacement range.""" - _replace_result: EditResult | None = field(init=False, default=None) - """Contains data relating to the replace operation.""" - _updated_selection: Location | None = field(init=False, default=None) + _updated_selection: Selection | None = field(init=False, default=None) """Where the selection should move to after the replace happens.""" def do(self, text_area: TextArea) -> EditResult: @@ -1198,7 +1196,6 @@ def do(self, text_area: TextArea) -> EditResult: selection_end_row, selection_end_column = selection_end replace_result = text_area.document.replace_range(edit_from, edit_to, text) - self._replace_result = replace_result new_edit_to_row, new_edit_to_column = replace_result.end_location @@ -1237,6 +1234,7 @@ def undo(self, text_area: TextArea) -> EditResult: Args: text_area: The TextArea to undo the insert operation on. """ + raise NotImplementedError() def after(self, text_area: TextArea) -> None: """Possibly update the cursor location after the widget has been refreshed. @@ -1244,7 +1242,8 @@ def after(self, text_area: TextArea) -> None: Args: text_area: The TextArea this operation was performed on. """ - text_area.selection = self._updated_selection + if self._updated_selection is not None: + text_area.selection = self._updated_selection @runtime_checkable From 99a2547c86fc2ca775d7d14431295da2425d6d4b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 21 Aug 2023 13:36:33 +0100 Subject: [PATCH 219/366] Add tree-sitter-languages stubs and fix typing issues in documents --- poetry.lock | 17 ++++++++++++++++- pyproject.toml | 1 + src/textual/document/_document.py | 12 ++++++++++-- src/textual/document/_syntax_aware_document.py | 6 +++--- src/textual/document/_syntax_theme.py | 10 +++++----- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/poetry.lock b/poetry.lock index e7e39cbc46..df92c6ebbc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1055,6 +1055,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "types-tree-sitter-languages" +version = "1.7.0.1" +description = "Typing stubs for tree-sitter-languages" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +types-tree-sitter = "*" + [[package]] name = "typing-extensions" version = "4.7.1" @@ -1153,7 +1164,7 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "1bdcf7f05a06c9d172aa29ac9b7ee47213a00d101ff3ac1bf61451bebabfe5b9" +content-hash = "89591af5a198994787719c36ca924dac0959f7955fd44c48f1a0abd64dc062b2" [metadata.files] aiohttp = [ @@ -2269,6 +2280,10 @@ types-tree-sitter = [ {file = "types-tree-sitter-0.20.1.4.tar.gz", hash = "sha256:673730dcc2efe09be6cdbd9795cdc5243c164262b7a539e6d7e7980fd06c0907"}, {file = "types_tree_sitter-0.20.1.4-py3-none-any.whl", hash = "sha256:9a38efd62a3cf66f9751c612588b7dbc72340fd6c81fd089c8a0f5877f86b58c"}, ] +types-tree-sitter-languages = [ + {file = "types-tree-sitter-languages-1.7.0.1.tar.gz", hash = "sha256:eadbbfa13f3fcad0711ac8f866cf87692f3c0cfeee72e979a5202b797588d57d"}, + {file = "types_tree_sitter_languages-1.7.0.1-py3-none-any.whl", hash = "sha256:818ec7824ed1bb5bcdbe21022340e0df3930199eb969ea1e08eb03a92440bce2"}, +] typing-extensions = [ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, diff --git a/pyproject.toml b/pyproject.toml index 1833cb4e24..bcb9bbd27b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ textual-dev = "^1.1.0" pytest-asyncio = "*" pytest-textual-snapshot = "0.2.0" # TODO: pinned while while we resolve issue #3042 types-tree-sitter = "^0.20.1.4" +types-tree-sitter-languages = "^1.7.0.1" [tool.black] includes = "src" diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 3827e7fe14..807c7f6d18 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -122,7 +122,7 @@ def line_count(self) -> int: """Returns the number of lines in the document.""" @overload - def __getitem__(self, line_index: SupportsIndex) -> str: + def __getitem__(self, line_index: int) -> str: ... @overload @@ -130,7 +130,7 @@ def __getitem__(self, line_index: slice) -> list[str]: ... @abstractmethod - def __getitem__(self, line_index: SupportsIndex | slice) -> str | list[str]: + def __getitem__(self, line_index: int | slice) -> str | list[str]: """Return the content of a line as a string, excluding newline characters. Args: @@ -295,6 +295,14 @@ def get_line_text(self, index: int) -> Text: line_string = line_string.replace("\n", "").replace("\r", "") return Text(line_string, end="") + @overload + def __getitem__(self, line_index: int) -> str: + ... + + @overload + def __getitem__(self, line_index: slice) -> list[str]: + ... + def __getitem__(self, line_index: int | slice) -> str | list[str]: """Return the content of a line as a string, excluding newline characters. diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 963b30a703..13b77df990 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -260,7 +260,7 @@ def _prepare_highlights( for line_index, updated_highlights in highlight_updates.items(): highlights[line_index] = updated_highlights - def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | None: + def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes: """A callable which informs tree-sitter about the document content. This is passed to tree-sitter which will call it frequently to retrieve @@ -282,7 +282,7 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No row_out_of_bounds = row >= len(lines) if row_out_of_bounds: - return None + return b"" else: row_text = lines[row] @@ -297,7 +297,7 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes | No if newline == "\r\n": return b"\n" else: - return None + return b"" @lru_cache(maxsize=128) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index 1f79c75692..f8ef2621cc 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -4,6 +4,9 @@ from rich.style import Style +_NULL_STYLE = Style.null() + + _MONOKAI = { "string": Style(color="#E6DB74"), "string.documentation": Style(color="#E6DB74"), @@ -33,14 +36,14 @@ "regex.punctuation.bracket": Style(color="#F92672"), "regex.operator": Style(color="#F92672"), "error": Style(color="black", bgcolor="red"), - "json.error": None, + "json.error": _NULL_STYLE, "html.end_tag_error": Style(color="black", bgcolor="red"), "tag": Style(color="#F92672"), "yaml.field": Style(color="#F92672", bold=True), "json.label": Style(color="#F92672", bold=True), "toml.type": Style(color="#F92672"), "toml.datetime": Style(color="#AE81FF"), - "toml.error": None, + "toml.error": _NULL_STYLE, } _BUILTIN_THEMES = { @@ -49,9 +52,6 @@ } -_NULL_STYLE = Style.null() - - @dataclass class SyntaxTheme: """Maps tree-sitter names to Rich styles for syntax-highlighting in `TextArea`. From fb1975cd906786a37e1ab4a3e08c23f7169f7323 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 21 Aug 2023 15:03:47 +0100 Subject: [PATCH 220/366] Fixing remaining typing issues with document --- .../document/_syntax_aware_document.py | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 13b77df990..9b50590934 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -27,9 +27,9 @@ TREE_SITTER_PATH = Path(__file__) / "../../../../tree-sitter/" HIGHLIGHTS_PATH = TREE_SITTER_PATH / "highlights/" -StartColumn = Optional[int] +StartColumn = int EndColumn = Optional[int] -HighlightName = Optional[str] +HighlightName = str Highlight = Tuple[StartColumn, EndColumn, HighlightName] """A tuple representing a syntax highlight within one line.""" @@ -90,7 +90,7 @@ def __init__( self._syntax_theme = SyntaxTheme.get_theme(syntax_theme) self._syntax_theme.highlight_query = highlight_query_path.read_text() - self._syntax_tree = self._parser.parse(self._read_callable) + self._syntax_tree = self._parser.parse(self._read_callable) # type: ignore self._query: Query = self._language.query( self._syntax_theme.highlight_query ) @@ -121,6 +121,8 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult if TREE_SITTER: text_byte_length = len(_utf8_encode(text)) end_location = replace_result.end_location + assert self._syntax_tree is not None + assert self._parser is not None self._syntax_tree.edit( start_byte=start_byte, old_end_byte=old_end_byte, @@ -131,7 +133,7 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult ) # Incrementally parse the document. self._syntax_tree = self._parser.parse( - self._read_callable, self._syntax_tree + self._read_callable, self._syntax_tree # type: ignore[arg-type] ) self._prepare_highlights() @@ -146,19 +148,23 @@ def get_line_text(self, line_index: int) -> Text: Returns: The syntax highlighted Text of the line. """ - line_bytes = _utf8_encode(self[line_index]) - byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) - line = Text(self[line_index], end="") + line_string = self[line_index] + line = Text(line_string, end="") + if not TREE_SITTER or self._syntax_theme is None: + return line + highlights = self._highlights - get_highlight_from_theme = self._syntax_theme.get_highlight if highlights: - highlights = highlights[line_index] - for start, end, highlight_name in highlights: + line_bytes = _utf8_encode(line_string) + byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) + get_highlight_from_theme = self._syntax_theme.get_highlight + line_highlights = highlights[line_index] + for start, end, highlight_name in line_highlights: node_style = get_highlight_from_theme(highlight_name) line.stylize( node_style, byte_to_codepoint.get(start, 0), - byte_to_codepoint.get(end), + byte_to_codepoint.get(end) if end else None, ) return line @@ -191,7 +197,8 @@ def _location_to_byte_offset(self, location: Location) -> int: def _location_to_point(self, location: Location) -> tuple[int, int]: """Convert a document location (row_index, column_index) to a tree-sitter - point (row_index, byte_offset_from_start_of_row). + point (row_index, byte_offset_from_start_of_row). If tree-sitter isn't available + returns (0, 0). Args: location: A location (row index, column codepoint offset) @@ -199,6 +206,9 @@ def _location_to_point(self, location: Location) -> tuple[int, int]: Returns: The point corresponding to that location (row index, column byte offset). """ + if not TREE_SITTER: + return 0, 0 + lines = self._lines row, column = location if row < len(lines): @@ -210,7 +220,7 @@ def _location_to_point(self, location: Location) -> tuple[int, int]: def _prepare_highlights( self, start_point: tuple[int, int] | None = None, - end_point: tuple[int, int] = None, + end_point: tuple[int, int] | None = None, ) -> None: """Query the tree for ranges to highlights, and update the internal highlights mapping. @@ -221,6 +231,8 @@ def _prepare_highlights( if not TREE_SITTER: return None + assert self._syntax_tree is not None + highlights = self._highlights highlights.clear() @@ -296,8 +308,8 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes: elif column == encoded_row_length + 1: if newline == "\r\n": return b"\n" - else: - return b"" + + return b"" @lru_cache(maxsize=128) From 5eedf623a11ea5bee1c55c7bd75c31c696922c8b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 21 Aug 2023 15:29:03 +0100 Subject: [PATCH 221/366] Updating Syntax themes --- src/textual/document/_syntax_theme.py | 7 +- .../__snapshots__/test_snapshots.ambr | 1393 ++++++++--------- tests/snapshot_tests/test_snapshots.py | 1 - tree-sitter/highlights/toml.scm | 2 +- 4 files changed, 700 insertions(+), 703 deletions(-) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index f8ef2621cc..679c6b2b94 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -12,6 +12,7 @@ "string.documentation": Style(color="#E6DB74"), "comment": Style(color="#75715E"), "keyword": Style(color="#F92672"), + "operator": Style(color="#F92672"), "repeat": Style(color="#F92672"), "exception": Style(color="#F92672"), "include": Style(color="#F92672"), @@ -28,14 +29,14 @@ "boolean": Style(color="#66D9EF", italic=True), "json.null": Style(color="#66D9EF", italic=True), # "constant": Style(color="#AE81FF"), - "variable": Style(color="white"), - "parameter": Style(color="cyan"), + # "variable": Style(color="white"), + # "parameter": Style(color="cyan"), # "type": Style(color="cyan"), # "escape": Style(bgcolor="magenta"), "heading": Style(color="#F92672", bold=True), "regex.punctuation.bracket": Style(color="#F92672"), "regex.operator": Style(color="#F92672"), - "error": Style(color="black", bgcolor="red"), + # "error": Style(color="black", bgcolor="red"), "json.error": _NULL_STYLE, "html.end_tag_error": Style(color="black", bgcolor="red"), "tag": Style(color="#F92672"), diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 73381b46f1..8045d88494 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -27500,319 +27500,319 @@ font-weight: 700; } - .terminal-1142773784-matrix { + .terminal-3624001113-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1142773784-title { + .terminal-3624001113-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1142773784-r1 { fill: #e4e5e6 } - .terminal-1142773784-r2 { fill: #1b1b1b } - .terminal-1142773784-r3 { fill: #75715e } - .terminal-1142773784-r4 { fill: #c5c8c6 } - .terminal-1142773784-r5 { fill: #7b7e82 } - .terminal-1142773784-r6 { fill: #e2e3e3 } - .terminal-1142773784-r7 { fill: #e6db74 } - .terminal-1142773784-r8 { fill: #ae81ff } - .terminal-1142773784-r9 { fill: #f92672 } - .terminal-1142773784-r10 { fill: #a6e22e } + .terminal-3624001113-r1 { fill: #e4e5e6 } + .terminal-3624001113-r2 { fill: #1b1b1b } + .terminal-3624001113-r3 { fill: #75715e } + .terminal-3624001113-r4 { fill: #c5c8c6 } + .terminal-3624001113-r5 { fill: #7b7e82 } + .terminal-3624001113-r6 { fill: #e2e3e3 } + .terminal-3624001113-r7 { fill: #e6db74 } + .terminal-3624001113-r8 { fill: #ae81ff } + .terminal-3624001113-r9 { fill: #f92672 } + .terminal-3624001113-r10 { fill: #a6e22e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  /* This is a comment in CSS */ -  2   -  3  /* Basic selectors and properties */ -  4  body {                                 -  5      font-family: Arial, sans-serif;    -  6      background-color: #f4f4f4;         -  7      margin: 0;                         -  8      padding: 0;                        -  9  }                                      - 10   - 11  /* Class and ID selectors */ - 12  .header {                              - 13      background-color: #333;            - 14      color: #fff;                       - 15      padding: 10px0;                   - 16      text-align: center;                - 17  }                                      - 18   - 19  #logo {                                - 20      font-size: 24px;                   - 21      font-weight: bold;                 - 22  }                                      - 23   - 24  /* Descendant and child selectors */ - 25  .nav ul {                              - 26      list-style-type: none;             - 27      padding: 0;                        - 28  }                                      - 29   - 30  .nav > li {                            - 31      display: inline-block;             - 32      margin-right: 10px;                - 33  }                                      - 34   - 35  /* Pseudo-classes */ - 36  a:hover {                              - 37      text-decoration: underline;        - 38  }                                      - 39   - 40  input:focus {                          - 41      border-color: #007BFF;             - 42  }                                      - 43   - 44  /* Media query */ - 45  @media (max-width: 768px) {            - 46      body {                             - 47          font-size: 16px;               - 48      }                                  - 49   - 50      .header {                          - 51          padding: 5px0;                - 52      }                                  - 53  }                                      - 54   - 55  /* Keyframes animation */ - 56  @keyframes slideIn {                   - 57  from {                             - 58          transform: translateX(-100%);  - 59      }                                  - 60  to {                               - 61          transform: translateX(0);      - 62      }                                  - 63  }                                      - 64   - 65  .slide-in-element {                    - 66      animation: slideIn 0.5s forwards;  - 67  }                                      - 68   + + + +  1  /* This is a comment in CSS */ +  2   +  3  /* Basic selectors and properties */ +  4  body {                                 +  5      font-family: Arial, sans-serif;    +  6      background-color: #f4f4f4;         +  7      margin: 0;                         +  8      padding: 0;                        +  9  }                                      + 10   + 11  /* Class and ID selectors */ + 12  .header {                              + 13      background-color: #333;            + 14      color: #fff;                       + 15      padding: 10px0;                   + 16      text-align: center;                + 17  }                                      + 18   + 19  #logo {                                + 20      font-size: 24px;                   + 21      font-weight: bold;                 + 22  }                                      + 23   + 24  /* Descendant and child selectors */ + 25  .nav ul {                              + 26      list-style-type: none;             + 27      padding: 0;                        + 28  }                                      + 29   + 30  .nav > li {                            + 31      display: inline-block;             + 32      margin-right: 10px;                + 33  }                                      + 34   + 35  /* Pseudo-classes */ + 36  a:hover {                              + 37      text-decoration: underline;        + 38  }                                      + 39   + 40  input:focus {                          + 41      border-color: #007BFF;             + 42  }                                      + 43   + 44  /* Media query */ + 45  @media (max-width: 768px) {            + 46      body {                             + 47          font-size: 16px;               + 48      }                                  + 49   + 50      .header {                          + 51          padding: 5px0;                + 52      }                                  + 53  }                                      + 54   + 55  /* Keyframes animation */ + 56  @keyframes slideIn {                   + 57  from {                             + 58          transform: translateX(-100%);  + 59      }                                  + 60  to {                               + 61          transform: translateX(0);      + 62      }                                  + 63  }                                      + 64   + 65  .slide-in-element {                    + 66      animation: slideIn 0.5s forwards;  + 67  }                                      + 68   @@ -27843,273 +27843,273 @@ font-weight: 700; } - .terminal-2642746815-matrix { + .terminal-2423874257-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2642746815-title { + .terminal-2423874257-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2642746815-r1 { fill: #e4e5e6 } - .terminal-2642746815-r2 { fill: #1b1b1b } - .terminal-2642746815-r3 { fill: #c5c8c6 } - .terminal-2642746815-r4 { fill: #7b7e82 } - .terminal-2642746815-r5 { fill: #e2e3e3 } - .terminal-2642746815-r6 { fill: #f92672 } - .terminal-2642746815-r7 { fill: #e6db74 } - .terminal-2642746815-r8 { fill: #75715e } + .terminal-2423874257-r1 { fill: #e4e5e6 } + .terminal-2423874257-r2 { fill: #1b1b1b } + .terminal-2423874257-r3 { fill: #c5c8c6 } + .terminal-2423874257-r4 { fill: #7b7e82 } + .terminal-2423874257-r5 { fill: #e2e3e3 } + .terminal-2423874257-r6 { fill: #f92672 } + .terminal-2423874257-r7 { fill: #e6db74 } + .terminal-2423874257-r8 { fill: #75715e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  <!DOCTYPE html>                                                              -  2  <html lang="en">                                                            -  3   -  4  <head>                                                                      -  5  <!-- Meta tags --> -  6      <meta charset="UTF-8">                                                  -  7      <meta name="viewport" content="width=device-width, initial-scale=1.0" -  8  <!-- Title --> -  9      <title>HTML Test Page</title>                                           - 10  <!-- Link to CSS --> - 11      <link rel="stylesheet" href="styles.css">                               - 12  </head>                                                                     - 13   - 14  <body>                                                                      - 15  <!-- Header section --> - 16      <header class="header">                                                 - 17          <h1 id="logo">HTML Test Page</h1>                                   - 18      </header>                                                               - 19   - 20  <!-- Navigation --> - 21      <nav class="nav">                                                       - 22          <ul>                                                                - 23              <li><a href="#">Home</a></li>                                   - 24              <li><a href="#">About</a></li>                                  - 25              <li><a href="#">Contact</a></li>                                - 26          </ul>                                                               - 27      </nav>                                                                  - 28   - 29  <!-- Main content area --> - 30      <main>                                                                  - 31          <article>                                                           - 32              <h2>Welcome to the Test Page</h2>                               - 33              <p>This is a paragraph to test the HTML structure.</p>          - 34              <img src="test-image.jpg" alt="Test Image" width="300">         - 35          </article>                                                          - 36      </main>                                                                 - 37   - 38  <!-- Form --> - 39      <section>                                                               - 40          <form action="/submit" method="post">                               - 41              <label for="name">Name:</label>                                 - 42              <input type="text" id="name" name="name">                       - 43              <input type="submit" value="Submit">                            - 44          </form>                                                             - 45      </section>                                                              - 46   - 47  <!-- Footer --> - 48      <footer>                                                                - 49          <p>&copy; 2023 HTML Test Page</p>                                   - 50      </footer>                                                               - 51   - 52  <!-- Script tag --> - 53      <script src="scripts.js"></script>                                      - 54  </body>                                                                     - 55   - 56  </html>                                                                     - 57   + + + +  1  <!DOCTYPE html>                                                              +  2  <html lang="en">                                                            +  3   +  4  <head>                                                                      +  5  <!-- Meta tags --> +  6      <meta charset="UTF-8">                                                  +  7      <meta name="viewport" content="width=device-width, initial-scale=1.0" +  8  <!-- Title --> +  9      <title>HTML Test Page</title>                                           + 10  <!-- Link to CSS --> + 11      <link rel="stylesheet" href="styles.css">                               + 12  </head>                                                                     + 13   + 14  <body>                                                                      + 15  <!-- Header section --> + 16      <header class="header">                                                 + 17          <h1 id="logo">HTML Test Page</h1>                                   + 18      </header>                                                               + 19   + 20  <!-- Navigation --> + 21      <nav class="nav">                                                       + 22          <ul>                                                                + 23              <li><a href="#">Home</a></li>                                   + 24              <li><a href="#">About</a></li>                                  + 25              <li><a href="#">Contact</a></li>                                + 26          </ul>                                                               + 27      </nav>                                                                  + 28   + 29  <!-- Main content area --> + 30      <main>                                                                  + 31          <article>                                                           + 32              <h2>Welcome to the Test Page</h2>                               + 33              <p>This is a paragraph to test the HTML structure.</p>          + 34              <img src="test-image.jpg" alt="Test Image" width="300">         + 35          </article>                                                          + 36      </main>                                                                 + 37   + 38  <!-- Form --> + 39      <section>                                                               + 40          <form action="/submit" method="post">                               + 41              <label for="name">Name:</label>                                 + 42              <input type="text" id="name" name="name">                       + 43              <input type="submit" value="Submit">                            + 44          </form>                                                             + 45      </section>                                                              + 46   + 47  <!-- Footer --> + 48      <footer>                                                                + 49          <p>&copy; 2023 HTML Test Page</p>                                   + 50      </footer>                                                               + 51   + 52  <!-- Script tag --> + 53      <script src="scripts.js"></script>                                      + 54  </body>                                                                     + 55   + 56  </html>                                                                     + 57   @@ -28675,364 +28675,363 @@ font-weight: 700; } - .terminal-1580289934-matrix { + .terminal-2259385514-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1580289934-title { + .terminal-2259385514-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1580289934-r1 { fill: #e4e5e6 } - .terminal-1580289934-r2 { fill: #1b1b1b } - .terminal-1580289934-r3 { fill: #f92672 } - .terminal-1580289934-r4 { fill: #c5c8c6 } - .terminal-1580289934-r5 { fill: #7b7e82 } - .terminal-1580289934-r6 { fill: #e2e3e3 } - .terminal-1580289934-r7 { fill: #75715e } - .terminal-1580289934-r8 { fill: #e6db74 } - .terminal-1580289934-r9 { fill: #ae81ff } - .terminal-1580289934-r10 { fill: #a6e22e } - .terminal-1580289934-r11 { fill: #68a0b3 } + .terminal-2259385514-r1 { fill: #e4e5e6 } + .terminal-2259385514-r2 { fill: #1b1b1b } + .terminal-2259385514-r3 { fill: #f92672 } + .terminal-2259385514-r4 { fill: #c5c8c6 } + .terminal-2259385514-r5 { fill: #7b7e82 } + .terminal-2259385514-r6 { fill: #e2e3e3 } + .terminal-2259385514-r7 { fill: #75715e } + .terminal-2259385514-r8 { fill: #e6db74 } + .terminal-2259385514-r9 { fill: #ae81ff } + .terminal-2259385514-r10 { fill: #a6e22e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  importmath -  2  fromosimportpath -  3   -  4  # I'm a comment :) -  5   -  6  string_var = "Hello, world!" -  7  int_var = 42 -  8  float_var = 3.14 -  9  complex_var = 1 + 2j - 10   - 11  list_var = [12345]                                                   - 12  tuple_var = (12345)                                                  - 13  set_var = {12345}                                                    - 14  dict_var = {"a"1"b"2"c"3}                                          - 15   - 16  deffunction_no_args():                                                      - 17  return"No arguments" - 18   - 19  deffunction_with_args(ab):                                                - 20  returna + b - 21   - 22  deffunction_with_default_args(a=0b=0):                                    - 23  returna * b - 24   - 25  lambda_func = lambdaxx**2 - 26   - 27  ifint_var == 42:                                                            - 28  print("It's the answer!")                                                - 29  elifint_var < 42:                                                           - 30  print("Less than the answer.")                                           - 31  else:                                                                        - 32  print("Greater than the answer.")                                        - 33   - 34  forindexvalue in enumerate(list_var):                                     - 35  print(f"Index: {index}, Value: {value}")                                 - 36   - 37  counter = 0 - 38  whilecounter < 5:                                                           - 39  print(f"Counter value: {counter}")                                       - 40  counter += 1 - 41   - 42  squared_numbers = [x**2forx in range(10ifx % 2 == 0]                    - 43   - 44  try:                                                                         - 45  result = 10 / 0 - 46  exceptZeroDivisionError:                                                    - 47  print("Cannot divide by zero!")                                          - 48  finally:                                                                     - 49  print("End of try-except block.")                                        - 50   - 51  classAnimal:                                                                - 52  def__init__(selfname):                                                - 53  self.name = name - 54   - 55  defspeak(self):                                                         - 56  raiseNotImplementedError("Subclasses must implement this method." - 57   - 58  classDog(Animal):                                                           - 59  defspeak(self):                                                         - 60  returnf"{self.name} says Woof!" - 61   - 62  deffibonacci(n):                                                            - 63  ab = 01 - 64  for_ in range(n):                                                       - 65  yielda - 66  ab = ba + b - 67   - 68  fornum in fibonacci(5):                                                     - 69  print(num)                                                               - 70   - 71  withopen('test.txt''w'asf:                                             - 72  f.write("Testing with statement.")                                       - 73   - 74  @my_decorator - 75  defsay_hello():                                                             - 76  print("Hello!")                                                          - 77   - 78  say_hello()                                                                  - 79   + + + +  1  import math                                                                  +  2  from os import path                                                          +  3   +  4  # I'm a comment :) +  5   +  6  string_var ="Hello, world!" +  7  int_var =42 +  8  float_var =3.14 +  9  complex_var =1+2j + 10   + 11  list_var = [12345]                                                   + 12  tuple_var = (12345)                                                  + 13  set_var = {12345}                                                    + 14  dict_var = {"a"1"b"2"c"3}                                          + 15   + 16  deffunction_no_args():                                                      + 17  return"No arguments" + 18   + 19  deffunction_with_args(a, b):                                                + 20  return a + b                                                             + 21   + 22  deffunction_with_default_args(a=0, b=0):                                    + 23  return a * b                                                             + 24   + 25  lambda_func =lambda x: x**2 + 26   + 27  if int_var ==42:                                                            + 28  print("It's the answer!")                                                + 29  elif int_var <42:                                                           + 30  print("Less than the answer.")                                           + 31  else:                                                                        + 32  print("Greater than the answer.")                                        + 33   + 34  for index, value in enumerate(list_var):                                     + 35  print(f"Index: {index}, Value: {value}")                                 + 36   + 37  counter =0 + 38  while counter <5:                                                           + 39  print(f"Counter value: {counter}")                                       + 40      counter +=1 + 41   + 42  squared_numbers = [x**2for x in range(10if x %2==0]                    + 43   + 44  try:                                                                         + 45      result =10/0 + 46  except ZeroDivisionError:                                                    + 47  print("Cannot divide by zero!")                                          + 48  finally:                                                                     + 49  print("End of try-except block.")                                        + 50   + 51  class Animal:                                                                + 52  def__init__(self, name):                                                + 53          self.name = name                                                     + 54   + 55  defspeak(self):                                                         + 56  raiseNotImplementedError("Subclasses must implement this method." + 57   + 58  class Dog(Animal):                                                           + 59  defspeak(self):                                                         + 60  returnf"{self.name} says Woof!" + 61   + 62  deffibonacci(n):                                                            + 63      a, b =01 + 64  for _ in range(n):                                                       + 65  yield a                                                              + 66          a, b = b, a + b                                                      + 67   + 68  for num in fibonacci(5):                                                     + 69  print(num)                                                               + 70   + 71  withopen('test.txt''w'as f:                                             + 72      f.write("Testing with statement.")                                       + 73   + 74  @my_decorator                                                                + 75  defsay_hello():                                                             + 76  print("Hello!")                                                          + 77   + 78  say_hello()                                                                  + 79   @@ -29231,222 +29230,222 @@ font-weight: 700; } - .terminal-3443060286-matrix { + .terminal-201148347-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3443060286-title { + .terminal-201148347-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3443060286-r1 { fill: #e4e5e6 } - .terminal-3443060286-r2 { fill: #1b1b1b } - .terminal-3443060286-r3 { fill: #75715e } - .terminal-3443060286-r4 { fill: #c5c8c6 } - .terminal-3443060286-r5 { fill: #7b7e82 } - .terminal-3443060286-r6 { fill: #e2e3e3 } - .terminal-3443060286-r7 { fill: #f92672 } - .terminal-3443060286-r8 { fill: #ae81ff } - .terminal-3443060286-r9 { fill: #e6db74 } + .terminal-201148347-r1 { fill: #e4e5e6 } + .terminal-201148347-r2 { fill: #1b1b1b } + .terminal-201148347-r3 { fill: #75715e } + .terminal-201148347-r4 { fill: #c5c8c6 } + .terminal-201148347-r5 { fill: #7b7e82 } + .terminal-201148347-r6 { fill: #e2e3e3 } + .terminal-201148347-r7 { fill: #f92672 } + .terminal-201148347-r8 { fill: #ae81ff } + .terminal-201148347-r9 { fill: #e6db74 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  -- This is a comment in SQL -  2   -  3  -- Create tables -  4  CREATETABLEAuthors (                                                       -  5  AuthorIDINTPRIMARY KEY,                                                -  6  NameVARCHAR(255NOT NULL,                                              -  7  CountryVARCHAR(50)                                                      -  8  );                                                                           -  9   - 10  CREATETABLEBooks (                                                         - 11  BookIDINTPRIMARY KEY,                                                  - 12  TitleVARCHAR(255NOT NULL,                                             - 13  AuthorIDINT,                                                            - 14  PublishedDateDATE,                                                      - 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)                      - 16  );                                                                           - 17   - 18  -- Insert data - 19  INSERTINTOAuthors (AuthorIDNameCountryVALUES (1'George Orwell''U - 20   - 21  INSERTINTOBooks (BookIDTitleAuthorIDPublishedDateVALUES (1'1984' - 22   - 23  -- Update data - 24  UPDATEAuthorsSETCountry = 'United Kingdom'WHERECountry = 'UK';          - 25   - 26  -- Select data with JOIN - 27  SELECTBooks.TitleAuthors.Name - 28  FROMBooks - 29  JOINAuthorsONBooks.AuthorID = Authors.AuthorID;                           - 30   - 31  -- Delete data (commented to preserve data for other examples) - 32  -- DELETE FROM Books WHERE BookID = 1; - 33   - 34  -- Alter table structure - 35  ALTER TABLEAuthors ADD COLUMN BirthDateDATE;                               - 36   - 37  -- Create index - 38  CREATEINDEXidx_author_nameONAuthors(Name);                               - 39   - 40  -- Drop index (commented to avoid actually dropping it) - 41  -- DROP INDEX idx_author_name ON Authors; - 42   - 43  -- End of script - 44   + + + +  1  -- This is a comment in SQL +  2   +  3  -- Create tables +  4  CREATETABLE Authors (                                                       +  5      AuthorID INT PRIMARY KEY,                                                +  6      Name VARCHAR(255NOT NULL,                                              +  7      Country VARCHAR(50)                                                      +  8  );                                                                           +  9   + 10  CREATETABLE Books (                                                         + 11      BookID INT PRIMARY KEY,                                                  + 12      Title VARCHAR(255NOT NULL,                                             + 13      AuthorID INT,                                                            + 14      PublishedDate DATE,                                                      + 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)                      + 16  );                                                                           + 17   + 18  -- Insert data + 19  INSERTINTO Authors (AuthorID, Name, Country) VALUES (1'George Orwell''U + 20   + 21  INSERTINTO Books (BookID, Title, AuthorID, PublishedDate) VALUES (1'1984' + 22   + 23  -- Update data + 24  UPDATE Authors SET Country ='United Kingdom'WHERE Country ='UK';          + 25   + 26  -- Select data with JOIN + 27  SELECT Books.Title, Authors.Name                                             + 28  FROM Books                                                                   + 29  JOIN Authors ON Books.AuthorID = Authors.AuthorID;                           + 30   + 31  -- Delete data (commented to preserve data for other examples) + 32  -- DELETE FROM Books WHERE BookID = 1; + 33   + 34  -- Alter table structure + 35  ALTER TABLE Authors ADD COLUMN BirthDate DATE;                               + 36   + 37  -- Create index + 38  CREATEINDEX idx_author_name ON Authors(Name);                               + 39   + 40  -- Drop index (commented to avoid actually dropping it) + 41  -- DROP INDEX idx_author_name ON Authors; + 42   + 43  -- End of script + 44   @@ -29875,61 +29874,61 @@ font-weight: 700; } - .terminal-3303661732-matrix { + .terminal-1515009057-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3303661732-title { + .terminal-1515009057-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3303661732-r1 { fill: #7b7e82 } - .terminal-3303661732-r2 { fill: #c5c8c6 } - .terminal-3303661732-r3 { fill: #4b4e55 } - .terminal-3303661732-r4 { fill: #e2e3e3 } - .terminal-3303661732-r5 { fill: #004578 } - .terminal-3303661732-r6 { fill: #e4e5e6 } - .terminal-3303661732-r7 { fill: #1b1b1b } + .terminal-1515009057-r1 { fill: #7b7e82 } + .terminal-1515009057-r2 { fill: #dde6ed } + .terminal-1515009057-r3 { fill: #e2e3e3 } + .terminal-1515009057-r4 { fill: #c5c8c6 } + .terminal-1515009057-r5 { fill: #004578 } + .terminal-1515009057-r6 { fill: #e4e5e6 } + .terminal-1515009057-r7 { fill: #1b1b1b } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline. - 2   - 3  Iamanotherline. - 4   - 5  Iamthefinalline. + + + + 1  I am a line. + 2   + 3  I am another line.          + 4   + 5  I am the final line.  @@ -29959,61 +29958,61 @@ font-weight: 700; } - .terminal-3329921234-matrix { + .terminal-3458068619-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3329921234-title { + .terminal-3458068619-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3329921234-r1 { fill: #e4e5e6 } - .terminal-3329921234-r2 { fill: #1b1b1b } - .terminal-3329921234-r3 { fill: #4b4e55 } - .terminal-3329921234-r4 { fill: #c5c8c6 } - .terminal-3329921234-r5 { fill: #7b7e82 } - .terminal-3329921234-r6 { fill: #004578 } - .terminal-3329921234-r7 { fill: #e2e3e3 } + .terminal-3458068619-r1 { fill: #e4e5e6 } + .terminal-3458068619-r2 { fill: #1b1b1b } + .terminal-3458068619-r3 { fill: #dde6ed } + .terminal-3458068619-r4 { fill: #c5c8c6 } + .terminal-3458068619-r5 { fill: #7b7e82 } + .terminal-3458068619-r6 { fill: #004578 } + .terminal-3458068619-r7 { fill: #e2e3e3 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline. - 2   - 3  Iamanotherline. - 4   - 5  Iamthefinalline. + + + + 1  I am a line. + 2   + 3  I am another line.    + 4   + 5  I am the final line.  @@ -30043,61 +30042,61 @@ font-weight: 700; } - .terminal-331256201-matrix { + .terminal-1823421656-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-331256201-title { + .terminal-1823421656-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-331256201-r1 { fill: #e4e5e6 } - .terminal-331256201-r2 { fill: #1b1b1b } - .terminal-331256201-r3 { fill: #4b4e55 } - .terminal-331256201-r4 { fill: #c5c8c6 } - .terminal-331256201-r5 { fill: #7b7e82 } - .terminal-331256201-r6 { fill: #004578 } - .terminal-331256201-r7 { fill: #e2e3e3 } + .terminal-1823421656-r1 { fill: #e4e5e6 } + .terminal-1823421656-r2 { fill: #1b1b1b } + .terminal-1823421656-r3 { fill: #dde6ed } + .terminal-1823421656-r4 { fill: #c5c8c6 } + .terminal-1823421656-r5 { fill: #7b7e82 } + .terminal-1823421656-r6 { fill: #004578 } + .terminal-1823421656-r7 { fill: #e2e3e3 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline. - 2   - 3  Iamanotherline. - 4   - 5  Iamthefinalline. + + + + 1  I am a line. + 2   + 3  I am another line. + 4   + 5  I am the final line.  @@ -30127,61 +30126,61 @@ font-weight: 700; } - .terminal-2227222898-matrix { + .terminal-4159010911-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2227222898-title { + .terminal-4159010911-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2227222898-r1 { fill: #7b7e82 } - .terminal-2227222898-r2 { fill: #c5c8c6 } - .terminal-2227222898-r3 { fill: #4b4e55 } - .terminal-2227222898-r4 { fill: #e2e3e3 } - .terminal-2227222898-r5 { fill: #004578 } - .terminal-2227222898-r6 { fill: #e4e5e6 } - .terminal-2227222898-r7 { fill: #1b1b1b } + .terminal-4159010911-r1 { fill: #7b7e82 } + .terminal-4159010911-r2 { fill: #dde6ed } + .terminal-4159010911-r3 { fill: #e2e3e3 } + .terminal-4159010911-r4 { fill: #c5c8c6 } + .terminal-4159010911-r5 { fill: #004578 } + .terminal-4159010911-r6 { fill: #e4e5e6 } + .terminal-4159010911-r7 { fill: #1b1b1b } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline. - 2   - 3  Iamanotherline. - 4   - 5  Iamthefinalline. + + + + 1  I am a line. + 2   + 3  I am another line. + 4   + 5  I am the final line. @@ -30211,60 +30210,59 @@ font-weight: 700; } - .terminal-2147792900-matrix { + .terminal-2537073741-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2147792900-title { + .terminal-2537073741-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2147792900-r1 { fill: #7b7e82 } - .terminal-2147792900-r2 { fill: #c5c8c6 } - .terminal-2147792900-r3 { fill: #4b4e55 } - .terminal-2147792900-r4 { fill: #e2e3e3 } - .terminal-2147792900-r5 { fill: #e4e5e6 } - .terminal-2147792900-r6 { fill: #1b1b1b } + .terminal-2537073741-r1 { fill: #7b7e82 } + .terminal-2537073741-r2 { fill: #e2e3e3 } + .terminal-2537073741-r3 { fill: #c5c8c6 } + .terminal-2537073741-r4 { fill: #e4e5e6 } + .terminal-2537073741-r5 { fill: #1b1b1b } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline. - 2   - 3  Iamanotherline. - 4   - 5  Iamthefinalline. + + + + 1  I am a line.          + 2   + 3  I am another line.    + 4   + 5  I am the final line.  @@ -30294,60 +30292,59 @@ font-weight: 700; } - .terminal-4132495341-matrix { + .terminal-3397495885-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4132495341-title { + .terminal-3397495885-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4132495341-r1 { fill: #7b7e82 } - .terminal-4132495341-r2 { fill: #c5c8c6 } - .terminal-4132495341-r3 { fill: #4b4e55 } - .terminal-4132495341-r4 { fill: #e2e3e3 } - .terminal-4132495341-r5 { fill: #e4e5e6 } - .terminal-4132495341-r6 { fill: #1b1b1b } + .terminal-3397495885-r1 { fill: #7b7e82 } + .terminal-3397495885-r2 { fill: #e2e3e3 } + .terminal-3397495885-r3 { fill: #c5c8c6 } + .terminal-3397495885-r4 { fill: #e4e5e6 } + .terminal-3397495885-r5 { fill: #1b1b1b } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  Iamaline. - 2   - 3  Iamanotherline. - 4   - 5  Iamthefinalline. + + + + 1  I am a line.          + 2   + 3  I am another line.          + 4   + 5  I am the final line.  diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 8b8299f587..4c828e2149 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -675,7 +675,6 @@ def test_text_area_selection_rendering(snap_compare, selection): def setup_selection(pilot): text_area = pilot.app.query_one(TextArea) text_area.load_text(text) - text_area.language = "python" text_area.selection = selection assert snap_compare( diff --git a/tree-sitter/highlights/toml.scm b/tree-sitter/highlights/toml.scm index 21e0d5b95b..9228d28072 100644 --- a/tree-sitter/highlights/toml.scm +++ b/tree-sitter/highlights/toml.scm @@ -24,7 +24,7 @@ "." @punctuation.delimiter "," @punctuation.delimiter -"=" @operator +"=" @toml.operator "[" @punctuation.bracket "]" @punctuation.bracket From 635308f9182b264d67c998ab12aaa95aaba631a9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 21 Aug 2023 17:14:32 +0100 Subject: [PATCH 222/366] Improve highlighting, add initial TextArea docs page --- docs/examples/widgets/text_area.py | 39 ++ docs/widgets/_template.md | 1 + docs/widgets/text_area.md | 77 ++++ mkdocs-nav.yml | 1 + src/textual/document/_syntax_theme.py | 1 + .../__snapshots__/test_snapshots.ambr | 350 +++++++++--------- tree-sitter/highlights/python.scm | 1 - 7 files changed, 294 insertions(+), 176 deletions(-) create mode 100644 docs/examples/widgets/text_area.py create mode 100644 docs/widgets/text_area.md diff --git a/docs/examples/widgets/text_area.py b/docs/examples/widgets/text_area.py new file mode 100644 index 0000000000..90f6f2fe4d --- /dev/null +++ b/docs/examples/widgets/text_area.py @@ -0,0 +1,39 @@ +from textual.app import App, ComposeResult +from textual.document import Selection +from textual.widgets import TextArea + +TEXT = """\ +def shrink(self, margin: tuple[int, int, int, int]) -> Region: + '''Shrink a region by subtracting spacing. + + Args: + margin: Shrink space by `(, , , )`. + + Returns: + The new, smaller region. + ''' + if not any(margin): + return self + top, right, bottom, left = margin + x, y, width, height = self + return Region( + x=x + left, + y=y + top, + width=max(0, width - (left + right)), + height=max(0, height - (top + bottom)), + ) +""" + + +class TextAreaExample(App): + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.load_text(TEXT) + text_area.language = "python" + text_area.selection = Selection((0, 0), (3, 7)) + yield text_area + + +app = TextAreaExample() +if __name__ == "__main__": + app.run() diff --git a/docs/widgets/_template.md b/docs/widgets/_template.md index 519173aa26..45cee1b3e9 100644 --- a/docs/widgets/_template.md +++ b/docs/widgets/_template.md @@ -32,6 +32,7 @@ Example app showing the widget: ## Reactive attributes +## Messages ## Bindings diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md new file mode 100644 index 0000000000..dad79c5b8b --- /dev/null +++ b/docs/widgets/text_area.md @@ -0,0 +1,77 @@ +# TextArea + +!!! tip "Added in version 0.34.0" + +A widget for editing text which may span multiple lines. + +- [x] Focusable +- [ ] Container + + +## Example + +Here's an example app which loads some Python code, sets the syntax highlighting language +to Python, and selects some text. + +=== "Output" + + ```{.textual path="docs/examples/widgets/text_area.py"} + ``` + +=== "text_area_example.py" + + ```python + --8<-- "docs/examples/widgets/text_area.py" + ``` + + +## Reactive attributes + +| Name | Type | Default | Description | +|---------------------|---------------------------|-------------------------|---------------------------------------------------| +| `language` | `str \| Language \| None` | `None` | The language to use for syntax highlighting. | +| `theme` | `str \| SyntaxTheme` | `SyntaxTheme.default()` | The theme to use for syntax highlighting. | +| `selection` | `Selection` | `Selection()` | The current selection. | +| `show_line_numbers` | `bool` | `True` | Show or hide line numbers. | +| `indent_width` | `int` | `4` | The number of spaces to indent and width of tabs. | + +## Bindings + +The `TextArea` widget defines the following bindings: + +::: textual.widgets._text_area.TextArea.BINDINGS + options: + show_root_heading: false + show_root_toc_entry: false + + +## Component classes + +The `TextArea` widget provides the following component classes: + +::: textual.widgets._text_area.TextArea.COMPONENT_CLASSES + options: + show_root_heading: false + show_root_toc_entry: false + + +## Additional notes + +### Tab characters + +The character(s) inserted when you press tab is controlled by setting the `indent_type` attribute to either `tabs` or `spaces`. + +### Python 3.7 is not supported + +Syntax highlighting is not available on Python 3.7. Highlighting will fail _silently_, so end-users who are running Python 3.7 can still edit text without highlighting, even if a `language` and `syntax_theme` is set. + +## See also + +- [`Input`][textual.widgets.Input] - for single-line text input. + +--- + + +::: textual.widgets.TextArea + options: + heading_level: 2 diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index adacbb6723..6bab8c2d96 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -160,6 +160,7 @@ nav: - "widgets/switch.md" - "widgets/tabbed_content.md" - "widgets/tabs.md" + - "widgets/text_area.md" - "widgets/tree.md" - API: - "api/index.md" diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index 679c6b2b94..d9be0c0d99 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -18,6 +18,7 @@ "include": Style(color="#F92672"), "keyword.function": Style(color="#F92672"), "keyword.return": Style(color="#F92672"), + "keyword.operator": Style(color="#F92672"), "conditional": Style(color="#F92672"), "number": Style(color="#AE81FF"), "float": Style(color="#AE81FF"), diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 8045d88494..3ced472739 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -28675,363 +28675,363 @@ font-weight: 700; } - .terminal-2259385514-matrix { + .terminal-685151692-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2259385514-title { + .terminal-685151692-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2259385514-r1 { fill: #e4e5e6 } - .terminal-2259385514-r2 { fill: #1b1b1b } - .terminal-2259385514-r3 { fill: #f92672 } - .terminal-2259385514-r4 { fill: #c5c8c6 } - .terminal-2259385514-r5 { fill: #7b7e82 } - .terminal-2259385514-r6 { fill: #e2e3e3 } - .terminal-2259385514-r7 { fill: #75715e } - .terminal-2259385514-r8 { fill: #e6db74 } - .terminal-2259385514-r9 { fill: #ae81ff } - .terminal-2259385514-r10 { fill: #a6e22e } + .terminal-685151692-r1 { fill: #e4e5e6 } + .terminal-685151692-r2 { fill: #1b1b1b } + .terminal-685151692-r3 { fill: #f92672 } + .terminal-685151692-r4 { fill: #c5c8c6 } + .terminal-685151692-r5 { fill: #7b7e82 } + .terminal-685151692-r6 { fill: #e2e3e3 } + .terminal-685151692-r7 { fill: #75715e } + .terminal-685151692-r8 { fill: #e6db74 } + .terminal-685151692-r9 { fill: #ae81ff } + .terminal-685151692-r10 { fill: #a6e22e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  import math                                                                  -  2  from os import path                                                          -  3   -  4  # I'm a comment :) -  5   -  6  string_var ="Hello, world!" -  7  int_var =42 -  8  float_var =3.14 -  9  complex_var =1+2j - 10   - 11  list_var = [12345]                                                   - 12  tuple_var = (12345)                                                  - 13  set_var = {12345}                                                    - 14  dict_var = {"a"1"b"2"c"3}                                          - 15   - 16  deffunction_no_args():                                                      - 17  return"No arguments" - 18   - 19  deffunction_with_args(a, b):                                                - 20  return a + b                                                             - 21   - 22  deffunction_with_default_args(a=0, b=0):                                    - 23  return a * b                                                             - 24   - 25  lambda_func =lambda x: x**2 - 26   - 27  if int_var ==42:                                                            - 28  print("It's the answer!")                                                - 29  elif int_var <42:                                                           - 30  print("Less than the answer.")                                           - 31  else:                                                                        - 32  print("Greater than the answer.")                                        - 33   - 34  for index, value in enumerate(list_var):                                     - 35  print(f"Index: {index}, Value: {value}")                                 - 36   - 37  counter =0 - 38  while counter <5:                                                           - 39  print(f"Counter value: {counter}")                                       - 40      counter +=1 - 41   - 42  squared_numbers = [x**2for x in range(10if x %2==0]                    - 43   - 44  try:                                                                         - 45      result =10/0 - 46  except ZeroDivisionError:                                                    - 47  print("Cannot divide by zero!")                                          - 48  finally:                                                                     - 49  print("End of try-except block.")                                        - 50   - 51  class Animal:                                                                - 52  def__init__(self, name):                                                - 53          self.name = name                                                     - 54   - 55  defspeak(self):                                                         - 56  raiseNotImplementedError("Subclasses must implement this method." - 57   - 58  class Dog(Animal):                                                           - 59  defspeak(self):                                                         - 60  returnf"{self.name} says Woof!" - 61   - 62  deffibonacci(n):                                                            - 63      a, b =01 - 64  for _ in range(n):                                                       - 65  yield a                                                              - 66          a, b = b, a + b                                                      - 67   - 68  for num in fibonacci(5):                                                     - 69  print(num)                                                               - 70   - 71  withopen('test.txt''w'as f:                                             - 72      f.write("Testing with statement.")                                       - 73   - 74  @my_decorator                                                                - 75  defsay_hello():                                                             - 76  print("Hello!")                                                          - 77   - 78  say_hello()                                                                  - 79   + + + +  1  import math                                                                  +  2  from os import path                                                          +  3   +  4  # I'm a comment :) +  5   +  6  string_var ="Hello, world!" +  7  int_var =42 +  8  float_var =3.14 +  9  complex_var =1+2j + 10   + 11  list_var = [12345]                                                   + 12  tuple_var = (12345)                                                  + 13  set_var = {12345}                                                    + 14  dict_var = {"a"1"b"2"c"3}                                          + 15   + 16  deffunction_no_args():                                                      + 17  return"No arguments" + 18   + 19  deffunction_with_args(a, b):                                                + 20  return a + b                                                             + 21   + 22  deffunction_with_default_args(a=0, b=0):                                    + 23  return a * b                                                             + 24   + 25  lambda_func =lambda x: x**2 + 26   + 27  if int_var ==42:                                                            + 28  print("It's the answer!")                                                + 29  elif int_var <42:                                                           + 30  print("Less than the answer.")                                           + 31  else:                                                                        + 32  print("Greater than the answer.")                                        + 33   + 34  for index, value inenumerate(list_var):                                     + 35  print(f"Index: {index}, Value: {value}")                                 + 36   + 37  counter =0 + 38  while counter <5:                                                           + 39  print(f"Counter value: {counter}")                                       + 40      counter +=1 + 41   + 42  squared_numbers = [x**2for x inrange(10if x %2==0]                    + 43   + 44  try:                                                                         + 45      result =10/0 + 46  except ZeroDivisionError:                                                    + 47  print("Cannot divide by zero!")                                          + 48  finally:                                                                     + 49  print("End of try-except block.")                                        + 50   + 51  class Animal:                                                                + 52  def__init__(self, name):                                                + 53          self.name = name                                                     + 54   + 55  defspeak(self):                                                         + 56  raiseNotImplementedError("Subclasses must implement this method." + 57   + 58  class Dog(Animal):                                                           + 59  defspeak(self):                                                         + 60  returnf"{self.name} says Woof!" + 61   + 62  deffibonacci(n):                                                            + 63      a, b =01 + 64  for _ inrange(n):                                                       + 65  yield a                                                              + 66          a, b = b, a + b                                                      + 67   + 68  for num infibonacci(5):                                                     + 69  print(num)                                                               + 70   + 71  withopen('test.txt''w'as f:                                             + 72      f.write("Testing with statement.")                                       + 73   + 74  @my_decorator                                                                + 75  defsay_hello():                                                             + 76  print("Hello!")                                                          + 77   + 78  say_hello()                                                                  + 79   diff --git a/tree-sitter/highlights/python.scm b/tree-sitter/highlights/python.scm index 74074043ff..8778026979 100644 --- a/tree-sitter/highlights/python.scm +++ b/tree-sitter/highlights/python.scm @@ -229,7 +229,6 @@ "is" "not" "or" - "del" ] @keyword.operator From c6509cf9dd443c694f22eb4ebb7382ce825df093 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 21 Aug 2023 17:19:51 +0100 Subject: [PATCH 223/366] Add TextArea indent note --- docs/widgets/text_area.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index dad79c5b8b..eef603ae04 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -61,6 +61,8 @@ The `TextArea` widget provides the following component classes: The character(s) inserted when you press tab is controlled by setting the `indent_type` attribute to either `tabs` or `spaces`. +If `indent_type == "spaces"`, pressing ++tab++ will insert `indent_width` spaces. + ### Python 3.7 is not supported Syntax highlighting is not available on Python 3.7. Highlighting will fail _silently_, so end-users who are running Python 3.7 can still edit text without highlighting, even if a `language` and `syntax_theme` is set. From 52cd5351bc1f7cd306c5100976112daa26618313 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 21 Aug 2023 17:22:06 +0100 Subject: [PATCH 224/366] Start TextArea guide inside reference --- docs/widgets/text_area.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index eef603ae04..084aa42984 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -8,7 +8,9 @@ A widget for editing text which may span multiple lines. - [ ] Container -## Example +## Guide + +### Basic example Here's an example app which loads some Python code, sets the syntax highlighting language to Python, and selects some text. From 3e3cf36dca9d745cc768e79fbb16e85c7fb51fd8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 21 Aug 2023 17:23:27 +0100 Subject: [PATCH 225/366] Add TextArea to widget gallery --- docs/widget_gallery.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/widget_gallery.md b/docs/widget_gallery.md index da06823945..d1b21f1aaa 100644 --- a/docs/widget_gallery.md +++ b/docs/widget_gallery.md @@ -287,6 +287,15 @@ A Combination of Tabs and ContentSwitcher to navigate static content. ```{.textual path="docs/examples/widgets/tabbed_content.py" press="j"} ``` +## TextArea + +A multi-line text area which supports syntax highlighting. + +[TextArea reference](./widgets/text_area.md){ .md-button .md-button--primary } + +```{.textual path="docs/examples/widgets/text_area.py"} +``` + ## Tree From 17ada8165bd35766c391b1d3d83ea35ee5b7c4da Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 21 Aug 2023 17:27:25 +0100 Subject: [PATCH 226/366] Fleshing out TextArea docs --- docs/widget_gallery.md | 1 - docs/widgets/text_area.md | 13 +++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/widget_gallery.md b/docs/widget_gallery.md index d1b21f1aaa..546e2b33b1 100644 --- a/docs/widget_gallery.md +++ b/docs/widget_gallery.md @@ -296,7 +296,6 @@ A multi-line text area which supports syntax highlighting. ```{.textual path="docs/examples/widgets/text_area.py"} ``` - ## Tree A tree control with expandable nodes. diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 084aa42984..6e923abb9c 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -26,6 +26,19 @@ to Python, and selects some text. --8<-- "docs/examples/widgets/text_area.py" ``` +### Styling the `TextArea` + +You can use component classes to customise the look and feel of the `TextArea` widget. + +TODO + +### Adding support for custom languages + +### Syntax highlighting themes + +### Building on top of `TextArea` + + ## Reactive attributes From 32d56b76bd6092da36591c518a0ba2145233289a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 21 Aug 2023 17:28:52 +0100 Subject: [PATCH 227/366] Add note --- docs/widgets/text_area.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 6e923abb9c..8a3e6b7319 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -34,6 +34,10 @@ TODO ### Adding support for custom languages + !!! note + More built-in languages will be added in the future. + + ### Syntax highlighting themes ### Building on top of `TextArea` From c8319de36a5eb8f1be4e6ef8e860bd88cc80317c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Aug 2023 14:35:26 +0100 Subject: [PATCH 228/366] Fix TextArea programmatic insert/cursor interaction --- src/textual/widgets/_text_area.py | 6 ++++++ tests/text_area/test_edit_via_api.py | 29 ++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 3c7c0c73e8..c980e6a382 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1188,7 +1188,11 @@ def do(self, text_area: TextArea) -> EditResult: # position in the document even if an insert happens before # their cursor position. + edit_to_row, edit_to_column = edit_to + edit_from_row, edit_from_column = edit_from + edit_top, edit_bottom = _sort_ascending(edit_from, edit_to) + edit_top_row, edit_top_column = edit_top edit_bottom_row, edit_bottom_column = edit_bottom selection_start, selection_end = text_area.selection @@ -1205,11 +1209,13 @@ def do(self, text_area: TextArea) -> EditResult: target_selection_start_column = ( selection_start_column + column_offset if edit_bottom_row == selection_start_row + and edit_bottom_column <= selection_start_column else selection_start_column ) target_selection_end_column = ( selection_end_column + column_offset if edit_bottom_row == selection_end_row + and edit_bottom_column <= selection_end_column else selection_end_column ) diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index 5fffab70a6..3caa3b6810 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -48,7 +48,8 @@ async def test_insert_text_start_maintain_selection_offset(): async def test_insert_text_start(): - """If we don't maintain the selection offset, the cursor jumps + """The document is correctly updated on inserting at the start. + If we don't maintain the selection offset, the cursor jumps to the end of the edit and the selection is empty.""" app = TextAreaApp() async with app.run_test(): @@ -59,6 +60,29 @@ async def test_insert_text_start(): assert text_area.selection == Selection.cursor((0, 5)) +@pytest.mark.parametrize( + "cursor_location,insert_location,cursor_destination", + [ + ((0, 3), (0, 2), (0, 4)), # API insert just before cursor + ((0, 3), (0, 3), (0, 4)), # API insert at cursor location + ((0, 3), (0, 4), (0, 3)), # API insert just after cursor + ((0, 3), (0, 5), (0, 3)), # API insert just after cursor + ], +) +async def test_insert_character_near_cursor_maintain_selection_offset( + cursor_location, + insert_location, + cursor_destination, +): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("012345") + text_area.move_cursor(cursor_location) + text_area.insert("X", location=insert_location) + assert text_area.selection == Selection.cursor(cursor_destination) + + async def test_insert_newlines_start(): app = TextAreaApp() async with app.run_test(): @@ -454,4 +478,5 @@ async def test_delete_fully_within_selection(): replaced_text="45", end_location=(0, 4), ) - assert text_area.selected_text == "01236" + # We deleted 45, but the other characters are still available + assert text_area.selected_text == "236" From e641f571791b86db60a02a36fa5703903b7eda22 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Aug 2023 14:40:30 +0100 Subject: [PATCH 229/366] Improve a test --- tests/text_area/test_edit_via_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index 3caa3b6810..d2266544c1 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -480,3 +480,4 @@ async def test_delete_fully_within_selection(): ) # We deleted 45, but the other characters are still available assert text_area.selected_text == "236" + assert text_area.text == "01236789" From 8410601813e13cd68fb776e7c5ed584501b5ada5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Aug 2023 14:53:03 +0100 Subject: [PATCH 230/366] Testing replacement within selection --- tests/text_area/test_edit_via_api.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index d2266544c1..06896328de 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -481,3 +481,20 @@ async def test_delete_fully_within_selection(): # We deleted 45, but the other characters are still available assert text_area.selected_text == "236" assert text_area.text == "01236789" + + +async def test_replace_fully_within_selection(): + """Adjust the selection when a replacement happens inside it.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + text_area.selection = Selection((0, 2), (0, 7)) + assert text_area.selected_text == "23456" + + result = text_area.replace("XX", start=(0, 2), end=(0, 5)) + assert result == EditResult( + replaced_text="234", + end_location=(0, 4), + ) + assert text_area.selected_text == "XX56" From 7aea7b96fe8656ce58e711e3b59f38d3fb58ef7c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Aug 2023 15:21:39 +0100 Subject: [PATCH 231/366] Testing double-width character keyboard navigation and deletion keybinds with active selections --- tests/text_area/test_edit_via_bindings.py | 38 +++++++++++++++++++++++ tests/text_area/test_selection.py | 32 ++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index 27be5fee4a..f85814c6cc 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -6,6 +6,7 @@ Note that more extensive testing for editing is done at the Document level. """ +import pytest from textual.app import App, ComposeResult from textual.document import Selection @@ -17,6 +18,14 @@ I will face my fear. """ +SIMPLE_TEXT = """\ +ABCDE +FGHIJ +KLMNO +PQRST +UVWXY +Z""" + class TextAreaApp(App): def compose(self) -> ComposeResult: @@ -73,6 +82,35 @@ async def test_delete_left_end(): assert text_area.selection == Selection.cursor((0, 12)) +@pytest.mark.parametrize( + "key,selection", + [ + ("delete", Selection((1, 2), (3, 4))), + ("delete", Selection((3, 4), (1, 2))), + ("backspace", Selection((1, 2), (3, 4))), + ("backspace", Selection((3, 4), (1, 2))), + ], +) +async def test_deletion_with_non_empty_selection(key, selection): + """When there's a selection, pressing backspace or delete should delete everything + that is selected and reset the selection to a cursor at the appropriate location.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text(SIMPLE_TEXT) + text_area.selection = selection + await pilot.press(key) + assert text_area.selection == Selection.cursor((1, 2)) + assert ( + text_area.text + == """\ +ABCDE +FGT +UVWXY +Z""" + ) + + async def test_delete_right(): """Pressing 'delete' deletes the character to the right of the cursor.""" app = TextAreaApp() diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 8a33b6ee65..3365000d41 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -1,7 +1,7 @@ import pytest from textual.app import App, ComposeResult -from textual.document import Selection +from textual.document import Document, Selection from textual.geometry import Offset from textual.widgets import TextArea @@ -72,6 +72,15 @@ async def test_selected_text_backward(): ) +async def test_selected_text_multibyte(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("こんにちは") + text_area.selection = Selection((0, 1), (0, 3)) + assert text_area.selected_text == "んに" + + async def test_selection_clamp(): """When you set the selection reactive, it's clamped to within the document bounds.""" app = TextAreaApp() @@ -268,3 +277,24 @@ async def test_cursor_page_up(): assert text_area.selection == Selection.cursor( (100 - app.console.height + 1, 1) ) + + +async def test_cursor_vertical_movement_visual_alignment_snapping(): + """When you move the cursor vertically, it should stay vertically + aligned even when double-width characters are used.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_document(Document("こんにちは\n012345")) + text_area.move_cursor((1, 3), record_width=True) + + # The '3' is aligned with ん at (0, 1) + # こんにちは + # 012345 + # Pressing `up` takes us from (1, 3) to (0, 1) because record_width=True. + await pilot.press("up") + assert text_area.selection == Selection.cursor((0, 1)) + + # Pressing `down` takes us from (0, 1) to (1, 3) + await pilot.press("down") + assert text_area.selection == Selection.cursor((1, 3)) From 124dced204f406d6bb360e91a5147c59f3c52998 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Aug 2023 15:36:41 +0100 Subject: [PATCH 232/366] Testing "delete to start of line" TextArea binding --- tests/text_area/test_edit_via_bindings.py | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index f85814c6cc..2420fcc185 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -134,3 +134,31 @@ async def test_delete_right_end_of_line(): await pilot.press("delete") assert text_area.selection == Selection.cursor((0, 5)) assert text_area.text == "helloworld!" + + +@pytest.mark.parametrize( + "selection,expected_result", + [ + # Cursors + (Selection.cursor((0, 0)), "0123456789"), + (Selection.cursor((0, 5)), "56789"), + (Selection.cursor((0, 9)), "9"), + (Selection.cursor((0, 10)), ""), + # Selections + (Selection((0, 0), (0, 9)), "9"), + (Selection((0, 0), (0, 10)), ""), + (Selection((0, 2), (0, 5)), "56789"), + (Selection((0, 5), (0, 2)), "23456789"), + ], +) +async def test_delete_to_start_of_line(selection, expected_result): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + text_area.selection = selection + + await pilot.press("ctrl+u") + + assert text_area.selection == Selection.cursor((0, 0)) + assert text_area.text == expected_result From 239a35335925d6b71fb2ef45b5f88f9fcf77fcfa Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Aug 2023 15:59:06 +0100 Subject: [PATCH 233/366] Testing TextArea delete line methods and delete to end of line --- tests/text_area/test_edit_via_bindings.py | 79 +++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index 2420fcc185..c22346d7a5 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -136,6 +136,85 @@ async def test_delete_right_end_of_line(): assert text_area.text == "helloworld!" +@pytest.mark.parametrize( + "selection,expected_result", + [ + (Selection.cursor((0, 0)), ""), + (Selection.cursor((0, 4)), ""), + (Selection.cursor((0, 10)), ""), + (Selection((0, 2), (0, 4)), ""), + (Selection((0, 4), (0, 2)), ""), + ], +) +async def test_delete_line(selection, expected_result): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + text_area.selection = selection + + await pilot.press("ctrl+x") + + assert text_area.selection == Selection.cursor((0, 0)) + assert text_area.text == expected_result + + +@pytest.mark.parametrize( + "selection,expected_result", + [ + # Cursors + (Selection.cursor((0, 0)), "345\n678\n9\n"), + (Selection.cursor((0, 2)), "345\n678\n9\n"), + (Selection.cursor((3, 1)), "012\n345\n678\n"), + (Selection.cursor((4, 0)), "012\n345\n678\n9\n"), + # Selections + (Selection((1, 1), (1, 2)), "012\n678\n9\n"), # non-empty single line selection + (Selection((1, 2), (2, 1)), "012\n9\n"), # delete lines selection touches + (Selection((0, 0), (4, 0)), ""), # delete all lines + ], +) +async def test_delete_line_multiline_document(selection, expected_result): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("012\n345\n678\n9\n") + text_area.selection = selection + + await pilot.press("ctrl+x") + + cursor_row, _ = text_area.cursor_location + assert text_area.selection == Selection.cursor((cursor_row, 0)) + assert text_area.text == expected_result + + +@pytest.mark.parametrize( + "selection,expected_result", + [ + # Cursors + (Selection.cursor((0, 0)), ""), + (Selection.cursor((0, 5)), "01234"), + (Selection.cursor((0, 9)), "012345678"), + (Selection.cursor((0, 10)), "0123456789"), + # Selections + (Selection((0, 0), (0, 9)), "012345678"), + (Selection((0, 0), (0, 10)), "0123456789"), + (Selection((0, 2), (0, 5)), "01234"), + (Selection((0, 5), (0, 2)), "01"), + ], +) +async def test_delete_to_end_of_line(selection, expected_result): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + text_area.selection = selection + + await pilot.press("ctrl+k") + + assert text_area.selection == Selection.cursor(selection.end) + assert text_area.text == expected_result + + @pytest.mark.parametrize( "selection,expected_result", [ From c5842caf67a385ca3eec707c56653ddf91564ab2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Aug 2023 16:46:00 +0100 Subject: [PATCH 234/366] Testing shift selecting using keyboard in vertical direction --- tests/text_area/test_selection.py | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 3365000d41..deb792acdf 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -162,6 +162,53 @@ async def test_cursor_selection_left_to_previous_line(): assert text_area.selection == Selection((2, 2), (1, end_of_previous_line)) +async def test_cursor_selection_up(): + """When you press shift+up the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.move_cursor((2, 3)) + + await pilot.press("shift+up") + assert text_area.selection == Selection((2, 3), (1, 3)) + + +async def test_cursor_selection_up_when_cursor_on_first_line(): + """When you press shift+up the on the first line, it selects to the start.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.move_cursor((0, 4)) + + await pilot.press("shift+up") + assert text_area.selection == Selection((0, 4), (0, 0)) + await pilot.press("shift+up") + assert text_area.selection == Selection((0, 4), (0, 0)) + + +async def test_cursor_selection_down(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.move_cursor((2, 5)) + + await pilot.press("shift+down") + assert text_area.selection == Selection((2, 5), (3, 5)) + + +async def test_cursor_selection_down_when_cursor_on_last_line(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("ABCDEF\nGHIJK") + text_area.move_cursor((1, 2)) + + await pilot.press("shift+down") + assert text_area.selection == Selection((1, 2), (1, 5)) + await pilot.press("shift+down") + assert text_area.selection == Selection((1, 2), (1, 5)) + + async def test_cursor_to_line_end(): """You can use the keyboard to jump the cursor to the end of the current line.""" app = TextAreaApp() From 20c5736acd1f5b4ad8ae29049d45d41c581ddfa5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Aug 2023 16:48:09 +0100 Subject: [PATCH 235/366] Expand tests for home and end keybinds in TextArea --- tests/text_area/test_selection.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index deb792acdf..5b9f027451 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -209,25 +209,27 @@ async def test_cursor_selection_down_when_cursor_on_last_line(): assert text_area.selection == Selection((1, 2), (1, 5)) -async def test_cursor_to_line_end(): +@pytest.mark.parametrize("key", ["end", "ctrl+e"]) +async def test_cursor_to_line_end(key): """You can use the keyboard to jump the cursor to the end of the current line.""" app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.selection = Selection.cursor((2, 2)) - await pilot.press("end") + await pilot.press(key) eol_index = len(TEXT.splitlines()[2]) assert text_area.cursor_location == (2, eol_index) assert text_area.selection.is_empty -async def test_cursor_to_line_home(): +@pytest.mark.parametrize("key", ["home", "ctrl+a"]) +async def test_cursor_to_line_home(key): """You can use the keyboard to jump the cursor to the start of the current line.""" app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.selection = Selection.cursor((2, 2)) - await pilot.press("home") + await pilot.press(key) assert text_area.cursor_location == (2, 0) assert text_area.selection.is_empty From 4c805bf5c45045a13abdbe7e1a6a17727ccff346 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Aug 2023 16:54:58 +0100 Subject: [PATCH 236/366] Renaming tests, testing empty replace and insert --- src/textual/widgets/_text_area.py | 4 +--- tests/text_area/test_edit_via_api.py | 36 ++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index c980e6a382..2d4c8f70b1 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -528,9 +528,7 @@ async def _on_mouse_up(self, event: events.MouseUp) -> None: async def _on_paste(self, event: events.Paste) -> None: """When a paste occurs, insert the text from the paste event into the document.""" - text = event.text - if text: - self.replace(text, *self.selection) + self.replace(event.text, *self.selection) def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: """Return the column that the cell width corresponds to on the given row. diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index 06896328de..66f086cf88 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -1,4 +1,4 @@ -"""Tests editing the document using the API (insert_range etc.) +"""Tests editing the document using the API (replace etc.) The tests in this module directly call the edit APIs on the TextArea rather than going via bindings. @@ -60,6 +60,28 @@ async def test_insert_text_start(): assert text_area.selection == Selection.cursor((0, 5)) +async def test_insert_empty_string(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + + text_area.insert("", location=(0, 3)) + + assert text_area.text == "0123456789" + + +async def test_replace_empty_string(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + + text_area.replace("", start=(0, 3), end=(0, 7)) + + assert text_area.text == "012789" + + @pytest.mark.parametrize( "cursor_location,insert_location,cursor_destination", [ @@ -194,7 +216,7 @@ async def test_insert_multiline_text_maintain_offset(): assert text_area.text == expected_content -async def test_insert_range_multiline_text(): +async def test_replace_multiline_text(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) @@ -220,7 +242,7 @@ async def test_insert_range_multiline_text(): assert text_area.text == expected_content -async def test_insert_range_multiline_text_maintain_selection(): +async def test_replace_multiline_text_maintain_selection(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) @@ -254,7 +276,7 @@ async def test_insert_range_multiline_text_maintain_selection(): assert text_area.text == expected_content -async def test_delete_range_within_line(): +async def test_delete_within_line(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) @@ -283,7 +305,7 @@ async def test_delete_range_within_line(): assert text_area.text == expected_text -async def test_delete_range_within_line_dont_maintain_offset(): +async def test_delete_within_line_dont_maintain_offset(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) @@ -298,7 +320,7 @@ async def test_delete_range_within_line_dont_maintain_offset(): assert text_area.text == expected_text -async def test_delete_range_multiple_lines_selection_above(): +async def test_delete_multiple_lines_selection_above(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) @@ -334,7 +356,7 @@ async def test_delete_range_multiple_lines_selection_above(): ) -async def test_delete_range_empty_document(): +async def test_delete_empty_document(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) From 76e288a0359f0eae1f5148ef9c479da48bd19dbd Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 11:07:51 +0100 Subject: [PATCH 237/366] Testing delete word left via API --- tests/text_area/test_edit_via_bindings.py | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index c22346d7a5..794a32a3da 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -241,3 +241,33 @@ async def test_delete_to_start_of_line(selection, expected_result): assert text_area.selection == Selection.cursor((0, 0)) assert text_area.text == expected_result + + +@pytest.mark.parametrize( + "selection,expected_result,final_selection", + [ + (Selection.cursor((0, 0)), " 012 345 6789", Selection.cursor((0, 0))), + (Selection.cursor((0, 4)), " 2 345 6789", Selection.cursor((0, 2))), + (Selection.cursor((0, 5)), " 345 6789", Selection.cursor((0, 2))), + pytest.param( + Selection.cursor((0, 6)), + " 345 6789", + Selection.cursor((0, 2)), + marks=pytest.mark.xfail(reason="Should skip whitespace."), + ), + (Selection.cursor((0, 14)), " 012 345 ", Selection.cursor((0, 10))), + # When there's a selection and you "delete word left", it just deletes the selection + (Selection((0, 4), (0, 11)), " 01789", Selection.cursor((0, 4))), + ], +) +async def test_delete_word_left(selection, expected_result, final_selection): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text(" 012 345 6789") + text_area.selection = selection + + await pilot.press("ctrl+w") + + assert text_area.text == expected_result + assert text_area.selection == final_selection From 763089484123de4944fe3b97d5c09e7a2d7523d9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 11:11:39 +0100 Subject: [PATCH 238/366] Testing delete word left via API --- src/textual/document/_document.py | 2 +- tests/text_area/test_edit_via_bindings.py | 30 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 807c7f6d18..a3219cb5f9 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -9,7 +9,7 @@ from textual._cells import cell_len from textual._fix_direction import _sort_ascending -from textual._types import Literal, SupportsIndex, get_args +from textual._types import Literal, get_args from textual.geometry import Size Newline = Literal["\r\n", "\n", "\r"] diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index 794a32a3da..63d6087b64 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -271,3 +271,33 @@ async def test_delete_word_left(selection, expected_result, final_selection): assert text_area.text == expected_result assert text_area.selection == final_selection + + +@pytest.mark.parametrize( + "selection,expected_result,final_selection", + [ + (Selection.cursor((0, 0)), " 012 345 6789", Selection.cursor((0, 0))), + (Selection.cursor((0, 4)), " 2 345 6789", Selection.cursor((0, 2))), + (Selection.cursor((0, 5)), " 345 6789", Selection.cursor((0, 2))), + pytest.param( + Selection.cursor((0, 6)), + " 345 6789", + Selection.cursor((0, 2)), + marks=pytest.mark.xfail(reason="Should skip whitespace."), + ), + (Selection.cursor((0, 14)), " 012 345 ", Selection.cursor((0, 10))), + # When there's a selection and you "delete word left", it just deletes the selection + (Selection((0, 4), (0, 11)), " 01789", Selection.cursor((0, 4))), + ], +) +async def test_delete_word_right(selection, expected_result, final_selection): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text(" 012 345 6789") + text_area.selection = selection + + await pilot.press("ctrl+w") + + assert text_area.text == expected_result + assert text_area.selection == final_selection From 87b27df49ce6879ecd22184f32e35d4faf20b8ef Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 11:42:09 +0100 Subject: [PATCH 239/366] Testing delete_word_left with tabs, and delete_word_right --- docs/widgets/text_area.md | 2 +- tests/text_area/test_edit_via_bindings.py | 42 ++++++++++++++++++----- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 8a3e6b7319..93dfd63d93 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -1,6 +1,6 @@ # TextArea -!!! tip "Added in version 0.34.0" +!!! tip "Added in version 0.35.0" A widget for editing text which may span multiple lines. diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index 63d6087b64..697d8dfbb3 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -253,7 +253,7 @@ async def test_delete_to_start_of_line(selection, expected_result): Selection.cursor((0, 6)), " 345 6789", Selection.cursor((0, 2)), - marks=pytest.mark.xfail(reason="Should skip whitespace."), + marks=pytest.mark.xfail(reason="Should skip initial whitespace."), ), (Selection.cursor((0, 14)), " 012 345 ", Selection.cursor((0, 10))), # When there's a selection and you "delete word left", it just deletes the selection @@ -276,17 +276,41 @@ async def test_delete_word_left(selection, expected_result, final_selection): @pytest.mark.parametrize( "selection,expected_result,final_selection", [ - (Selection.cursor((0, 0)), " 012 345 6789", Selection.cursor((0, 0))), - (Selection.cursor((0, 4)), " 2 345 6789", Selection.cursor((0, 2))), - (Selection.cursor((0, 5)), " 345 6789", Selection.cursor((0, 2))), + (Selection.cursor((0, 0)), "\t012 \t 345\t6789", Selection.cursor((0, 0))), + (Selection.cursor((0, 4)), "\t \t 345\t6789", Selection.cursor((0, 1))), + (Selection.cursor((0, 5)), "\t012\t 345\t6789", Selection.cursor((0, 4))), pytest.param( Selection.cursor((0, 6)), - " 345 6789", - Selection.cursor((0, 2)), - marks=pytest.mark.xfail(reason="Should skip whitespace."), + "\t 345\t6789", + Selection.cursor((0, 1)), + marks=pytest.mark.xfail(reason="Should skip initial whitespace."), ), - (Selection.cursor((0, 14)), " 012 345 ", Selection.cursor((0, 10))), + (Selection.cursor((0, 15)), "\t012 \t 345\t", Selection.cursor((0, 11))), # When there's a selection and you "delete word left", it just deletes the selection + (Selection((0, 4), (0, 11)), "\t0126789", Selection.cursor((0, 4))), + ], +) +async def test_delete_word_left_with_tabs(selection, expected_result, final_selection): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("\t012 \t 345\t6789") + text_area.selection = selection + + await pilot.press("ctrl+w") + + assert text_area.text == expected_result + assert text_area.selection == final_selection + + +@pytest.mark.parametrize( + "selection,expected_result,final_selection", + [ + (Selection.cursor((0, 0)), "012 345 6789", Selection.cursor((0, 0))), + (Selection.cursor((0, 4)), " 01 345 6789", Selection.cursor((0, 4))), + (Selection.cursor((0, 5)), " 012345 6789", Selection.cursor((0, 5))), + (Selection.cursor((0, 14)), " 012 345 6789", Selection.cursor((0, 14))), + # When non-empty selection, "delete word right" just deletes the selection (Selection((0, 4), (0, 11)), " 01789", Selection.cursor((0, 4))), ], ) @@ -297,7 +321,7 @@ async def test_delete_word_right(selection, expected_result, final_selection): text_area.load_text(" 012 345 6789") text_area.selection = selection - await pilot.press("ctrl+w") + await pilot.press("ctrl+f") assert text_area.text == expected_result assert text_area.selection == final_selection From 092e4c31d54167b712e4df6975561aacd68da2b2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 11:48:47 +0100 Subject: [PATCH 240/366] Remove unused variables --- src/textual/widgets/_text_area.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 2d4c8f70b1..74b1cd4a79 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1186,11 +1186,7 @@ def do(self, text_area: TextArea) -> EditResult: # position in the document even if an insert happens before # their cursor position. - edit_to_row, edit_to_column = edit_to - edit_from_row, edit_from_column = edit_from - edit_top, edit_bottom = _sort_ascending(edit_from, edit_to) - edit_top_row, edit_top_column = edit_top edit_bottom_row, edit_bottom_column = edit_bottom selection_start, selection_end = text_area.selection From af58450515d841231c961b3d59be55dae48728b7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 11:58:45 +0100 Subject: [PATCH 241/366] Remove debugging width guide --- src/textual/widgets/_text_area.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 74b1cd4a79..56da729a6c 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -76,7 +76,6 @@ class TextArea(ScrollView, can_focus=True): | `text-area--cursor-line` | Targets the line of text the cursor is on. | | `text-area--cursor-line-gutter` | Targets the gutter of the line the cursor is on. | | `text-area--selection` | Targets the selected text. | -| `text-area--width-guide` | Targets the width guide. | """ BINDINGS = [ @@ -187,9 +186,6 @@ class TextArea(ScrollView, can_focus=True): altering this value will immediately change the display width of the visible tabs. """ - _show_width_guide: Reactive[bool] = reactive(False) - """If True, a vertical line will indicate the width of the document.""" - def __init__( self, name: str | None = None, @@ -363,12 +359,6 @@ def render_line(self, widget_y: int) -> Strip: line.stylize(cursor_style, cursor_column, cursor_column + 1) line.stylize_before(active_line_style) - # The width guide is a visual indicator showing the virtual width of the TextArea widget. - if self._show_width_guide: - width_guide_style = self.get_component_rich_style("text-area--width-guide") - width_column = virtual_width - self.gutter_width - line.stylize_before(width_guide_style, width_column - 1, width_column) - # Build the gutter text for this line if self.show_line_numbers: if cursor_row == line_index: From 3ea5948c5155503500bc66ed06108b7ad18a2112 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 12:01:40 +0100 Subject: [PATCH 242/366] Fix snapshot report path --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3693892fc0..5951c2d350 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -54,4 +54,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: snapshot-report-textual - path: tests/snapshot_tests/output/snapshot_report.html + path: snapshot_report.html From 5b0474b59c3931ca6ae831f9007d1c84e535c97e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 12:44:45 +0100 Subject: [PATCH 243/366] Deleting word left/right interaction with line ends fixes, ensure cursor width recorded on all edits --- src/textual/widgets/_text_area.py | 18 ++++---- tests/text_area/test_edit_via_bindings.py | 56 +++++++++++++++++++++++ 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 56da729a6c..c8dd40c510 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1096,18 +1096,14 @@ def action_delete_word_left(self) -> None: cursor_row, cursor_column = end - # Check the current line for a word boundary line = self.document[cursor_row][:cursor_column] matches = list(re.finditer(self.word_pattern, line)) if matches: - # If a word boundary is found, delete the word from_location = (cursor_row, matches[-1].start()) - elif cursor_row > 0: - # If no word boundary is found, and we're not on the first line, delete to the end of the previous line + elif cursor_row > 0 and cursor_column == 0: from_location = (cursor_row - 1, len(self.document[cursor_row - 1])) else: - # If we're already on the first line and no word boundary is found, delete to the start of the line from_location = (cursor_row, 0) self.delete(from_location, self.selection.end, maintain_selection_offset=False) @@ -1128,15 +1124,16 @@ def action_delete_word_right(self) -> None: line = self.document[cursor_row][cursor_column:] matches = list(re.finditer(self.word_pattern, line)) + current_row_length = len(self.document[cursor_row]) if matches: - # If a word boundary is found, delete the word to_location = (cursor_row, cursor_column + matches[0].end()) - elif cursor_row < self.document.line_count - 1: - # If no word boundary is found, and we're not on the last line, delete to the start of the next line + elif ( + cursor_row < self.document.line_count - 1 + and cursor_column == current_row_length + ): to_location = (cursor_row + 1, 0) else: - # If we're already on the last line and no word boundary is found, delete to the end of the line - to_location = (cursor_row, len(self.document[cursor_row])) + to_location = (cursor_row, current_row_length) self.delete(end, to_location, maintain_selection_offset=False) @@ -1234,6 +1231,7 @@ def after(self, text_area: TextArea) -> None: """ if self._updated_selection is not None: text_area.selection = self._updated_selection + text_area.record_cursor_width() @runtime_checkable diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index 697d8dfbb3..db1b14eef6 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -303,6 +303,36 @@ async def test_delete_word_left_with_tabs(selection, expected_result, final_sele assert text_area.selection == final_selection +async def test_delete_word_left_to_start_of_line(): + """If no word boundary found when we 'delete word left', then + the deletion happens to the start of the line.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("0123\n 456789") + text_area.selection = Selection.cursor((1, 3)) + + await pilot.press("ctrl+w") + + assert text_area.text == "0123\n456789" + assert text_area.selection == Selection.cursor((1, 0)) + + +async def test_delete_word_left_at_line_start(): + """If we're at the start of a line and we 'delete word left', the + line merges with the line above (if possible).""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("0123\n 456789") + text_area.selection = Selection.cursor((1, 0)) + + await pilot.press("ctrl+w") + + assert text_area.text == "0123 456789" + assert text_area.selection == Selection.cursor((0, 4)) + + @pytest.mark.parametrize( "selection,expected_result,final_selection", [ @@ -325,3 +355,29 @@ async def test_delete_word_right(selection, expected_result, final_selection): assert text_area.text == expected_result assert text_area.selection == final_selection + + +async def test_delete_word_right_delete_to_end_of_line(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("01234\n56789") + text_area.selection = Selection.cursor((0, 3)) + + await pilot.press("ctrl+f") + + assert text_area.text == "012\n56789" + assert text_area.selection == Selection.cursor((0, 3)) + + +async def test_delete_word_right_at_end_of_line(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("01234\n56789") + text_area.selection = Selection.cursor((0, 5)) + + await pilot.press("ctrl+f") + + assert text_area.text == "0123456789" + assert text_area.selection == Selection.cursor((0, 5)) From a0095301faa098aface05c2a2d0a89fcb0d41566 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 13:02:50 +0100 Subject: [PATCH 244/366] Docstring fixes --- src/textual/document/_document.py | 11 ++++- .../document/_syntax_aware_document.py | 10 +++++ src/textual/document/_syntax_theme.py | 16 ++++++-- src/textual/widgets/_text_area.py | 41 +++++++++++++++---- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index a3219cb5f9..0275c0d77f 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -113,8 +113,15 @@ def get_text_range(self, start: Location, end: Location) -> str: def get_size(self, indent_width: int) -> Size: """Get the size of the document. - The height will generally be the number of lines, and the width - will be the maximum cell length of all the lines.""" + The height is generally be the number of lines, and the width + is generally the maximum cell length of all the lines. + + Args: + indent_width: The width to use for tab characters. + + Returns: + The Size of the document bounding box. + """ @property @abstractmethod diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 9b50590934..ec8d129d07 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -51,6 +51,16 @@ class SyntaxAwareDocument(Document): def __init__( self, text: str, language: str | Language, syntax_theme: str | SyntaxTheme ): + """Construct a SyntaxAwareDocument. + + Args: + text: The initial text contained in the document. + language: The language to use. You can pass a string to use a supported + language, or pass in your own tree-sitter `Language` object. + syntax_theme: The syntax highlighting theme to use. You can pass a string + to use a builtin theme, or construct your own custom SyntaxTheme and + provide that. + """ if not TREE_SITTER: raise RuntimeError("SyntaxAwareDocument is unavailable on Python 3.7.") diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index d9be0c0d99..13cadf573b 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -6,7 +6,6 @@ _NULL_STYLE = Style.null() - _MONOKAI = { "string": Style(color="#E6DB74"), "string.documentation": Style(color="#E6DB74"), @@ -108,12 +107,23 @@ def get_theme(cls, theme_name: str) -> "SyntaxTheme": def get_highlight(self, name: str) -> Style: """Return the Rich style corresponding to the name defined in the tree-sitter - highlight query for the current theme.""" + highlight query for the current theme. + + Args: + name: The name of the highlight. + + Returns: + The `Style` to use for this highlight. + """ return self.style_mapping.get(name, _NULL_STYLE) @classmethod def available_themes(cls) -> list[SyntaxTheme]: - """A list of all available SyntaxThemes.""" + """Get a list of all available SyntaxThemes. + + Returns: + A list of all available SyntaxThemes. + """ return [SyntaxTheme(name, mapping) for name, mapping in _BUILTIN_THEMES.items()] @classmethod diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index c8dd40c510..afca9158d3 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -193,6 +193,15 @@ def __init__( classes: str | None = None, disabled: bool = False, ) -> None: + """Construct a new `TextArea`. + + Args: + name: The name of the `TextArea` widget. + id: The ID of the widget, used to refer to it from Textual CSS. + classes: One or more Textual CSS compatible class names separated by spaces. + disabled: True if the widget is disabled. + """ + super().__init__(name=name, id=id, classes=classes, disabled=disabled) self.document: DocumentBase = Document("") @@ -765,7 +774,7 @@ def get_cursor_down_location(self) -> Location: """Get the location the cursor will move to if it moves down. Returns: - the location the cursor will move to if it moves down. + The location the cursor will move to if it moves down. """ cursor_row, cursor_column = self.selection.end if self.cursor_at_last_row: @@ -958,7 +967,7 @@ def insert( edit. Returns: - An EditResult containing information about the edit. + An `EditResult` containing information about the edit. """ if location is None: location = self.cursor_location @@ -981,7 +990,7 @@ def delete( edit. Returns: - An EditResult containing information about the edit. + An `EditResult` containing information about the edit. """ top, bottom = _sort_ascending(start, end) return self.edit(Edit("", top, bottom, maintain_selection_offset)) @@ -1005,7 +1014,7 @@ def replace( edit. Returns: - An EditResult containing information about the edit. + An `EditResult` containing information about the edit. """ return self.edit(Edit(insert, start, end, maintain_selection_offset)) @@ -1160,7 +1169,7 @@ def do(self, text_area: TextArea) -> EditResult: text_area: The TextArea to perform the edit on. Returns: - An EditResult containing information about the replace operation. + An `EditResult` containing information about the replace operation. """ text = self.text @@ -1243,8 +1252,22 @@ class Undoable(Protocol): To perform an edit operation, pass the Edit to `TextArea.edit()`""" - def do(self, text_area: TextArea) -> object | None: - """Do the action.""" + def do(self, text_area: TextArea) -> Any: + """Do the action. + + Args: + The `TextArea` to perform the action on. + + Returns: + Anything. This protocol doesn't prescribe what is returned. + """ - def undo(self, text_area: TextArea) -> object | None: - """Undo the action.""" + def undo(self, text_area: TextArea) -> Any: + """Undo the action. + + Args: + The `TextArea` to perform the action on. + + Returns: + Anything. This protocol doesn't prescribe what is returned. + """ From 353a6470a288a749a5f88c16fcf6f0c4c5905860 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 13:08:54 +0100 Subject: [PATCH 245/366] Unpin textual snapshot library dependency (issue is fixed) --- poetry.lock | 2522 ++++++++++++++++++++++++------------------------ pyproject.toml | 2 +- 2 files changed, 1262 insertions(+), 1262 deletions(-) diff --git a/poetry.lock b/poetry.lock index df92c6ebbc..9edbad9711 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,1173 +1,13 @@ -[[package]] -name = "aiohttp" -version = "3.8.5" -description = "Async http client/server framework (asyncio)" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -aiosignal = ">=1.1.2" -async-timeout = ">=4.0.0a3,<5.0" -asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""} -attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<4.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns", "cchardet"] - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -frozenlist = ">=1.1.0" - -[[package]] -name = "anyio" -version = "3.7.1" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] - -[[package]] -name = "async-timeout" -version = "4.0.2" -description = "Timeout context manager for asyncio programs" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} - -[[package]] -name = "asynctest" -version = "0.13.0" -description = "Enhance the standard unittest package with features for testing asyncio libraries" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "attrs" -version = "23.1.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] - -[[package]] -name = "black" -version = "23.3.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "cached-property" -version = "1.5.2" -description = "A decorator for caching properties in classes." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "certifi" -version = "2023.7.22" -description = "Python package for providing Mozilla's CA Bundle." -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "cfgv" -version = "3.3.1" -description = "Validate configuration and produce human readable error messages." -category = "dev" -optional = false -python-versions = ">=3.6.1" - -[[package]] -name = "charset-normalizer" -version = "3.2.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "dev" -optional = false -python-versions = ">=3.7.0" - -[[package]] -name = "click" -version = "8.1.6" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" - -[[package]] -name = "colored" -version = "1.4.4" -description = "Simple library for color and formatting to terminal" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "coverage" -version = "7.2.7" -description = "Code coverage measurement for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "distlib" -version = "0.3.7" -description = "Distribution utilities" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "exceptiongroup" -version = "1.1.2" -description = "Backport of PEP 654 (exception groups)" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "filelock" -version = "3.12.2" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "frozenlist" -version = "1.3.3" -description = "A list-like structure which implements collections.abc.MutableSequence" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "ghp-import" -version = "2.1.0" -description = "Copy your docs directly to the gh-pages branch." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -python-dateutil = ">=2.8.1" - -[package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] - -[[package]] -name = "gitdb" -version = "4.0.10" -description = "Git Object Database" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -smmap = ">=3.0.1,<6" - -[[package]] -name = "gitpython" -version = "3.1.32" -description = "GitPython is a Python library used to interact with Git repositories" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} - -[[package]] -name = "griffe" -version = "0.30.1" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -cached-property = {version = "*", markers = "python_version < \"3.8\""} -colorama = ">=0.4" - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "httpcore" -version = "0.16.3" -description = "A minimal low-level HTTP client." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -anyio = ">=3.0,<5.0" -certifi = "*" -h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" - -[package.extras] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - -[[package]] -name = "httpx" -version = "0.23.3" -description = "The next generation HTTP client." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - -[[package]] -name = "identify" -version = "2.5.24" -description = "File identification library for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "importlib-metadata" -version = "6.7.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "linkify-it-py" -version = "2.0.2" -description = "Links recognition library with FULL unicode support." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -uc-micro-py = "*" - -[package.extras] -benchmark = ["pytest", "pytest-benchmark"] -dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] -doc = ["myst-parser", "sphinx", "sphinx-book-theme"] -test = ["coverage", "pytest", "pytest-cov"] - -[[package]] -name = "markdown" -version = "3.4.4" -description = "Python implementation of John Gruber's Markdown." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markdown-it-py" -version = "2.2.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} -mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""} -mdurl = ">=0.1,<1.0" -typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "2.1.3" -description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "mdit-py-plugins" -version = "0.3.5" -description = "Collection of plugins for markdown-it-py" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -markdown-it-py = ">=1.0.0,<3.0.0" - -[package.extras] -code-style = ["pre-commit"] -rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "mkdocs" -version = "1.5.2" -description = "Project documentation with Markdown." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} -ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} -jinja2 = ">=2.11.1" -markdown = ">=3.2.1" -markupsafe = ">=2.0.1" -mergedeep = ">=1.3.4" -packaging = ">=20.5" -pathspec = ">=0.11.1" -platformdirs = ">=2.2.0" -pyyaml = ">=5.1" -pyyaml-env-tag = ">=0.1" -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} -watchdog = ">=2.0" - -[package.extras] -i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] - -[[package]] -name = "mkdocs-autorefs" -version = "0.4.1" -description = "Automatically link across pages in MkDocs." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -Markdown = ">=3.3" -mkdocs = ">=1.1" - -[[package]] -name = "mkdocs-exclude" -version = "1.0.2" -description = "A mkdocs plugin that lets you exclude files or trees." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -mkdocs = "*" - -[[package]] -name = "mkdocs-material" -version = "9.1.21" -description = "Documentation that simply works" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -colorama = ">=0.4" -jinja2 = ">=3.0" -markdown = ">=3.2" -mkdocs = ">=1.5.0" -mkdocs-material-extensions = ">=1.1" -pygments = ">=2.14" -pymdown-extensions = ">=9.9.1" -regex = ">=2022.4.24" -requests = ">=2.26" - -[[package]] -name = "mkdocs-material-extensions" -version = "1.1.1" -description = "Extension pack for Python Markdown and MkDocs Material." -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "mkdocs-rss-plugin" -version = "1.5.0" -description = "MkDocs plugin which generates a static RSS feed using git log and page.meta." -category = "dev" -optional = false -python-versions = ">=3.7, <4" - -[package.dependencies] -GitPython = ">=3.1,<3.2" -mkdocs = ">=1.1,<2" -pytz = {version = ">=2022.0.0,<2023.0.0", markers = "python_version < \"3.9\""} -tzdata = {version = ">=2022.0.0,<2023.0.0", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""} - -[package.extras] -dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (>=4.0.0,<4.1.0)", "validator-collection (>=1.5,<1.6)"] -doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"] - -[[package]] -name = "mkdocstrings" -version = "0.20.0" -description = "Automatic documentation from sources, for MkDocs." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -Jinja2 = ">=2.11.1" -Markdown = ">=3.3" -MarkupSafe = ">=1.1" -mkdocs = ">=1.2" -mkdocs-autorefs = ">=0.3.1" -mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} -pymdown-extensions = ">=6.3" - -[package.extras] -crystal = ["mkdocstrings-crystal (>=0.3.4)"] -python = ["mkdocstrings-python (>=0.5.2)"] -python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] - -[[package]] -name = "mkdocstrings-python" -version = "0.10.1" -description = "A Python handler for mkdocstrings." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -griffe = ">=0.24" -mkdocstrings = ">=0.20" - -[[package]] -name = "msgpack" -version = "1.0.5" -description = "MessagePack serializer" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "multidict" -version = "6.0.4" -description = "multidict implementation" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "mypy" -version = "1.4.1" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" -category = "dev" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "packaging" -version = "23.1" -description = "Core utilities for Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "pathspec" -version = "0.11.2" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "platformdirs" -version = "3.10.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""} - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] - -[[package]] -name = "pluggy" -version = "1.2.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "2.21.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pygments" -version = "2.16.1" -description = "Pygments is a syntax highlighting package written in Python." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pymdown-extensions" -version = "10.1" -description = "Extension pack for Python Markdown." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -markdown = ">=3.2" -pyyaml = "*" - -[[package]] -name = "pytest" -version = "7.4.0" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-aiohttp" -version = "1.0.4" -description = "Pytest plugin for aiohttp support" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -aiohttp = ">=3.8.1" -pytest = ">=6.1.0" -pytest-asyncio = ">=0.17.2" - -[package.extras] -testing = ["coverage (==6.2)", "mypy (==0.931)"] - -[[package]] -name = "pytest-asyncio" -version = "0.21.1" -description = "Pytest support for asyncio" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -pytest = ">=7.0.0" -typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] - -[[package]] -name = "pytest-cov" -version = "2.12.1" -description = "Pytest plugin for measuring coverage." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -coverage = ">=5.2.1" -pytest = ">=4.6" -toml = "*" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "pytest-textual-snapshot" -version = "0.2.0" -description = "Snapshot testing for Textual apps" -category = "dev" -optional = false -python-versions = ">=3.6,<4.0" - -[package.dependencies] -jinja2 = ">=3.0.0" -pytest = ">=7.0.0" -rich = ">=12.0.0" -syrupy = ">=3.0.0" -textual = ">=0.28.0" - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2022.7.1" -description = "World timezone definitions, modern and historical" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyyaml = "*" - -[[package]] -name = "regex" -version = "2023.6.3" -description = "Alternative regular expression module, to replace re." -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "requests" -version = "2.31.0" -description = "Python HTTP for Humans." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - -[[package]] -name = "rich" -version = "13.5.2" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" -optional = false -python-versions = ">=3.7.0" - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "setuptools" -version = "68.0.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "smmap" -version = "5.0.0" -description = "A pure Python implementation of a sliding window memory map manager" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "syrupy" -version = "3.0.6" -description = "Pytest Snapshot Test Utility" -category = "dev" -optional = false -python-versions = ">=3.7,<4" - -[package.dependencies] -colored = ">=1.3.92,<2.0.0" -pytest = ">=5.1.0,<8.0.0" - -[[package]] -name = "textual-dev" -version = "1.1.0" -description = "Development tools for working with Textual" -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" - -[package.dependencies] -aiohttp = ">=3.8.1" -click = ">=8.1.2" -msgpack = ">=1.0.3" -textual = ">=0.32.0" -typing-extensions = ">=4.4.0,<5.0.0" - -[[package]] -name = "time-machine" -version = "2.10.0" -description = "Travel through time in your tests." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -python-dateutil = "*" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "tree-sitter" -version = "0.20.1" -description = "Python bindings to the Tree-sitter parsing library" -category = "main" -optional = false -python-versions = ">=3.3" - -[[package]] -name = "tree-sitter-languages" -version = "1.7.0" -description = "Binary Python wheels for all tree sitter languages." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -tree-sitter = "*" - -[[package]] -name = "typed-ast" -version = "1.5.5" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "types-setuptools" -version = "67.8.0.0" -description = "Typing stubs for setuptools" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "types-tree-sitter" -version = "0.20.1.4" -description = "Typing stubs for tree-sitter" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "types-tree-sitter-languages" -version = "1.7.0.1" -description = "Typing stubs for tree-sitter-languages" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -types-tree-sitter = "*" - -[[package]] -name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "tzdata" -version = "2022.7" -description = "Provider of IANA time zone data" -category = "dev" -optional = false -python-versions = ">=2" - -[[package]] -name = "uc-micro-py" -version = "1.0.2" -description = "Micro subset of unicode data files for linkify-it-py projects." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -test = ["coverage", "pytest", "pytest-cov"] - -[[package]] -name = "urllib3" -version = "2.0.4" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "virtualenv" -version = "20.24.2" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} -platformdirs = ">=3.9.1,<4" - -[package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "watchdog" -version = "3.0.0" -description = "Filesystem events monitoring" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] -name = "yarl" -version = "1.9.2" -description = "Yet another URL library" +name = "aiohttp" +version = "3.8.5" +description = "Async http client/server framework (asyncio)" category = "dev" optional = false -python-versions = ">=3.7" - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} - -[[package]] -name = "zipp" -version = "3.15.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[metadata] -lock-version = "1.1" -python-versions = "^3.7" -content-hash = "89591af5a198994787719c36ca924dac0959f7955fd44c48f1a0abd64dc062b2" - -[metadata.files] -aiohttp = [ +python-versions = ">=3.6" +files = [ {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8"}, {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84"}, {file = "aiohttp-3.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a"}, @@ -1256,27 +96,116 @@ aiohttp = [ {file = "aiohttp-3.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c"}, {file = "aiohttp-3.8.5.tar.gz", hash = "sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc"}, ] -aiosignal = [ + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" +asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""} +attrs = ">=17.3.0" +charset-normalizer = ">=2.0,<4.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "cchardet"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, ] -anyio = [ + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "anyio" +version = "3.7.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, ] -async-timeout = [ + +[package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] + +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] -asynctest = [ + +[package.dependencies] +typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} + +[[package]] +name = "asynctest" +version = "0.13.0" +description = "Enhance the standard unittest package with features for testing asyncio libraries" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, ] -attrs = [ + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, ] -black = [ + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "23.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, @@ -1303,19 +232,67 @@ black = [ {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, ] -cached-property = [ + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cached-property" +version = "1.5.2" +description = "A decorator for caching properties in classes." +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, ] -certifi = [ + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] -cfgv = [ + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" +files = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] -charset-normalizer = [ + +[[package]] +name = "charset-normalizer" +version = "3.2.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" +optional = false +python-versions = ">=3.7.0" +files = [ {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, @@ -1392,18 +369,54 @@ charset-normalizer = [ {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, ] -click = [ + +[[package]] +name = "click" +version = "8.1.6" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, ] -colorama = [ + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -colored = [ + +[[package]] +name = "colored" +version = "1.4.4" +description = "Simple library for color and formatting to terminal" +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"}, ] -coverage = [ + +[[package]] +name = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, @@ -1465,19 +478,61 @@ coverage = [ {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, ] -distlib = [ + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "distlib" +version = "0.3.7" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, ] -exceptiongroup = [ + +[[package]] +name = "exceptiongroup" +version = "1.1.2" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, ] -filelock = [ + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.12.2" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, ] -frozenlist = [ + +[package.extras] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "frozenlist" +version = "1.3.3" +description = "A list-like structure which implements collections.abc.MutableSequence" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, @@ -1553,67 +608,287 @@ frozenlist = [ {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, ] -ghp-import = [ + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] -gitdb = [ + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "gitdb" +version = "4.0.10" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, ] -gitpython = [ + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.32" +description = "GitPython is a Python library used to interact with Git repositories" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "GitPython-3.1.32-py3-none-any.whl", hash = "sha256:e3d59b1c2c6ebb9dfa7a184daf3b6dd4914237e7488a1730a6d8f6f5d0b4187f"}, {file = "GitPython-3.1.32.tar.gz", hash = "sha256:8d9b8cb1e80b9735e8717c9362079d3ce4c6e5ddeebedd0361b228c3a67a62f6"}, ] -griffe = [ + +[package.dependencies] +gitdb = ">=4.0.1,<5" +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} + +[[package]] +name = "griffe" +version = "0.30.1" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c"}, {file = "griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109"}, ] -h11 = [ + +[package.dependencies] +cached-property = {version = "*", markers = "python_version < \"3.8\""} +colorama = ">=0.4" + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] -httpcore = [ + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "httpcore" +version = "0.16.3" +description = "A minimal low-level HTTP client." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, ] -httpx = [ + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.23.3" +description = "The next generation HTTP client." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, ] -identify = [ + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.17.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "identify" +version = "2.5.24" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, ] -idna = [ + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] -importlib-metadata = [ + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, ] -iniconfig = [ + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -jinja2 = [ + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] -linkify-it-py = [ + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "linkify-it-py" +version = "2.0.2" +description = "Links recognition library with FULL unicode support." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"}, {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"}, ] -markdown = [ + +[package.dependencies] +uc-micro-py = "*" + +[package.extras] +benchmark = ["pytest", "pytest-benchmark"] +dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] +doc = ["myst-parser", "sphinx", "sphinx-book-theme"] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "markdown" +version = "3.4.4" +description = "Python implementation of John Gruber's Markdown." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, ] -markdown-it-py = [ + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-it-py" +version = "2.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, ] -markupsafe = [ + +[package.dependencies] +linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} +mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""} +mdurl = ">=0.1,<1.0" +typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, @@ -1665,50 +940,221 @@ markupsafe = [ {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] -mdit-py-plugins = [ + +[[package]] +name = "mdit-py-plugins" +version = "0.3.5" +description = "Collection of plugins for markdown-it-py" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, ] -mdurl = [ + +[package.dependencies] +markdown-it-py = ">=1.0.0,<3.0.0" + +[package.extras] +code-style = ["pre-commit"] +rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] -mergedeep = [ + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] -mkdocs = [ + +[[package]] +name = "mkdocs" +version = "1.5.2" +description = "Project documentation with Markdown." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "mkdocs-1.5.2-py3-none-any.whl", hash = "sha256:60a62538519c2e96fe8426654a67ee177350451616118a41596ae7c876bb7eac"}, {file = "mkdocs-1.5.2.tar.gz", hash = "sha256:70d0da09c26cff288852471be03c23f0f521fc15cf16ac89c7a3bfb9ae8d24f9"}, ] -mkdocs-autorefs = [ + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.2.1" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +packaging = ">=20.5" +pathspec = ">=0.11.1" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "0.4.1" +description = "Automatically link across pages in MkDocs." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, ] -mkdocs-exclude = [ + +[package.dependencies] +Markdown = ">=3.3" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-exclude" +version = "1.0.2" +description = "A mkdocs plugin that lets you exclude files or trees." +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"}, ] -mkdocs-material = [ + +[package.dependencies] +mkdocs = "*" + +[[package]] +name = "mkdocs-material" +version = "9.1.21" +description = "Documentation that simply works" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "mkdocs_material-9.1.21-py3-none-any.whl", hash = "sha256:58bb2f11ef240632e176d6f0f7d1cff06be1d11c696a5a1b553b808b4280ed47"}, {file = "mkdocs_material-9.1.21.tar.gz", hash = "sha256:71940cdfca84ab296b6362889c25395b1621273fb16c93deda257adb7ff44ec8"}, ] -mkdocs-material-extensions = [ + +[package.dependencies] +colorama = ">=0.4" +jinja2 = ">=3.0" +markdown = ">=3.2" +mkdocs = ">=1.5.0" +mkdocs-material-extensions = ">=1.1" +pygments = ">=2.14" +pymdown-extensions = ">=9.9.1" +regex = ">=2022.4.24" +requests = ">=2.26" + +[[package]] +name = "mkdocs-material-extensions" +version = "1.1.1" +description = "Extension pack for Python Markdown and MkDocs Material." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"}, ] -mkdocs-rss-plugin = [ + +[[package]] +name = "mkdocs-rss-plugin" +version = "1.5.0" +description = "MkDocs plugin which generates a static RSS feed using git log and page.meta." +category = "dev" +optional = false +python-versions = ">=3.7, <4" +files = [ {file = "mkdocs-rss-plugin-1.5.0.tar.gz", hash = "sha256:4178b3830dcbad9b53b12459e315b1aad6b37d1e7e5c56c686866a10f99878a4"}, {file = "mkdocs_rss_plugin-1.5.0-py2.py3-none-any.whl", hash = "sha256:2ab14c20bf6b7983acbe50181e7e4a0778731d9c2d5c38107ca7047a7abd2165"}, ] -mkdocstrings = [ + +[package.dependencies] +GitPython = ">=3.1,<3.2" +mkdocs = ">=1.1,<2" +pytz = {version = ">=2022.0.0,<2023.0.0", markers = "python_version < \"3.9\""} +tzdata = {version = ">=2022.0.0,<2023.0.0", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""} + +[package.extras] +dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (>=4.0.0,<4.1.0)", "validator-collection (>=1.5,<1.6)"] +doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"] + +[[package]] +name = "mkdocstrings" +version = "0.20.0" +description = "Automatic documentation from sources, for MkDocs." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"}, {file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"}, ] -mkdocstrings-python = [ + +[package.dependencies] +Jinja2 = ">=2.11.1" +Markdown = ">=3.3" +MarkupSafe = ">=1.1" +mkdocs = ">=1.2" +mkdocs-autorefs = ">=0.3.1" +mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "0.10.1" +description = "A Python handler for mkdocstrings." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "mkdocstrings_python-0.10.1-py3-none-any.whl", hash = "sha256:ef239cee2c688e2b949a0a47e42a141d744dd12b7007311b3309dc70e3bafc5c"}, {file = "mkdocstrings_python-0.10.1.tar.gz", hash = "sha256:b72301fff739070ec517b5b36bf2f7c49d1360a275896a64efb97fc17d3f3968"}, ] -msgpack = [ + +[package.dependencies] +griffe = ">=0.24" +mkdocstrings = ">=0.20" + +[[package]] +name = "msgpack" +version = "1.0.5" +description = "MessagePack serializer" +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, @@ -1773,7 +1219,15 @@ msgpack = [ {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, ] -multidict = [ + +[[package]] +name = "multidict" +version = "6.0.4" +description = "multidict implementation" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, @@ -1849,7 +1303,15 @@ multidict = [ {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, ] -mypy = [ + +[[package]] +name = "mypy" +version = "1.4.1" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, @@ -1877,71 +1339,297 @@ mypy = [ {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, ] -mypy-extensions = [ + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] -nodeenv = [ + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, ] -packaging = [ + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] -pathspec = [ + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] -platformdirs = [ + +[[package]] +name = "platformdirs" +version = "3.10.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, ] -pluggy = [ + +[package.dependencies] +typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, ] -pre-commit = [ + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, ] -pygments = [ + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, ] -pymdown-extensions = [ + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pymdown-extensions" +version = "10.1" +description = "Extension pack for Python Markdown." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "pymdown_extensions-10.1-py3-none-any.whl", hash = "sha256:ef25dbbae530e8f67575d222b75ff0649b1e841e22c2ae9a20bad9472c2207dc"}, {file = "pymdown_extensions-10.1.tar.gz", hash = "sha256:508009b211373058debb8247e168de4cbcb91b1bff7b5e961b2c3e864e00b195"}, ] -pytest = [ + +[package.dependencies] +markdown = ">=3.2" +pyyaml = "*" + +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] -pytest-aiohttp = [ + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-aiohttp" +version = "1.0.4" +description = "Pytest plugin for aiohttp support" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"}, {file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"}, ] -pytest-asyncio = [ + +[package.dependencies] +aiohttp = ">=3.8.1" +pytest = ">=6.1.0" +pytest-asyncio = ">=0.17.2" + +[package.extras] +testing = ["coverage (==6.2)", "mypy (==0.931)"] + +[[package]] +name = "pytest-asyncio" +version = "0.21.1" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, ] -pytest-cov = [ + +[package.dependencies] +pytest = ">=7.0.0" +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] -pytest-textual-snapshot = [ - {file = "pytest_textual_snapshot-0.2.0-py3-none-any.whl", hash = "sha256:663fe07bf62181ec0c63139daaeaf50eb8088164037eb30d721f028adc9edc8c"}, - {file = "pytest_textual_snapshot-0.2.0.tar.gz", hash = "sha256:5e9f8c4b1b011bdae67d4f1129530afd6611f3f8bcf03cf06699402179bc12cf"}, + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-textual-snapshot" +version = "0.4.0" +description = "Snapshot testing for Textual apps" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "pytest_textual_snapshot-0.4.0-py3-none-any.whl", hash = "sha256:879cc5de29cdd31cfe1b6daeb1dc5e42682abebcf4f88e7e3375bd5200683fc0"}, + {file = "pytest_textual_snapshot-0.4.0.tar.gz", hash = "sha256:63782e053928a925d88ff7359dd640f2900e23bc708b3007f8b388e65f2527cb"}, ] -python-dateutil = [ + +[package.dependencies] +jinja2 = ">=3.0.0" +pytest = ">=7.0.0" +rich = ">=12.0.0" +syrupy = ">=3.0.0" +textual = ">=0.28.0" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] -pytz = [ + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2022.7.1" +description = "World timezone definitions, modern and historical" +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, ] -pyyaml = [ + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, @@ -1983,11 +1671,30 @@ pyyaml = [ {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] -pyyaml-env-tag = [ + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, ] -regex = [ + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "regex" +version = "2023.6.3" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ {file = "regex-2023.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:824bf3ac11001849aec3fa1d69abcb67aac3e150a933963fb12bda5151fe1bfd"}, {file = "regex-2023.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05ed27acdf4465c95826962528f9e8d41dbf9b1aa8531a387dee6ed215a3e9ef"}, {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b49c764f88a79160fa64f9a7b425620e87c9f46095ef9c9920542ab2495c8bc"}, @@ -2077,43 +1784,163 @@ regex = [ {file = "regex-2023.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:bdff5eab10e59cf26bc479f565e25ed71a7d041d1ded04ccf9aee1d9f208487a"}, {file = "regex-2023.6.3.tar.gz", hash = "sha256:72d1a25bf36d2050ceb35b517afe13864865268dfb45910e2e17a84be6cbfeb0"}, ] -requests = [ + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] -rfc3986 = [ + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] -rich = [ + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "13.5.2" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, ] -setuptools = [ + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] -six = [ + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -smmap = [ + +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, ] -sniffio = [ + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] -syrupy = [ + +[[package]] +name = "syrupy" +version = "3.0.6" +description = "Pytest Snapshot Test Utility" +category = "dev" +optional = false +python-versions = ">=3.7,<4" +files = [ {file = "syrupy-3.0.6-py3-none-any.whl", hash = "sha256:9c18e22264026b34239bcc87ab7cc8d893eb17236ea7dae634217ea4f22a848d"}, {file = "syrupy-3.0.6.tar.gz", hash = "sha256:583aa5ca691305c27902c3e29a1ce9da50ff9ab5f184c54b1dc124a16e4a6cf4"}, ] -textual-dev = [ + +[package.dependencies] +colored = ">=1.3.92,<2.0.0" +pytest = ">=5.1.0,<8.0.0" + +[[package]] +name = "textual-dev" +version = "1.1.0" +description = "Development tools for working with Textual" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" +files = [ {file = "textual_dev-1.1.0-py3-none-any.whl", hash = "sha256:c57320636098e31fa5d5c29fc3bc60829bb420da3c76bfed24db6eacf178dbc6"}, {file = "textual_dev-1.1.0.tar.gz", hash = "sha256:e2f8ce4e1c18a16b80282f3257cd2feb49a7ede289a78908c9063ce071bb77ce"}, ] -time-machine = [ + +[package.dependencies] +aiohttp = ">=3.8.1" +click = ">=8.1.2" +msgpack = ">=1.0.3" +textual = ">=0.32.0" +typing-extensions = ">=4.4.0,<5.0.0" + +[[package]] +name = "time-machine" +version = "2.10.0" +description = "Travel through time in your tests." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "time_machine-2.10.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d5e93c14b935d802a310c1d4694a9fe894b48a733ebd641c9a570d6f9e1f667"}, {file = "time_machine-2.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c0dda6b132c0180941944ede357109016d161d840384c2fb1096a3a2ef619f4"}, {file = "time_machine-2.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:900517e4a4121bf88527343d6aea2b5c99df134815bb8271ef589ec792502a71"}, @@ -2169,19 +1996,54 @@ time-machine = [ {file = "time_machine-2.10.0-cp39-cp39-win_arm64.whl", hash = "sha256:c1775a949dd830579d1af5a271ec53d920dc01657035ad305f55c5a1ac9b9f1e"}, {file = "time_machine-2.10.0.tar.gz", hash = "sha256:64fd89678cf589fc5554c311417128b2782222dd65f703bf248ef41541761da0"}, ] -toml = [ + +[package.dependencies] +python-dateutil = "*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] -tomli = [ + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -tree-sitter = [ + +[[package]] +name = "tree-sitter" +version = "0.20.1" +description = "Python bindings to the Tree-sitter parsing library" +category = "main" +optional = false +python-versions = ">=3.3" +files = [ {file = "tree_sitter-0.20.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:6f11a1fd909dcf569e7b1d98861a837436799e757bbbc5cd5280989050929e12"}, {file = "tree_sitter-0.20.1.tar.gz", hash = "sha256:e93f082c545d6649bcfb5d681ed255eb004a6ce22988971a128f40692feec60d"}, ] -tree-sitter-languages = [ + +[[package]] +name = "tree-sitter-languages" +version = "1.7.0" +description = "Binary Python wheels for all tree sitter languages." +category = "main" +optional = false +python-versions = "*" +files = [ {file = "tree_sitter_languages-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fd8b856c224a74c395ed9495761c3ef8ba86014dbf6037d73634436ae683c808"}, {file = "tree_sitter_languages-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:277d1bec6e101a26a4445cd7cb1eb8f8cf5a9bbad1ca80692bfae1af63568272"}, {file = "tree_sitter_languages-1.7.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0473bd896799ccc87f428766813ddedd3506cad8430dbe863b663c81d7387680"}, @@ -2229,7 +2091,18 @@ tree-sitter-languages = [ {file = "tree_sitter_languages-1.7.0-cp39-cp39-win32.whl", hash = "sha256:f28e9904833b7a909f8227c4560401049bd3310cebe3e0a884d9461f783b9af2"}, {file = "tree_sitter_languages-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ea47ee390ec2e1c9bf96d7b418775263766021a834910c9f2d578f95a3e27d0f"}, ] -typed-ast = [ + +[package.dependencies] +tree-sitter = "*" + +[[package]] +name = "typed-ast" +version = "1.5.5" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, @@ -2272,39 +2145,133 @@ typed-ast = [ {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, ] -types-setuptools = [ + +[[package]] +name = "types-setuptools" +version = "67.8.0.0" +description = "Typing stubs for setuptools" +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "types-setuptools-67.8.0.0.tar.gz", hash = "sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f"}, {file = "types_setuptools-67.8.0.0-py3-none-any.whl", hash = "sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff"}, ] -types-tree-sitter = [ + +[[package]] +name = "types-tree-sitter" +version = "0.20.1.4" +description = "Typing stubs for tree-sitter" +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "types-tree-sitter-0.20.1.4.tar.gz", hash = "sha256:673730dcc2efe09be6cdbd9795cdc5243c164262b7a539e6d7e7980fd06c0907"}, {file = "types_tree_sitter-0.20.1.4-py3-none-any.whl", hash = "sha256:9a38efd62a3cf66f9751c612588b7dbc72340fd6c81fd089c8a0f5877f86b58c"}, ] -types-tree-sitter-languages = [ + +[[package]] +name = "types-tree-sitter-languages" +version = "1.7.0.1" +description = "Typing stubs for tree-sitter-languages" +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "types-tree-sitter-languages-1.7.0.1.tar.gz", hash = "sha256:eadbbfa13f3fcad0711ac8f866cf87692f3c0cfeee72e979a5202b797588d57d"}, {file = "types_tree_sitter_languages-1.7.0.1-py3-none-any.whl", hash = "sha256:818ec7824ed1bb5bcdbe21022340e0df3930199eb969ea1e08eb03a92440bce2"}, ] -typing-extensions = [ + +[package.dependencies] +types-tree-sitter = "*" + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] -tzdata = [ + +[[package]] +name = "tzdata" +version = "2022.7" +description = "Provider of IANA time zone data" +category = "dev" +optional = false +python-versions = ">=2" +files = [ {file = "tzdata-2022.7-py2.py3-none-any.whl", hash = "sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d"}, {file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"}, ] -uc-micro-py = [ + +[[package]] +name = "uc-micro-py" +version = "1.0.2" +description = "Micro subset of unicode data files for linkify-it-py projects." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "uc-micro-py-1.0.2.tar.gz", hash = "sha256:30ae2ac9c49f39ac6dce743bd187fcd2b574b16ca095fa74cd9396795c954c54"}, {file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"}, ] -urllib3 = [ + +[package.extras] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "urllib3" +version = "2.0.4" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, ] -virtualenv = [ + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.24.2" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, ] -watchdog = [ + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} +platformdirs = ">=3.9.1,<4" + +[package.extras] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "watchdog" +version = "3.0.0" +description = "Filesystem events monitoring" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, @@ -2333,7 +2300,18 @@ watchdog = [ {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, ] -yarl = [ + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "yarl" +version = "1.9.2" +description = "Yet another URL library" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, @@ -2409,7 +2387,29 @@ yarl = [ {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, ] -zipp = [ + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, ] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.7" +content-hash = "cce38f5dbb080f492f07b0f468d5c718bf6803ce27dd37157ebb576017e22cad" diff --git a/pyproject.toml b/pyproject.toml index bcb9bbd27b..f2ffd76d8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ httpx = "^0.23.1" types-setuptools = "^67.2.0.1" textual-dev = "^1.1.0" pytest-asyncio = "*" -pytest-textual-snapshot = "0.2.0" # TODO: pinned while while we resolve issue #3042 +pytest-textual-snapshot = ">0.3.0" types-tree-sitter = "^0.20.1.4" types-tree-sitter-languages = "^1.7.0.1" From 23a9e618379cc744b6a2f2c5500c152775e262e6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 13:11:30 +0100 Subject: [PATCH 246/366] Docstring fixes --- src/textual/widgets/_text_area.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index afca9158d3..576d3d6576 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1166,7 +1166,7 @@ def do(self, text_area: TextArea) -> EditResult: """Perform the edit operation. Args: - text_area: The TextArea to perform the edit on. + text_area: The `TextArea` to perform the edit on. Returns: An `EditResult` containing information about the replace operation. @@ -1225,10 +1225,10 @@ def do(self, text_area: TextArea) -> EditResult: return replace_result def undo(self, text_area: TextArea) -> EditResult: - """Undo the Replace operation. + """Undo the edit operation. Args: - text_area: The TextArea to undo the insert operation on. + text_area: The `TextArea` to undo the insert operation on. """ raise NotImplementedError() @@ -1236,7 +1236,7 @@ def after(self, text_area: TextArea) -> None: """Possibly update the cursor location after the widget has been refreshed. Args: - text_area: The TextArea this operation was performed on. + text_area: The `TextArea` this operation was performed on. """ if self._updated_selection is not None: text_area.selection = self._updated_selection From 3951d49d346fb4f2bbd95e78bedc734093715d11 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 13:25:17 +0100 Subject: [PATCH 247/366] Fix recording cursor width --- src/textual/widgets/_text_area.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 576d3d6576..e97e3d5834 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1218,7 +1218,6 @@ def do(self, text_area: TextArea) -> EditResult: start=(target_selection_start_row, target_selection_start_column), end=(target_selection_end_row, target_selection_end_column), ) - text_area.record_cursor_width() else: self._updated_selection = Selection.cursor(replace_result.end_location) From 063fd18f567adba8b8c92b5b134fdd045e5dd99f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 13:26:18 +0100 Subject: [PATCH 248/366] Fix a docstring --- src/textual/widgets/_text_area.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index e97e3d5834..38569d7d48 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1228,6 +1228,9 @@ def undo(self, text_area: TextArea) -> EditResult: Args: text_area: The `TextArea` to undo the insert operation on. + + Returns: + An `EditResult` containing information about the replace operation. """ raise NotImplementedError() From cfbf6f64526276c5ef5c8a7962a7b957111a8790 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 13:50:40 +0100 Subject: [PATCH 249/366] Add select_all to TextArea --- src/textual/widgets/_text_area.py | 8 ++++++++ tests/text_area/test_selection.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 38569d7d48..c358b1d3ad 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -649,6 +649,14 @@ def move_cursor_relative( target = clamp_visitable((current_row + rows, current_column + columns)) self.move_cursor(target, select, center, record_width) + def select_all(self) -> None: + """Select all of the text in the `TextArea`.""" + last_line = self.document.line_count - 1 + length_of_last_line = len(self.document[last_line]) + selection_start = (0, 0) + selection_end = (last_line, length_of_last_line) + self.selection = Selection(selection_start, selection_end) + @property def cursor_location(self) -> Location: """The current location of the cursor in the document. diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 5b9f027451..e3f7057b30 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -347,3 +347,22 @@ async def test_cursor_vertical_movement_visual_alignment_snapping(): # Pressing `down` takes us from (0, 1) to (1, 3) await pilot.press("down") assert text_area.selection == Selection.cursor((1, 3)) + + +@pytest.mark.parametrize( + "content,expected_selection", + [ + ("123\n456\n789", Selection((0, 0), (2, 3))), + ("123\n456\n789\n", Selection((0, 0), (3, 0))), + ("", Selection((0, 0), (0, 0))), + ], +) +async def test_select_all(content, expected_selection): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text(content) + + text_area.select_all() + + assert text_area.selection == expected_selection From ae16ede7d70f9b8636e36c4064bcd7c76984efb8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 14:26:28 +0100 Subject: [PATCH 250/366] Remove unused tree-sitter stuff from .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1da36b6f27..b3307c9311 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ __pycache__/ # C extensions *.so -!tree-sitter/textual-languages.so # Distribution / packaging .Python From 2ff5b1786e5362f41b12e7c506143dfe47fe3789 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 14:57:57 +0100 Subject: [PATCH 251/366] Line select --- src/textual/widgets/_text_area.py | 9 +++++++++ tests/text_area/test_selection.py | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index c358b1d3ad..44875da957 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -649,6 +649,15 @@ def move_cursor_relative( target = clamp_visitable((current_row + rows, current_column + columns)) self.move_cursor(target, select, center, record_width) + def select_line(self, index: int) -> None: + """Select all the text in the specified line.""" + try: + line = self.document[index] + except IndexError: + return + else: + self.selection = Selection((index, 0), (index, len(line))) + def select_all(self) -> None: """Select all of the text in the `TextArea`.""" last_line = self.document.line_count - 1 diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index e3f7057b30..a9735034e8 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -366,3 +366,23 @@ async def test_select_all(content, expected_selection): text_area.select_all() assert text_area.selection == expected_selection + + +@pytest.mark.parametrize( + "index,content,expected_selection", + [ + (1, "123\n456\n789\n", Selection((1, 0), (1, 3))), + (2, "123\n456\n789\n", Selection((2, 0), (2, 3))), + (3, "123\n456\n789\n", Selection((3, 0), (3, 0))), + (0, "", Selection((0, 0), (0, 0))), + ], +) +async def test_select_line(index, content, expected_selection): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text(content) + + text_area.select_line(index) + + assert text_area.selection == expected_selection From 5bd559cd4e5eca74fe152cee4b85269fe6b99aa5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 15:00:04 +0100 Subject: [PATCH 252/366] Make word pattern private in TextArea --- src/textual/widgets/_text_area.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 44875da957..13ba1522c7 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -210,7 +210,7 @@ def __init__( self.indent_type: Literal["tabs", "spaces"] = "spaces" """Whether to indent using tabs or spaces.""" - self.word_pattern = re.compile(r"(?<=\W)(?=\w)|(?<=\w)(?=\W)") + self._word_pattern = re.compile(r"(?<=\W)(?=\w)|(?<=\w)(?=\W)") """Compiled regular expression for what we consider to be a 'word'.""" self._last_intentional_cell_width: int = 0 @@ -879,7 +879,7 @@ def get_cursor_left_word_location(self) -> Location: cursor_row, cursor_column = self.cursor_location # Check the current line for a word boundary line = self.document[cursor_row][:cursor_column] - matches = list(re.finditer(self.word_pattern, line)) + matches = list(re.finditer(self._word_pattern, line)) if matches: # If a word boundary is found, move the cursor there cursor_column = matches[-1].start() @@ -910,7 +910,7 @@ def get_cursor_right_word_location(self): cursor_row, cursor_column = self.selection.end # Check the current line for a word boundary line = self.document[cursor_row][cursor_column:] - matches = list(re.finditer(self.word_pattern, line)) + matches = list(re.finditer(self._word_pattern, line)) if matches: # If a word boundary is found, move the cursor there cursor_column += matches[0].end() @@ -1123,7 +1123,7 @@ def action_delete_word_left(self) -> None: cursor_row, cursor_column = end line = self.document[cursor_row][:cursor_column] - matches = list(re.finditer(self.word_pattern, line)) + matches = list(re.finditer(self._word_pattern, line)) if matches: from_location = (cursor_row, matches[-1].start()) @@ -1148,7 +1148,7 @@ def action_delete_word_right(self) -> None: # Check the current line for a word boundary line = self.document[cursor_row][cursor_column:] - matches = list(re.finditer(self.word_pattern, line)) + matches = list(re.finditer(self._word_pattern, line)) current_row_length = len(self.document[cursor_row]) if matches: From 6407fed450dae6fac475a9f0d650b3617882728f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 15:29:50 +0100 Subject: [PATCH 253/366] Add blinking cursor to TextArea --- src/textual/widgets/_text_area.py | 49 +++++++++++++++++-- .../snapshot_apps/text_area_languages.py | 4 +- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 13ba1522c7..5a03de1817 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -186,6 +186,12 @@ class TextArea(ScrollView, can_focus=True): altering this value will immediately change the display width of the visible tabs. """ + cursor_blink: Reactive[bool] = reactive(True) + """True if the cursor should blink.""" + + _cursor_blink_visible: Reactive[bool] = reactive(True) + """True if the cursor should be rendered """ + def __init__( self, name: str | None = None, @@ -364,8 +370,12 @@ def render_line(self, widget_y: int) -> Strip: cursor_row, cursor_column = end active_line_style = self.get_component_rich_style("text-area--cursor-line") if cursor_row == line_index: - cursor_style = self.get_component_rich_style("text-area--cursor") - line.stylize(cursor_style, cursor_column, cursor_column + 1) + draw_cursor = not self.cursor_blink or ( + self.cursor_blink and self._cursor_blink_visible + ) + if draw_cursor: + cursor_style = self.get_component_rich_style("text-area--cursor") + line.stylize(cursor_style, cursor_column, cursor_column + 1) line.stylize_before(active_line_style) # Build the gutter text for this line @@ -456,6 +466,7 @@ async def _on_key(self, event: events.Key) -> None: "tab": " " * self.indent_width if self.indent_type == "spaces" else "\t", "enter": "\n", } + self._reset_blink() if event.is_printable or key in insert_values: event.stop() event.prevent_default() @@ -466,7 +477,6 @@ async def _on_key(self, event: events.Key) -> None: start, end = self.selection self.replace(insert, start, end, False) - # --- Lower level event/key handling def get_target_document_location(self, event: MouseEvent) -> Location: """Given a MouseEvent, return the row and column offset of the event in document-space. @@ -487,6 +497,7 @@ def get_target_document_location(self, event: MouseEvent) -> Location: target_column = self.cell_width_to_column_index(target_x, target_row) return target_row, target_column + # --- Lower level event/key handling @property def gutter_width(self) -> int: """The width of the gutter (the left column containing line numbers). @@ -503,6 +514,36 @@ def gutter_width(self) -> int: ) return gutter_width + def _on_mount(self, _: events.Mount) -> None: + self.blink_timer = self.set_interval( + 0.5, + self._toggle_cursor_blink_visible, + pause=not (self.cursor_blink and self.has_focus), + ) + + def _on_blur(self, _: events.Blur) -> None: + self._pause_blink_visible() + + def _on_focus(self, _: events.Focus) -> None: + self._reset_blink() + + def _toggle_cursor_blink_visible(self) -> None: + """Toggle visibility of the cursor for the purposes of 'cursor blink'.""" + self._cursor_blink_visible = not self._cursor_blink_visible + cursor_row, _ = self.cursor_location + self.refresh_lines(cursor_row) + + def _reset_blink(self): + """Reset the cursor blink timer.""" + if self.cursor_blink: + self._cursor_blink_visible = True + self.blink_timer.reset() + + def _pause_blink_visible(self): + """Pause the cursor blinking but ensure it stays visible.""" + self._cursor_blink_visible = True + self.blink_timer.pause() + async def _on_mouse_down(self, event: events.MouseDown) -> None: """Update the cursor position, and begin a selection using the mouse.""" target = self.get_target_document_location(event) @@ -511,6 +552,7 @@ async def _on_mouse_down(self, event: events.MouseDown) -> None: # Capture the mouse so that if the cursor moves outside the # TextArea widget while selecting, the widget still scrolls. self.capture_mouse() + self._pause_blink_visible() async def _on_mouse_move(self, event: events.MouseMove) -> None: """Handles click and drag to expand and contract the selection.""" @@ -524,6 +566,7 @@ async def _on_mouse_up(self, event: events.MouseUp) -> None: self._selecting = False self.release_mouse() self.record_cursor_width() + self._reset_blink() async def _on_paste(self, event: events.Paste) -> None: """When a paste occurs, insert the text from the paste event into the document.""" diff --git a/tests/snapshot_tests/snapshot_apps/text_area_languages.py b/tests/snapshot_tests/snapshot_apps/text_area_languages.py index 3e6f05ab8d..da6bd06992 100644 --- a/tests/snapshot_tests/snapshot_apps/text_area_languages.py +++ b/tests/snapshot_tests/snapshot_apps/text_area_languages.py @@ -5,7 +5,9 @@ class TextAreaSnapshot(App): def compose(self) -> ComposeResult: - yield TextArea() + text_area = TextArea() + text_area.cursor_blink = False + yield text_area app = TextAreaSnapshot() From 1c9a678f9ea839dd080356df3ce0e687cb070be8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 15:41:59 +0100 Subject: [PATCH 254/366] Renaming, adding missing return typing --- src/textual/widgets/_text_area.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 5a03de1817..fe7aa2af47 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -466,7 +466,7 @@ async def _on_key(self, event: events.Key) -> None: "tab": " " * self.indent_width if self.indent_type == "spaces" else "\t", "enter": "\n", } - self._reset_blink() + self._restart_blink() if event.is_printable or key in insert_values: event.stop() event.prevent_default() @@ -525,7 +525,7 @@ def _on_blur(self, _: events.Blur) -> None: self._pause_blink_visible() def _on_focus(self, _: events.Focus) -> None: - self._reset_blink() + self._restart_blink() def _toggle_cursor_blink_visible(self) -> None: """Toggle visibility of the cursor for the purposes of 'cursor blink'.""" @@ -533,13 +533,13 @@ def _toggle_cursor_blink_visible(self) -> None: cursor_row, _ = self.cursor_location self.refresh_lines(cursor_row) - def _reset_blink(self): + def _restart_blink(self) -> None: """Reset the cursor blink timer.""" if self.cursor_blink: self._cursor_blink_visible = True self.blink_timer.reset() - def _pause_blink_visible(self): + def _pause_blink_visible(self) -> None: """Pause the cursor blinking but ensure it stays visible.""" self._cursor_blink_visible = True self.blink_timer.pause() @@ -566,7 +566,7 @@ async def _on_mouse_up(self, event: events.MouseUp) -> None: self._selecting = False self.release_mouse() self.record_cursor_width() - self._reset_blink() + self._restart_blink() async def _on_paste(self, event: events.Paste) -> None: """When a paste occurs, insert the text from the paste event into the document.""" From c30158b015c0dcfb9630e6c01419d62cf67c4f8f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 17:33:13 +0100 Subject: [PATCH 255/366] Add selection bindings --- src/textual/widgets/_text_area.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index fe7aa2af47..bb40415328 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -79,7 +79,7 @@ class TextArea(ScrollView, can_focus=True): """ BINDINGS = [ - Binding("escape", "screen.focus_next", "focus next", show=False), + Binding("escape", "screen.focus_next", "unfocus text area"), # Cursor movement Binding("up", "cursor_up", "cursor up", show=False), Binding("down", "cursor_down", "cursor down", show=False), @@ -112,6 +112,9 @@ class TextArea(ScrollView, can_focus=True): "ctrl+u", "delete_to_start_of_line", "delete to line start", show=False ), Binding("ctrl+k", "delete_to_end_of_line", "delete to line end", show=False), + # Binding("f5", "select_word", "select word", show=False), + Binding("f6", "select_line", "select line", show=False), + Binding("f7", "select_all", "select all", show=False), ] """ | Key(s) | Description | @@ -138,6 +141,8 @@ class TextArea(ScrollView, can_focus=True): | ctrl+x | Delete the current line. | | ctrl+u | Delete from cursor to the start of the line. | | ctrl+k | Delete from cursor to the end of the line. | + | f6 | Select the current line. | + | f7 | Select all text in the document. | """ language: Reactive[str | "Language" | None] = reactive(None, always_update=True) @@ -693,13 +698,23 @@ def move_cursor_relative( self.move_cursor(target, select, center, record_width) def select_line(self, index: int) -> None: - """Select all the text in the specified line.""" + """Select all the text in the specified line. + + Args: + index: The index of the line to select (starting from 0). + """ try: line = self.document[index] except IndexError: return else: self.selection = Selection((index, 0), (index, len(line))) + self.record_cursor_width() + + def action_select_line(self) -> None: + """Select all the text on the current line.""" + cursor_row, _ = self.cursor_location + self.select_line(cursor_row) def select_all(self) -> None: """Select all of the text in the `TextArea`.""" @@ -708,6 +723,11 @@ def select_all(self) -> None: selection_start = (0, 0) selection_end = (last_line, length_of_last_line) self.selection = Selection(selection_start, selection_end) + self.record_cursor_width() + + def action_select_all(self) -> None: + """Select all the text in the document.""" + self.select_all() @property def cursor_location(self) -> Location: @@ -775,9 +795,7 @@ def action_cursor_left_select(self): This will expand or contract the selection. """ new_cursor_location = self.get_cursor_left_location() - selection_start, selection_end = self.selection - self.selection = Selection(selection_start, new_cursor_location) - self.record_cursor_width() + self.move_cursor(new_cursor_location, select=True) def get_cursor_left_location(self) -> Location: """Get the location the cursor will move to if it moves left. @@ -800,7 +818,6 @@ def action_cursor_right(self) -> None: """ target = self.get_cursor_right_location() self.move_cursor(target) - self.record_cursor_width() def action_cursor_right_select(self): """Move the end of the selection one location to the right.""" From 8f69944167d9b284ed99b4ab29a6b206030dc206 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 17:43:04 +0100 Subject: [PATCH 256/366] Moving cursor left/right by word while selecting --- src/textual/widgets/_text_area.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index bb40415328..abbb10fee5 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -85,8 +85,20 @@ class TextArea(ScrollView, can_focus=True): Binding("down", "cursor_down", "cursor down", show=False), Binding("left", "cursor_left", "cursor left", show=False), Binding("ctrl+left", "cursor_left_word", "cursor left word", show=False), + Binding( + "ctrl+shift+left", + "cursor_left_word(True)", + "cursor left word select", + show=False, + ), Binding("right", "cursor_right", "cursor right", show=False), Binding("ctrl+right", "cursor_right_word", "cursor right word", show=False), + Binding( + "ctrl+shift+right", + "cursor_right_word(True)", + "cursor right word select", + show=False, + ), Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False), Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False), Binding("pageup", "cursor_page_up", "cursor page up", show=False), @@ -124,8 +136,10 @@ class TextArea(ScrollView, can_focus=True): | down | Move the cursor down. | | left | Move the cursor left. | | ctrl+left | Move the cursor to the start of the word. | + | ctrl+shift+left | Move the cursor to the start of the word and select. | | right | Move the cursor right. | | ctrl+right | Move the cursor to the end of the word. | + | ctrl+shift+right | Move the cursor to the end of the word and select. | | home,ctrl+a | Move the cursor to the start of the line. | | end,ctrl+e | Move the cursor to the end of the line. | | pageup | Move the cursor one page up. | @@ -923,12 +937,16 @@ def get_cursor_line_start_location(self) -> Location: cursor_row, _cursor_column = end return cursor_row, 0 - def action_cursor_left_word(self) -> None: - """Move the cursor left by a single word, skipping spaces.""" + def action_cursor_left_word(self, select: bool = False) -> None: + """Move the cursor left by a single word, skipping spaces. + + Args: + select: Whether to select while moving the cursor. + """ if self.cursor_at_start_of_document: return target = self.get_cursor_left_word_location() - self.move_cursor(target) + self.move_cursor(target, select=select) def get_cursor_left_word_location(self) -> Location: """Get the location the cursor will jump to if it goes 1 word left. @@ -952,14 +970,14 @@ def get_cursor_left_word_location(self) -> Location: cursor_column = 0 return cursor_row, cursor_column - def action_cursor_right_word(self) -> None: + def action_cursor_right_word(self, select: bool = False) -> None: """Move the cursor right by a single word, skipping spaces.""" if self.cursor_at_end_of_document: return target = self.get_cursor_right_word_location() - self.move_cursor(target) + self.move_cursor(target, select=select) def get_cursor_right_word_location(self): """Get the location the cursor will jump to if it goes 1 word right. From 72cd7d4dc72c142d8128daff5ead2ea8864004e9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 17:45:08 +0100 Subject: [PATCH 257/366] Change escape keybind description, TextArea --- src/textual/widgets/_text_area.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index abbb10fee5..2384f4d130 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -79,7 +79,7 @@ class TextArea(ScrollView, can_focus=True): """ BINDINGS = [ - Binding("escape", "screen.focus_next", "unfocus text area"), + Binding("escape", "screen.focus_next", "Shift Focus"), # Cursor movement Binding("up", "cursor_up", "cursor up", show=False), Binding("down", "cursor_down", "cursor down", show=False), From 4f87f152cba13888798f6b29784c280640de9788 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 18:00:29 +0100 Subject: [PATCH 258/366] Stripping whitespace when going word left/right --- src/textual/widgets/_text_area.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 2384f4d130..564d413c1e 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -956,7 +956,7 @@ def get_cursor_left_word_location(self) -> Location: """ cursor_row, cursor_column = self.cursor_location # Check the current line for a word boundary - line = self.document[cursor_row][:cursor_column] + line = self.document[cursor_row][:cursor_column].rstrip() matches = list(re.finditer(self._word_pattern, line)) if matches: # If a word boundary is found, move the cursor there @@ -987,7 +987,7 @@ def get_cursor_right_word_location(self): """ cursor_row, cursor_column = self.selection.end # Check the current line for a word boundary - line = self.document[cursor_row][cursor_column:] + line = self.document[cursor_row][cursor_column:].lstrip() matches = list(re.finditer(self._word_pattern, line)) if matches: # If a word boundary is found, move the cursor there From 535e5d741058f2d7d4564bead0e9592da2d052e0 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 18:05:02 +0100 Subject: [PATCH 259/366] Add missing annotation --- src/textual/widgets/_text_area.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 564d413c1e..6849541aa4 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -979,7 +979,7 @@ def action_cursor_right_word(self, select: bool = False) -> None: target = self.get_cursor_right_word_location() self.move_cursor(target, select=select) - def get_cursor_right_word_location(self): + def get_cursor_right_word_location(self) -> Location: """Get the location the cursor will jump to if it goes 1 word right. Returns: @@ -989,6 +989,7 @@ def get_cursor_right_word_location(self): # Check the current line for a word boundary line = self.document[cursor_row][cursor_column:].lstrip() matches = list(re.finditer(self._word_pattern, line)) + if matches: # If a word boundary is found, move the cursor there cursor_column += matches[0].end() @@ -999,6 +1000,7 @@ def get_cursor_right_word_location(self): else: # If we're already on the last line and no word boundary is found, move to the end of the line cursor_column = len(self.document[cursor_row]) + return cursor_row, cursor_column def action_cursor_page_up(self) -> None: From e17af672235a862e4358c6eb83744fb8a4a52112 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 22:08:08 +0100 Subject: [PATCH 260/366] Cursor word right and left parity with PyCharm --- src/textual/widgets/_text_area.py | 73 +++++++++++++++---------------- tests/text_area/test_selection.py | 60 +++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 37 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 6849541aa4..829d1baa85 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -84,18 +84,18 @@ class TextArea(ScrollView, can_focus=True): Binding("up", "cursor_up", "cursor up", show=False), Binding("down", "cursor_down", "cursor down", show=False), Binding("left", "cursor_left", "cursor left", show=False), - Binding("ctrl+left", "cursor_left_word", "cursor left word", show=False), + Binding("ctrl+left", "cursor_word_left", "cursor word left", show=False), Binding( "ctrl+shift+left", - "cursor_left_word(True)", + "cursor_word_left(True)", "cursor left word select", show=False, ), Binding("right", "cursor_right", "cursor right", show=False), - Binding("ctrl+right", "cursor_right_word", "cursor right word", show=False), + Binding("ctrl+right", "cursor_word_right", "cursor word right", show=False), Binding( "ctrl+shift+right", - "cursor_right_word(True)", + "cursor_word_right(True)", "cursor right word select", show=False, ), @@ -937,69 +937,68 @@ def get_cursor_line_start_location(self) -> Location: cursor_row, _cursor_column = end return cursor_row, 0 - def action_cursor_left_word(self, select: bool = False) -> None: - """Move the cursor left by a single word, skipping spaces. + def action_cursor_word_left(self, select: bool = False) -> None: + """Move the cursor left by a single word, skipping trailing whitespace. Args: select: Whether to select while moving the cursor. """ if self.cursor_at_start_of_document: return - target = self.get_cursor_left_word_location() + target = self.get_cursor_word_left_location() self.move_cursor(target, select=select) - def get_cursor_left_word_location(self) -> Location: + def get_cursor_word_left_location(self) -> Location: """Get the location the cursor will jump to if it goes 1 word left. Returns: The location the cursor will jump on "jump word left". """ cursor_row, cursor_column = self.cursor_location - # Check the current line for a word boundary - line = self.document[cursor_row][:cursor_column].rstrip() - matches = list(re.finditer(self._word_pattern, line)) - if matches: - # If a word boundary is found, move the cursor there - cursor_column = matches[-1].start() - elif cursor_row > 0: - # If no word boundary is found, and we're not on the first line, move to the end of the previous line - cursor_row -= 1 - cursor_column = len(self.document[cursor_row]) - else: - # If we're already on the first line and no word boundary is found, move to the start of the line - cursor_column = 0 + if cursor_row > 0 and cursor_column == 0: + # Going to the previous row + return cursor_row - 1, len(self.document[cursor_row - 1]) + + # Staying on the same row + line = self.document[cursor_row][:cursor_column] + search_string = line.rstrip() + + matches = list(re.finditer(self._word_pattern, search_string)) + cursor_column = matches[-1].start() if matches else 0 return cursor_row, cursor_column - def action_cursor_right_word(self, select: bool = False) -> None: - """Move the cursor right by a single word, skipping spaces.""" + def action_cursor_word_right(self, select: bool = False) -> None: + """Move the cursor right by a single word, skipping leading whitespace.""" if self.cursor_at_end_of_document: return - target = self.get_cursor_right_word_location() + target = self.get_cursor_word_right_location() self.move_cursor(target, select=select) - def get_cursor_right_word_location(self) -> Location: + def get_cursor_word_right_location(self) -> Location: """Get the location the cursor will jump to if it goes 1 word right. Returns: The location the cursor will jump on "jump word right". """ cursor_row, cursor_column = self.selection.end - # Check the current line for a word boundary - line = self.document[cursor_row][cursor_column:].lstrip() - matches = list(re.finditer(self._word_pattern, line)) - + line = self.document[cursor_row] + if cursor_row < self.document.line_count - 1 and cursor_column == len(line): + # Moving to the line below + return cursor_row + 1, 0 + + # Staying on the same line + search_string = line[cursor_column:] + pre_strip_length = len(search_string) + search_string = search_string.lstrip() + strip_offset = pre_strip_length - len(search_string) + + matches = list(re.finditer(self._word_pattern, search_string)) if matches: - # If a word boundary is found, move the cursor there - cursor_column += matches[0].end() - elif cursor_row < self.document.line_count - 1: - # If no word boundary is found and we're not on the last line, move to the start of the next line - cursor_row += 1 - cursor_column = 0 + cursor_column += matches[0].start() + strip_offset else: - # If we're already on the last line and no word boundary is found, move to the end of the line - cursor_column = len(self.document[cursor_row]) + cursor_column = len(line) return cursor_row, cursor_column diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index a9735034e8..fddeab5f66 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -304,6 +304,66 @@ async def test_get_cursor_down_location(start, end): assert text_area.get_cursor_down_location() == end +@pytest.mark.parametrize( + "start,end", + [ + ((0, 0), (0, 0)), + ((0, 1), (0, 0)), + ((0, 2), (0, 0)), + ((0, 3), (0, 0)), + ((0, 4), (0, 3)), + ((0, 5), (0, 3)), + ((0, 6), (0, 3)), + ((0, 7), (0, 3)), + ((0, 10), (0, 7)), + ((1, 0), (0, 10)), + ((1, 2), (1, 0)), + ((1, 4), (1, 0)), + ((1, 7), (1, 4)), + ((1, 8), (1, 7)), + ((1, 13), (1, 11)), + ((1, 14), (1, 11)), + ], +) +async def test_cursor_word_left_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("AB CD EFG\n HI\tJK LM ") + text_area.move_cursor(start) + assert text_area.get_cursor_word_left_location() == end + + +@pytest.mark.parametrize( + "start,end", + [ + ((0, 0), (0, 2)), + ((0, 1), (0, 2)), + ((0, 2), (0, 5)), + ((0, 3), (0, 5)), + ((0, 4), (0, 5)), + ((0, 5), (0, 10)), + ((0, 6), (0, 10)), + ((0, 7), (0, 10)), + ((0, 10), (1, 0)), + ((1, 0), (1, 6)), + ((1, 2), (1, 6)), + ((1, 4), (1, 6)), + ((1, 7), (1, 9)), + ((1, 8), (1, 9)), + ((1, 13), (1, 14)), + ((1, 14), (1, 14)), + ], +) +async def test_cursor_word_right_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("AB CD EFG\n HI\tJK LM ") + text_area.move_cursor(start) + assert text_area.get_cursor_word_right_location() == end + + async def test_cursor_page_down(): """Pagedown moves the cursor down 1 page, retaining column index.""" app = TextAreaApp() From ae1adc4a6bed3a779543bf08d848e524f9b71201 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Aug 2023 22:55:50 +0100 Subject: [PATCH 261/366] Use repaint=False for cursor blink --- src/textual/widgets/_text_area.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 829d1baa85..aadb110c42 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -56,9 +56,6 @@ class TextArea(ScrollView, can_focus=True): TextArea > .text-area--selection { background: $primary; } -TextArea > .text-area--width-guide { - background: white 4%; -} """ COMPONENT_CLASSES: ClassVar[set[str]] = { @@ -67,7 +64,6 @@ class TextArea(ScrollView, can_focus=True): "text-area--cursor-line", "text-area--cursor-line-gutter", "text-area--selection", - "text-area--width-guide", } """| Class | Description | |:--------------------------------|:-------------------------------------------------| @@ -208,7 +204,7 @@ class TextArea(ScrollView, can_focus=True): cursor_blink: Reactive[bool] = reactive(True) """True if the cursor should blink.""" - _cursor_blink_visible: Reactive[bool] = reactive(True) + _cursor_blink_visible: Reactive[bool] = reactive(True, repaint=False) """True if the cursor should be rendered """ def __init__( From d7ee6ee87186087d790bce007403a6f2e145d861 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Aug 2023 11:13:37 +0100 Subject: [PATCH 262/366] Improve focus/blur styling --- src/textual/widgets/_text_area.py | 42 +- .../__snapshots__/test_snapshots.ambr | 2274 ++++++++--------- 2 files changed, 1168 insertions(+), 1148 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index aadb110c42..d78ca5b08f 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -35,27 +35,47 @@ class TextArea(ScrollView, can_focus=True): $text-area-active-line-bg: white 8%; TextArea { - background: $panel; + background: $panel 70%; width: 1fr; height: 1fr; } +TextArea:focus { + background: $panel; +} +TextArea:focus > .text-area--cursor-line { + background: $text-area-active-line-bg; +} TextArea > .text-area--cursor-line { + background: white 5%; +} +TextArea:focus > .text-area--cursor-line-gutter { + color: $text; background: $text-area-active-line-bg; } TextArea > .text-area--cursor-line-gutter { - color: $text; + color: $text 65%; background: $text-area-active-line-bg; } +TextArea:focus > .text-area--gutter { + color: $text-muted 45%; +} TextArea > .text-area--gutter { - color: $text-muted 40%; + color: $text-muted 35%; } -TextArea > .text-area--cursor { - color: $text; +TextArea:focus > .text-area--cursor { + color: black 90%; background: white 80%; } -TextArea > .text-area--selection { +TextArea > .text-area--cursor { + color: black 90%; + background: white 25%; +} +TextArea:focus > .text-area--selection { background: $primary; } +TextArea > .text-area--selection { + background: $primary 65%; +} """ COMPONENT_CLASSES: ClassVar[set[str]] = { @@ -75,7 +95,7 @@ class TextArea(ScrollView, can_focus=True): """ BINDINGS = [ - Binding("escape", "screen.focus_next", "Shift Focus"), + Binding("escape", "screen.focus_next", "Shift Focus", show=False), # Cursor movement Binding("up", "cursor_up", "cursor up", show=False), Binding("down", "cursor_down", "cursor down", show=False), @@ -537,7 +557,7 @@ def _on_mount(self, _: events.Mount) -> None: ) def _on_blur(self, _: events.Blur) -> None: - self._pause_blink_visible() + self._pause_blink(visible=True) def _on_focus(self, _: events.Focus) -> None: self._restart_blink() @@ -554,9 +574,9 @@ def _restart_blink(self) -> None: self._cursor_blink_visible = True self.blink_timer.reset() - def _pause_blink_visible(self) -> None: + def _pause_blink(self, visible: bool = True) -> None: """Pause the cursor blinking but ensure it stays visible.""" - self._cursor_blink_visible = True + self._cursor_blink_visible = visible self.blink_timer.pause() async def _on_mouse_down(self, event: events.MouseDown) -> None: @@ -567,7 +587,7 @@ async def _on_mouse_down(self, event: events.MouseDown) -> None: # Capture the mouse so that if the cursor moves outside the # TextArea widget while selecting, the widget still scrolls. self.capture_mouse() - self._pause_blink_visible() + self._pause_blink(visible=True) async def _on_mouse_move(self, event: events.MouseMove) -> None: """Handles click and drag to expand and contract the selection.""" diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 0983987c47..52b3e24053 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -27982,319 +27982,319 @@ font-weight: 700; } - .terminal-3624001113-matrix { + .terminal-1270666592-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3624001113-title { + .terminal-1270666592-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3624001113-r1 { fill: #e4e5e6 } - .terminal-3624001113-r2 { fill: #1b1b1b } - .terminal-3624001113-r3 { fill: #75715e } - .terminal-3624001113-r4 { fill: #c5c8c6 } - .terminal-3624001113-r5 { fill: #7b7e82 } - .terminal-3624001113-r6 { fill: #e2e3e3 } - .terminal-3624001113-r7 { fill: #e6db74 } - .terminal-3624001113-r8 { fill: #ae81ff } - .terminal-3624001113-r9 { fill: #f92672 } - .terminal-3624001113-r10 { fill: #a6e22e } + .terminal-1270666592-r1 { fill: #e4e5e6 } + .terminal-1270666592-r2 { fill: #151515 } + .terminal-1270666592-r3 { fill: #75715e } + .terminal-1270666592-r4 { fill: #c5c8c6 } + .terminal-1270666592-r5 { fill: #86898c } + .terminal-1270666592-r6 { fill: #e2e3e3 } + .terminal-1270666592-r7 { fill: #e6db74 } + .terminal-1270666592-r8 { fill: #ae81ff } + .terminal-1270666592-r9 { fill: #f92672 } + .terminal-1270666592-r10 { fill: #a6e22e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  /* This is a comment in CSS */ -  2   -  3  /* Basic selectors and properties */ -  4  body {                                 -  5      font-family: Arial, sans-serif;    -  6      background-color: #f4f4f4;         -  7      margin: 0;                         -  8      padding: 0;                        -  9  }                                      - 10   - 11  /* Class and ID selectors */ - 12  .header {                              - 13      background-color: #333;            - 14      color: #fff;                       - 15      padding: 10px0;                   - 16      text-align: center;                - 17  }                                      - 18   - 19  #logo {                                - 20      font-size: 24px;                   - 21      font-weight: bold;                 - 22  }                                      - 23   - 24  /* Descendant and child selectors */ - 25  .nav ul {                              - 26      list-style-type: none;             - 27      padding: 0;                        - 28  }                                      - 29   - 30  .nav > li {                            - 31      display: inline-block;             - 32      margin-right: 10px;                - 33  }                                      - 34   - 35  /* Pseudo-classes */ - 36  a:hover {                              - 37      text-decoration: underline;        - 38  }                                      - 39   - 40  input:focus {                          - 41      border-color: #007BFF;             - 42  }                                      - 43   - 44  /* Media query */ - 45  @media (max-width: 768px) {            - 46      body {                             - 47          font-size: 16px;               - 48      }                                  - 49   - 50      .header {                          - 51          padding: 5px0;                - 52      }                                  - 53  }                                      - 54   - 55  /* Keyframes animation */ - 56  @keyframes slideIn {                   - 57  from {                             - 58          transform: translateX(-100%);  - 59      }                                  - 60  to {                               - 61          transform: translateX(0);      - 62      }                                  - 63  }                                      - 64   - 65  .slide-in-element {                    - 66      animation: slideIn 0.5s forwards;  - 67  }                                      - 68   + +  1  /* This is a comment in CSS */ +  2   +  3  /* Basic selectors and properties */ +  4  body {                                 +  5      font-family: Arial, sans-serif;    +  6      background-color: #f4f4f4;         +  7      margin: 0;                         +  8      padding: 0;                        +  9  }                                      + 10   + 11  /* Class and ID selectors */ + 12  .header {                              + 13      background-color: #333;            + 14      color: #fff;                       + 15      padding: 10px0;                   + 16      text-align: center;                + 17  }                                      + 18   + 19  #logo {                                + 20      font-size: 24px;                   + 21      font-weight: bold;                 + 22  }                                      + 23   + 24  /* Descendant and child selectors */ + 25  .nav ul {                              + 26      list-style-type: none;             + 27      padding: 0;                        + 28  }                                      + 29   + 30  .nav > li {                            + 31      display: inline-block;             + 32      margin-right: 10px;                + 33  }                                      + 34   + 35  /* Pseudo-classes */ + 36  a:hover {                              + 37      text-decoration: underline;        + 38  }                                      + 39   + 40  input:focus {                          + 41      border-color: #007BFF;             + 42  }                                      + 43   + 44  /* Media query */ + 45  @media (max-width: 768px) {            + 46      body {                             + 47          font-size: 16px;               + 48      }                                  + 49   + 50      .header {                          + 51          padding: 5px0;                + 52      }                                  + 53  }                                      + 54   + 55  /* Keyframes animation */ + 56  @keyframes slideIn {                   + 57  from {                             + 58          transform: translateX(-100%);  + 59      }                                  + 60  to {                               + 61          transform: translateX(0);      + 62      }                                  + 63  }                                      + 64   + 65  .slide-in-element {                    + 66      animation: slideIn 0.5s forwards;  + 67  }                                      + 68   @@ -28325,273 +28325,273 @@ font-weight: 700; } - .terminal-2423874257-matrix { + .terminal-2506971960-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2423874257-title { + .terminal-2506971960-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2423874257-r1 { fill: #e4e5e6 } - .terminal-2423874257-r2 { fill: #1b1b1b } - .terminal-2423874257-r3 { fill: #c5c8c6 } - .terminal-2423874257-r4 { fill: #7b7e82 } - .terminal-2423874257-r5 { fill: #e2e3e3 } - .terminal-2423874257-r6 { fill: #f92672 } - .terminal-2423874257-r7 { fill: #e6db74 } - .terminal-2423874257-r8 { fill: #75715e } + .terminal-2506971960-r1 { fill: #e4e5e6 } + .terminal-2506971960-r2 { fill: #151515 } + .terminal-2506971960-r3 { fill: #c5c8c6 } + .terminal-2506971960-r4 { fill: #86898c } + .terminal-2506971960-r5 { fill: #e2e3e3 } + .terminal-2506971960-r6 { fill: #f92672 } + .terminal-2506971960-r7 { fill: #e6db74 } + .terminal-2506971960-r8 { fill: #75715e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  <!DOCTYPE html>                                                              -  2  <html lang="en">                                                            -  3   -  4  <head>                                                                      -  5  <!-- Meta tags --> -  6      <meta charset="UTF-8">                                                  -  7      <meta name="viewport" content="width=device-width, initial-scale=1.0" -  8  <!-- Title --> -  9      <title>HTML Test Page</title>                                           - 10  <!-- Link to CSS --> - 11      <link rel="stylesheet" href="styles.css">                               - 12  </head>                                                                     - 13   - 14  <body>                                                                      - 15  <!-- Header section --> - 16      <header class="header">                                                 - 17          <h1 id="logo">HTML Test Page</h1>                                   - 18      </header>                                                               - 19   - 20  <!-- Navigation --> - 21      <nav class="nav">                                                       - 22          <ul>                                                                - 23              <li><a href="#">Home</a></li>                                   - 24              <li><a href="#">About</a></li>                                  - 25              <li><a href="#">Contact</a></li>                                - 26          </ul>                                                               - 27      </nav>                                                                  - 28   - 29  <!-- Main content area --> - 30      <main>                                                                  - 31          <article>                                                           - 32              <h2>Welcome to the Test Page</h2>                               - 33              <p>This is a paragraph to test the HTML structure.</p>          - 34              <img src="test-image.jpg" alt="Test Image" width="300">         - 35          </article>                                                          - 36      </main>                                                                 - 37   - 38  <!-- Form --> - 39      <section>                                                               - 40          <form action="/submit" method="post">                               - 41              <label for="name">Name:</label>                                 - 42              <input type="text" id="name" name="name">                       - 43              <input type="submit" value="Submit">                            - 44          </form>                                                             - 45      </section>                                                              - 46   - 47  <!-- Footer --> - 48      <footer>                                                                - 49          <p>&copy; 2023 HTML Test Page</p>                                   - 50      </footer>                                                               - 51   - 52  <!-- Script tag --> - 53      <script src="scripts.js"></script>                                      - 54  </body>                                                                     - 55   - 56  </html>                                                                     - 57   + +  1  <!DOCTYPE html>                                                              +  2  <html lang="en">                                                            +  3   +  4  <head>                                                                      +  5  <!-- Meta tags --> +  6      <meta charset="UTF-8">                                                  +  7      <meta name="viewport" content="width=device-width, initial-scale=1.0" +  8  <!-- Title --> +  9      <title>HTML Test Page</title>                                           + 10  <!-- Link to CSS --> + 11      <link rel="stylesheet" href="styles.css">                               + 12  </head>                                                                     + 13   + 14  <body>                                                                      + 15  <!-- Header section --> + 16      <header class="header">                                                 + 17          <h1 id="logo">HTML Test Page</h1>                                   + 18      </header>                                                               + 19   + 20  <!-- Navigation --> + 21      <nav class="nav">                                                       + 22          <ul>                                                                + 23              <li><a href="#">Home</a></li>                                   + 24              <li><a href="#">About</a></li>                                  + 25              <li><a href="#">Contact</a></li>                                + 26          </ul>                                                               + 27      </nav>                                                                  + 28   + 29  <!-- Main content area --> + 30      <main>                                                                  + 31          <article>                                                           + 32              <h2>Welcome to the Test Page</h2>                               + 33              <p>This is a paragraph to test the HTML structure.</p>          + 34              <img src="test-image.jpg" alt="Test Image" width="300">         + 35          </article>                                                          + 36      </main>                                                                 + 37   + 38  <!-- Form --> + 39      <section>                                                               + 40          <form action="/submit" method="post">                               + 41              <label for="name">Name:</label>                                 + 42              <input type="text" id="name" name="name">                       + 43              <input type="submit" value="Submit">                            + 44          </form>                                                             + 45      </section>                                                              + 46   + 47  <!-- Footer --> + 48      <footer>                                                                + 49          <p>&copy; 2023 HTML Test Page</p>                                   + 50      </footer>                                                               + 51   + 52  <!-- Script tag --> + 53      <script src="scripts.js"></script>                                      + 54  </body>                                                                     + 55   + 56  </html>                                                                     + 57   @@ -28622,170 +28622,170 @@ font-weight: 700; } - .terminal-3961698941-matrix { + .terminal-703968804-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3961698941-title { + .terminal-703968804-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3961698941-r1 { fill: #e4e5e6 } - .terminal-3961698941-r2 { fill: #1b1b1b } - .terminal-3961698941-r3 { fill: #c5c8c6 } - .terminal-3961698941-r4 { fill: #7b7e82 } - .terminal-3961698941-r5 { fill: #e2e3e3 } - .terminal-3961698941-r6 { fill: #f92672;font-weight: bold } - .terminal-3961698941-r7 { fill: #e6db74 } - .terminal-3961698941-r8 { fill: #ae81ff } - .terminal-3961698941-r9 { fill: #66d9ef;font-style: italic; } + .terminal-703968804-r1 { fill: #e4e5e6 } + .terminal-703968804-r2 { fill: #151515 } + .terminal-703968804-r3 { fill: #c5c8c6 } + .terminal-703968804-r4 { fill: #86898c } + .terminal-703968804-r5 { fill: #e2e3e3 } + .terminal-703968804-r6 { fill: #f92672;font-weight: bold } + .terminal-703968804-r7 { fill: #e6db74 } + .terminal-703968804-r8 { fill: #ae81ff } + .terminal-703968804-r9 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  { -  2  "name""John Doe",                            -  3  "age"30,                                     -  4  "isStudent"false,                            -  5  "address": {                                   -  6  "street""123 Main St",                   -  7  "city""Anytown",                         -  8  "state""CA",                             -  9  "zip""12345" - 10      },                                             - 11  "phoneNumbers": [                              - 12          {                                          - 13  "type""home",                        - 14  "number""555-555-1234" - 15          },                                         - 16          {                                          - 17  "type""work",                        - 18  "number""555-555-5678" - 19          }                                          - 20      ],                                             - 21  "hobbies": ["reading""hiking""swimming"],  - 22  "pets": [                                      - 23          {                                          - 24  "type""dog",                         - 25  "name""Fido" - 26          },                                         - 27      ],                                             - 28  "graduationYear"null - 29  }                                                  - 30   - 31   + +  1  { +  2  "name""John Doe",                            +  3  "age"30,                                     +  4  "isStudent"false,                            +  5  "address": {                                   +  6  "street""123 Main St",                   +  7  "city""Anytown",                         +  8  "state""CA",                             +  9  "zip""12345" + 10      },                                             + 11  "phoneNumbers": [                              + 12          {                                          + 13  "type""home",                        + 14  "number""555-555-1234" + 15          },                                         + 16          {                                          + 17  "type""work",                        + 18  "number""555-555-5678" + 19          }                                          + 20      ],                                             + 21  "hobbies": ["reading""hiking""swimming"],  + 22  "pets": [                                      + 23          {                                          + 24  "type""dog",                         + 25  "name""Fido" + 26          },                                         + 27      ],                                             + 28  "graduationYear"null + 29  }                                                  + 30   + 31   @@ -28816,318 +28816,318 @@ font-weight: 700; } - .terminal-2526664541-matrix { + .terminal-3326201444-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2526664541-title { + .terminal-3326201444-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2526664541-r1 { fill: #e4e5e6 } - .terminal-2526664541-r2 { fill: #1b1b1b;font-weight: bold } - .terminal-2526664541-r3 { fill: #f92672;font-weight: bold } - .terminal-2526664541-r4 { fill: #c5c8c6 } - .terminal-2526664541-r5 { fill: #7b7e82 } - .terminal-2526664541-r6 { fill: #e2e3e3 } - .terminal-2526664541-r7 { fill: #75715e } - .terminal-2526664541-r8 { fill: #23568b } + .terminal-3326201444-r1 { fill: #e4e5e6 } + .terminal-3326201444-r2 { fill: #151515;font-weight: bold } + .terminal-3326201444-r3 { fill: #f92672;font-weight: bold } + .terminal-3326201444-r4 { fill: #c5c8c6 } + .terminal-3326201444-r5 { fill: #86898c } + .terminal-3326201444-r6 { fill: #e2e3e3 } + .terminal-3326201444-r7 { fill: #75715e } + .terminal-3326201444-r8 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  Heading -  2  =======                                                                      -  3   -  4  Sub-heading -  5  -----------                                                                  -  6   -  7  ### Heading -  8   -  9  #### H4 Heading - 10   - 11  ##### H5 Heading - 12   - 13  ###### H6 Heading - 14   - 15   - 16  Paragraphs are separated                                                     - 17  by a blank line.                                                             - 18   - 19  Two spaces at the end of a line                                              - 20  produces a line break.                                                       - 21   - 22  Text attributes _italic_,                                                    - 23  **bold**, `monospace`.                                                       - 24   - 25  Horizontal rule:                                                             - 26   - 27  ---                                                                          - 28   - 29  Bullet list:                                                                 - 30   - 31  * apples                                                                   - 32  * oranges                                                                  - 33  * pears                                                                    - 34   - 35  Numbered list:                                                               - 36   - 37  1. lather                                                                  - 38  2. rinse                                                                   - 39  3. repeat                                                                  - 40   - 41  An [example](http://example.com).                                            - 42   - 43  > Markdown uses email-style > characters for blockquoting.                   - 44  >                                                                            - 45  > Lorem ipsum                                                                - 46   - 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) - 48   - 49   - 50  ```                                                                          - 51  a=1                                                                          - 52  ```                                                                          - 53   - 54  ```python                                                                    - 55  import this                                                                  - 56  ```                                                                          - 57   - 58  ```somelang                                                                  - 59  foobar                                                                       - 60  ```                                                                          - 61   - 62      import this                                                              - 63   - 64   - 65  1. List item                                                                 - 66   - 67         Code block                                                            - 68   - + +  1  Heading +  2  =======                                                                      +  3   +  4  Sub-heading +  5  -----------                                                                  +  6   +  7  ### Heading +  8   +  9  #### H4 Heading + 10   + 11  ##### H5 Heading + 12   + 13  ###### H6 Heading + 14   + 15   + 16  Paragraphs are separated                                                     + 17  by a blank line.                                                             + 18   + 19  Two spaces at the end of a line                                              + 20  produces a line break.                                                       + 21   + 22  Text attributes _italic_,                                                    + 23  **bold**, `monospace`.                                                       + 24   + 25  Horizontal rule:                                                             + 26   + 27  ---                                                                          + 28   + 29  Bullet list:                                                                 + 30   + 31  * apples                                                                   + 32  * oranges                                                                  + 33  * pears                                                                    + 34   + 35  Numbered list:                                                               + 36   + 37  1. lather                                                                  + 38  2. rinse                                                                   + 39  3. repeat                                                                  + 40   + 41  An [example](http://example.com).                                            + 42   + 43  > Markdown uses email-style > characters for blockquoting.                   + 44  >                                                                            + 45  > Lorem ipsum                                                                + 46   + 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) + 48   + 49   + 50  ```                                                                          + 51  a=1                                                                          + 52  ```                                                                          + 53   + 54  ```python                                                                    + 55  import this                                                                  + 56  ```                                                                          + 57   + 58  ```somelang                                                                  + 59  foobar                                                                       + 60  ```                                                                          + 61   + 62      import this                                                              + 63   + 64   + 65  1. List item                                                                 + 66   + 67         Code block                                                            + 68   + @@ -29157,363 +29157,363 @@ font-weight: 700; } - .terminal-685151692-matrix { + .terminal-1658030963-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-685151692-title { + .terminal-1658030963-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-685151692-r1 { fill: #e4e5e6 } - .terminal-685151692-r2 { fill: #1b1b1b } - .terminal-685151692-r3 { fill: #f92672 } - .terminal-685151692-r4 { fill: #c5c8c6 } - .terminal-685151692-r5 { fill: #7b7e82 } - .terminal-685151692-r6 { fill: #e2e3e3 } - .terminal-685151692-r7 { fill: #75715e } - .terminal-685151692-r8 { fill: #e6db74 } - .terminal-685151692-r9 { fill: #ae81ff } - .terminal-685151692-r10 { fill: #a6e22e } + .terminal-1658030963-r1 { fill: #e4e5e6 } + .terminal-1658030963-r2 { fill: #151515 } + .terminal-1658030963-r3 { fill: #f92672 } + .terminal-1658030963-r4 { fill: #c5c8c6 } + .terminal-1658030963-r5 { fill: #86898c } + .terminal-1658030963-r6 { fill: #e2e3e3 } + .terminal-1658030963-r7 { fill: #75715e } + .terminal-1658030963-r8 { fill: #e6db74 } + .terminal-1658030963-r9 { fill: #ae81ff } + .terminal-1658030963-r10 { fill: #a6e22e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  import math                                                                  -  2  from os import path                                                          -  3   -  4  # I'm a comment :) -  5   -  6  string_var ="Hello, world!" -  7  int_var =42 -  8  float_var =3.14 -  9  complex_var =1+2j - 10   - 11  list_var = [12345]                                                   - 12  tuple_var = (12345)                                                  - 13  set_var = {12345}                                                    - 14  dict_var = {"a"1"b"2"c"3}                                          - 15   - 16  deffunction_no_args():                                                      - 17  return"No arguments" - 18   - 19  deffunction_with_args(a, b):                                                - 20  return a + b                                                             - 21   - 22  deffunction_with_default_args(a=0, b=0):                                    - 23  return a * b                                                             - 24   - 25  lambda_func =lambda x: x**2 - 26   - 27  if int_var ==42:                                                            - 28  print("It's the answer!")                                                - 29  elif int_var <42:                                                           - 30  print("Less than the answer.")                                           - 31  else:                                                                        - 32  print("Greater than the answer.")                                        - 33   - 34  for index, value inenumerate(list_var):                                     - 35  print(f"Index: {index}, Value: {value}")                                 - 36   - 37  counter =0 - 38  while counter <5:                                                           - 39  print(f"Counter value: {counter}")                                       - 40      counter +=1 - 41   - 42  squared_numbers = [x**2for x inrange(10if x %2==0]                    - 43   - 44  try:                                                                         - 45      result =10/0 - 46  except ZeroDivisionError:                                                    - 47  print("Cannot divide by zero!")                                          - 48  finally:                                                                     - 49  print("End of try-except block.")                                        - 50   - 51  class Animal:                                                                - 52  def__init__(self, name):                                                - 53          self.name = name                                                     - 54   - 55  defspeak(self):                                                         - 56  raiseNotImplementedError("Subclasses must implement this method." - 57   - 58  class Dog(Animal):                                                           - 59  defspeak(self):                                                         - 60  returnf"{self.name} says Woof!" - 61   - 62  deffibonacci(n):                                                            - 63      a, b =01 - 64  for _ inrange(n):                                                       - 65  yield a                                                              - 66          a, b = b, a + b                                                      - 67   - 68  for num infibonacci(5):                                                     - 69  print(num)                                                               - 70   - 71  withopen('test.txt''w'as f:                                             - 72      f.write("Testing with statement.")                                       - 73   - 74  @my_decorator                                                                - 75  defsay_hello():                                                             - 76  print("Hello!")                                                          - 77   - 78  say_hello()                                                                  - 79   + +  1  import math                                                                  +  2  from os import path                                                          +  3   +  4  # I'm a comment :) +  5   +  6  string_var ="Hello, world!" +  7  int_var =42 +  8  float_var =3.14 +  9  complex_var =1+2j + 10   + 11  list_var = [12345]                                                   + 12  tuple_var = (12345)                                                  + 13  set_var = {12345}                                                    + 14  dict_var = {"a"1"b"2"c"3}                                          + 15   + 16  deffunction_no_args():                                                      + 17  return"No arguments" + 18   + 19  deffunction_with_args(a, b):                                                + 20  return a + b                                                             + 21   + 22  deffunction_with_default_args(a=0, b=0):                                    + 23  return a * b                                                             + 24   + 25  lambda_func =lambda x: x**2 + 26   + 27  if int_var ==42:                                                            + 28  print("It's the answer!")                                                + 29  elif int_var <42:                                                           + 30  print("Less than the answer.")                                           + 31  else:                                                                        + 32  print("Greater than the answer.")                                        + 33   + 34  for index, value inenumerate(list_var):                                     + 35  print(f"Index: {index}, Value: {value}")                                 + 36   + 37  counter =0 + 38  while counter <5:                                                           + 39  print(f"Counter value: {counter}")                                       + 40      counter +=1 + 41   + 42  squared_numbers = [x**2for x inrange(10if x %2==0]                    + 43   + 44  try:                                                                         + 45      result =10/0 + 46  except ZeroDivisionError:                                                    + 47  print("Cannot divide by zero!")                                          + 48  finally:                                                                     + 49  print("End of try-except block.")                                        + 50   + 51  class Animal:                                                                + 52  def__init__(self, name):                                                + 53          self.name = name                                                     + 54   + 55  defspeak(self):                                                         + 56  raiseNotImplementedError("Subclasses must implement this method." + 57   + 58  class Dog(Animal):                                                           + 59  defspeak(self):                                                         + 60  returnf"{self.name} says Woof!" + 61   + 62  deffibonacci(n):                                                            + 63      a, b =01 + 64  for _ inrange(n):                                                       + 65  yield a                                                              + 66          a, b = b, a + b                                                      + 67   + 68  for num infibonacci(5):                                                     + 69  print(num)                                                               + 70   + 71  withopen('test.txt''w'as f:                                             + 72      f.write("Testing with statement.")                                       + 73   + 74  @my_decorator                                                                + 75  defsay_hello():                                                             + 76  print("Hello!")                                                          + 77   + 78  say_hello()                                                                  + 79   @@ -29544,145 +29544,145 @@ font-weight: 700; } - .terminal-3114921998-matrix { + .terminal-2329209973-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3114921998-title { + .terminal-2329209973-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3114921998-r1 { fill: #e4e5e6 } - .terminal-3114921998-r2 { fill: #1b1b1b } - .terminal-3114921998-r3 { fill: #c5c8c6 } - .terminal-3114921998-r4 { fill: #7b7e82 } - .terminal-3114921998-r5 { fill: #e2e3e3 } - .terminal-3114921998-r6 { fill: #f92672 } - .terminal-3114921998-r7 { fill: #23568b } + .terminal-2329209973-r1 { fill: #e4e5e6 } + .terminal-2329209973-r2 { fill: #151515 } + .terminal-2329209973-r3 { fill: #c5c8c6 } + .terminal-2329209973-r4 { fill: #86898c } + .terminal-2329209973-r5 { fill: #e2e3e3 } + .terminal-2329209973-r6 { fill: #f92672 } + .terminal-2329209973-r7 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  ^abc            # Matches any string that starts with "abc"                  -  2  abc$            # Matches any string that ends with "abc"                    -  3  ^abc$           # Matches the string "abc" and nothing else                  -  4  a.b             # Matches any string containing "a", any character, then "b" -  5  a[.]b           # Matches the string "a.b"                                   -  6  a|b             # Matches either "a" or "b"                                  -  7  a{2}            # Matches "aa"                                               -  8  a{2,}           # Matches two or more consecutive "a" characters             -  9  a{2,5}          # Matches between 2 and 5 consecutive "a" characters         - 10  a?              # Matches "a" or nothing (0 or 1 occurrence of "a") - 11  a*              # Matches zero or more consecutive "a" characters            - 12  a+              # Matches one or more consecutive "a" characters             - 13  \d              # Matches any digit (equivalent to [0-9]) - 14  \D              # Matches any non-digit                                      - 15  \w              # Matches any word character (equivalent to [a-zA-Z0-9_]) - 16  \W              # Matches any non-word character                             - 17  \s              # Matches any whitespace character (spaces, tabs, line break - 18  \S              # Matches any non-whitespace character                       - 19  (?i)abc         # Case-insensitive match for "abc"                           - 20  (?:a|b)         # Non-capturing group for either "a" or "b"                  - 21  (?<=a)b         # Positive lookbehind: matches "b" that is preceded by "a"   - 22  (?<!a)b         # Negative lookbehind: matches "b" that is not preceded by " - 23  a(?=b)          # Positive lookahead: matches "a" that is followed by "b"    - 24  a(?!b)          # Negative lookahead: matches "a" that is not followed by "b - 25   - + +  1  ^abc            # Matches any string that starts with "abc"                  +  2  abc$            # Matches any string that ends with "abc"                    +  3  ^abc$           # Matches the string "abc" and nothing else                  +  4  a.b             # Matches any string containing "a", any character, then "b" +  5  a[.]b           # Matches the string "a.b"                                   +  6  a|b             # Matches either "a" or "b"                                  +  7  a{2}            # Matches "aa"                                               +  8  a{2,}           # Matches two or more consecutive "a" characters             +  9  a{2,5}          # Matches between 2 and 5 consecutive "a" characters         + 10  a?              # Matches "a" or nothing (0 or 1 occurrence of "a") + 11  a*              # Matches zero or more consecutive "a" characters            + 12  a+              # Matches one or more consecutive "a" characters             + 13  \d              # Matches any digit (equivalent to [0-9]) + 14  \D              # Matches any non-digit                                      + 15  \w              # Matches any word character (equivalent to [a-zA-Z0-9_]) + 16  \W              # Matches any non-word character                             + 17  \s              # Matches any whitespace character (spaces, tabs, line break + 18  \S              # Matches any non-whitespace character                       + 19  (?i)abc         # Case-insensitive match for "abc"                           + 20  (?:a|b)         # Non-capturing group for either "a" or "b"                  + 21  (?<=a)b         # Positive lookbehind: matches "b" that is preceded by "a"   + 22  (?<!a)b         # Negative lookbehind: matches "b" that is not preceded by " + 23  a(?=b)          # Positive lookahead: matches "a" that is followed by "b"    + 24  a(?!b)          # Negative lookahead: matches "a" that is not followed by "b + 25   + @@ -29712,222 +29712,222 @@ font-weight: 700; } - .terminal-201148347-matrix { + .terminal-4125966786-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-201148347-title { + .terminal-4125966786-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-201148347-r1 { fill: #e4e5e6 } - .terminal-201148347-r2 { fill: #1b1b1b } - .terminal-201148347-r3 { fill: #75715e } - .terminal-201148347-r4 { fill: #c5c8c6 } - .terminal-201148347-r5 { fill: #7b7e82 } - .terminal-201148347-r6 { fill: #e2e3e3 } - .terminal-201148347-r7 { fill: #f92672 } - .terminal-201148347-r8 { fill: #ae81ff } - .terminal-201148347-r9 { fill: #e6db74 } + .terminal-4125966786-r1 { fill: #e4e5e6 } + .terminal-4125966786-r2 { fill: #151515 } + .terminal-4125966786-r3 { fill: #75715e } + .terminal-4125966786-r4 { fill: #c5c8c6 } + .terminal-4125966786-r5 { fill: #86898c } + .terminal-4125966786-r6 { fill: #e2e3e3 } + .terminal-4125966786-r7 { fill: #f92672 } + .terminal-4125966786-r8 { fill: #ae81ff } + .terminal-4125966786-r9 { fill: #e6db74 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  -- This is a comment in SQL -  2   -  3  -- Create tables -  4  CREATETABLE Authors (                                                       -  5      AuthorID INT PRIMARY KEY,                                                -  6      Name VARCHAR(255NOT NULL,                                              -  7      Country VARCHAR(50)                                                      -  8  );                                                                           -  9   - 10  CREATETABLE Books (                                                         - 11      BookID INT PRIMARY KEY,                                                  - 12      Title VARCHAR(255NOT NULL,                                             - 13      AuthorID INT,                                                            - 14      PublishedDate DATE,                                                      - 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)                      - 16  );                                                                           - 17   - 18  -- Insert data - 19  INSERTINTO Authors (AuthorID, Name, Country) VALUES (1'George Orwell''U - 20   - 21  INSERTINTO Books (BookID, Title, AuthorID, PublishedDate) VALUES (1'1984' - 22   - 23  -- Update data - 24  UPDATE Authors SET Country ='United Kingdom'WHERE Country ='UK';          - 25   - 26  -- Select data with JOIN - 27  SELECT Books.Title, Authors.Name                                             - 28  FROM Books                                                                   - 29  JOIN Authors ON Books.AuthorID = Authors.AuthorID;                           - 30   - 31  -- Delete data (commented to preserve data for other examples) - 32  -- DELETE FROM Books WHERE BookID = 1; - 33   - 34  -- Alter table structure - 35  ALTER TABLE Authors ADD COLUMN BirthDate DATE;                               - 36   - 37  -- Create index - 38  CREATEINDEX idx_author_name ON Authors(Name);                               - 39   - 40  -- Drop index (commented to avoid actually dropping it) - 41  -- DROP INDEX idx_author_name ON Authors; - 42   - 43  -- End of script - 44   + +  1  -- This is a comment in SQL +  2   +  3  -- Create tables +  4  CREATETABLE Authors (                                                       +  5      AuthorID INT PRIMARY KEY,                                                +  6      Name VARCHAR(255NOT NULL,                                              +  7      Country VARCHAR(50)                                                      +  8  );                                                                           +  9   + 10  CREATETABLE Books (                                                         + 11      BookID INT PRIMARY KEY,                                                  + 12      Title VARCHAR(255NOT NULL,                                             + 13      AuthorID INT,                                                            + 14      PublishedDate DATE,                                                      + 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)                      + 16  );                                                                           + 17   + 18  -- Insert data + 19  INSERTINTO Authors (AuthorID, Name, Country) VALUES (1'George Orwell''U + 20   + 21  INSERTINTO Books (BookID, Title, AuthorID, PublishedDate) VALUES (1'1984' + 22   + 23  -- Update data + 24  UPDATE Authors SET Country ='United Kingdom'WHERE Country ='UK';          + 25   + 26  -- Select data with JOIN + 27  SELECT Books.Title, Authors.Name                                             + 28  FROM Books                                                                   + 29  JOIN Authors ON Books.AuthorID = Authors.AuthorID;                           + 30   + 31  -- Delete data (commented to preserve data for other examples) + 32  -- DELETE FROM Books WHERE BookID = 1; + 33   + 34  -- Alter table structure + 35  ALTER TABLE Authors ADD COLUMN BirthDate DATE;                               + 36   + 37  -- Create index + 38  CREATEINDEX idx_author_name ON Authors(Name);                               + 39   + 40  -- Drop index (commented to avoid actually dropping it) + 41  -- DROP INDEX idx_author_name ON Authors; + 42   + 43  -- End of script + 44   @@ -29958,151 +29958,151 @@ font-weight: 700; } - .terminal-618639109-matrix { + .terminal-1180609356-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-618639109-title { + .terminal-1180609356-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-618639109-r1 { fill: #e4e5e6 } - .terminal-618639109-r2 { fill: #1b1b1b } - .terminal-618639109-r3 { fill: #75715e } - .terminal-618639109-r4 { fill: #c5c8c6 } - .terminal-618639109-r5 { fill: #7b7e82 } - .terminal-618639109-r6 { fill: #e2e3e3 } - .terminal-618639109-r7 { fill: #f92672 } - .terminal-618639109-r8 { fill: #e6db74 } - .terminal-618639109-r9 { fill: #ae81ff } - .terminal-618639109-r10 { fill: #66d9ef;font-style: italic; } + .terminal-1180609356-r1 { fill: #e4e5e6 } + .terminal-1180609356-r2 { fill: #151515 } + .terminal-1180609356-r3 { fill: #75715e } + .terminal-1180609356-r4 { fill: #c5c8c6 } + .terminal-1180609356-r5 { fill: #86898c } + .terminal-1180609356-r6 { fill: #e2e3e3 } + .terminal-1180609356-r7 { fill: #f92672 } + .terminal-1180609356-r8 { fill: #e6db74 } + .terminal-1180609356-r9 { fill: #ae81ff } + .terminal-1180609356-r10 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  # This is a comment in TOML -  2   -  3  string = "Hello, world!" -  4  integer = 42 -  5  float = 3.14 -  6  boolean = true -  7  datetime = 1979-05-27T07:32:00Z -  8   -  9  fruits = ["apple""banana""cherry" - 10   - 11  [address]                               - 12  street = "123 Main St" - 13  city = "Anytown" - 14  state = "CA" - 15  zip = "12345" - 16   - 17  [person.john]                           - 18  name = "John Doe" - 19  age = 28 - 20  is_student = false - 21   - 22   - 23  [[animals]]                             - 24  name = "Fido" - 25  type = "dog" - 26   + +  1  # This is a comment in TOML +  2   +  3  string = "Hello, world!" +  4  integer = 42 +  5  float = 3.14 +  6  boolean = true +  7  datetime = 1979-05-27T07:32:00Z +  8   +  9  fruits = ["apple""banana""cherry" + 10   + 11  [address]                               + 12  street = "123 Main St" + 13  city = "Anytown" + 14  state = "CA" + 15  zip = "12345" + 16   + 17  [person.john]                           + 18  name = "John Doe" + 19  age = 28 + 20  is_student = false + 21   + 22   + 23  [[animals]]                             + 24  name = "Fido" + 25  type = "dog" + 26   @@ -30133,199 +30133,199 @@ font-weight: 700; } - .terminal-3785768015-matrix { + .terminal-1644508950-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3785768015-title { + .terminal-1644508950-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3785768015-r1 { fill: #e4e5e6 } - .terminal-3785768015-r2 { fill: #1b1b1b } - .terminal-3785768015-r3 { fill: #75715e } - .terminal-3785768015-r4 { fill: #c5c8c6 } - .terminal-3785768015-r5 { fill: #7b7e82 } - .terminal-3785768015-r6 { fill: #e2e3e3 } - .terminal-3785768015-r7 { fill: #f92672;font-weight: bold } - .terminal-3785768015-r8 { fill: #e6db74 } - .terminal-3785768015-r9 { fill: #ae81ff } - .terminal-3785768015-r10 { fill: #66d9ef;font-style: italic; } + .terminal-1644508950-r1 { fill: #e4e5e6 } + .terminal-1644508950-r2 { fill: #151515 } + .terminal-1644508950-r3 { fill: #75715e } + .terminal-1644508950-r4 { fill: #c5c8c6 } + .terminal-1644508950-r5 { fill: #86898c } + .terminal-1644508950-r6 { fill: #e2e3e3 } + .terminal-1644508950-r7 { fill: #f92672;font-weight: bold } + .terminal-1644508950-r8 { fill: #e6db74 } + .terminal-1644508950-r9 { fill: #ae81ff } + .terminal-1644508950-r10 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  # This is a comment in YAML -  2   -  3  # Scalars -  4  string"Hello, world!" -  5  integer42 -  6  float3.14 -  7  booleantrue -  8   -  9  # Sequences (Arrays) - 10  fruits:                                               - 11    - Apple - 12    - Banana - 13    - Cherry - 14   - 15  # Nested sequences - 16  persons:                                              - 17    - nameJohn - 18  age28 - 19  is_studentfalse - 20    - nameJane - 21  age22 - 22  is_studenttrue - 23   - 24  # Mappings (Dictionaries) - 25  address:                                              - 26  street123 Main St - 27  cityAnytown - 28  stateCA - 29  zip'12345' - 30   - 31  # Multiline string - 32  description| - 33    This is a multiline - 34    string in YAML. - 35   - 36  # Inline and nested collections - 37  colors: { redFF0000green00FF00blue0000FF }  - 38   + +  1  # This is a comment in YAML +  2   +  3  # Scalars +  4  string"Hello, world!" +  5  integer42 +  6  float3.14 +  7  booleantrue +  8   +  9  # Sequences (Arrays) + 10  fruits:                                               + 11    - Apple + 12    - Banana + 13    - Cherry + 14   + 15  # Nested sequences + 16  persons:                                              + 17    - nameJohn + 18  age28 + 19  is_studentfalse + 20    - nameJane + 21  age22 + 22  is_studenttrue + 23   + 24  # Mappings (Dictionaries) + 25  address:                                              + 26  street123 Main St + 27  cityAnytown + 28  stateCA + 29  zip'12345' + 30   + 31  # Multiline string + 32  description| + 33    This is a multiline + 34    string in YAML. + 35   + 36  # Inline and nested collections + 37  colors: { redFF0000green00FF00blue0000FF }  + 38   @@ -30356,61 +30356,61 @@ font-weight: 700; } - .terminal-1515009057-matrix { + .terminal-3971625736-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1515009057-title { + .terminal-3971625736-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1515009057-r1 { fill: #7b7e82 } - .terminal-1515009057-r2 { fill: #dde6ed } - .terminal-1515009057-r3 { fill: #e2e3e3 } - .terminal-1515009057-r4 { fill: #c5c8c6 } - .terminal-1515009057-r5 { fill: #004578 } - .terminal-1515009057-r6 { fill: #e4e5e6 } - .terminal-1515009057-r7 { fill: #1b1b1b } + .terminal-3971625736-r1 { fill: #86898c } + .terminal-3971625736-r2 { fill: #dde6ed } + .terminal-3971625736-r3 { fill: #e2e3e3 } + .terminal-3971625736-r4 { fill: #c5c8c6 } + .terminal-3971625736-r5 { fill: #004578 } + .terminal-3971625736-r6 { fill: #e4e5e6 } + .terminal-3971625736-r7 { fill: #151515 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - - 1  I am a line. - 2   - 3  I am another line.          - 4   - 5  I am the final line.  + + 1  I am a line. + 2   + 3  I am another line.          + 4   + 5  I am the final line.  @@ -30440,61 +30440,61 @@ font-weight: 700; } - .terminal-3458068619-matrix { + .terminal-572256114-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3458068619-title { + .terminal-572256114-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3458068619-r1 { fill: #e4e5e6 } - .terminal-3458068619-r2 { fill: #1b1b1b } - .terminal-3458068619-r3 { fill: #dde6ed } - .terminal-3458068619-r4 { fill: #c5c8c6 } - .terminal-3458068619-r5 { fill: #7b7e82 } - .terminal-3458068619-r6 { fill: #004578 } - .terminal-3458068619-r7 { fill: #e2e3e3 } + .terminal-572256114-r1 { fill: #e4e5e6 } + .terminal-572256114-r2 { fill: #151515 } + .terminal-572256114-r3 { fill: #dde6ed } + .terminal-572256114-r4 { fill: #c5c8c6 } + .terminal-572256114-r5 { fill: #86898c } + .terminal-572256114-r6 { fill: #004578 } + .terminal-572256114-r7 { fill: #e2e3e3 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - - 1  I am a line. - 2   - 3  I am another line.    - 4   - 5  I am the final line.  + + 1  I am a line. + 2   + 3  I am another line.    + 4   + 5  I am the final line.  @@ -30524,61 +30524,61 @@ font-weight: 700; } - .terminal-1823421656-matrix { + .terminal-247215039-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1823421656-title { + .terminal-247215039-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1823421656-r1 { fill: #e4e5e6 } - .terminal-1823421656-r2 { fill: #1b1b1b } - .terminal-1823421656-r3 { fill: #dde6ed } - .terminal-1823421656-r4 { fill: #c5c8c6 } - .terminal-1823421656-r5 { fill: #7b7e82 } - .terminal-1823421656-r6 { fill: #004578 } - .terminal-1823421656-r7 { fill: #e2e3e3 } + .terminal-247215039-r1 { fill: #e4e5e6 } + .terminal-247215039-r2 { fill: #151515 } + .terminal-247215039-r3 { fill: #dde6ed } + .terminal-247215039-r4 { fill: #c5c8c6 } + .terminal-247215039-r5 { fill: #86898c } + .terminal-247215039-r6 { fill: #004578 } + .terminal-247215039-r7 { fill: #e2e3e3 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - - 1  I am a line. - 2   - 3  I am another line. - 4   - 5  I am the final line.  + + 1  I am a line. + 2   + 3  I am another line. + 4   + 5  I am the final line.  @@ -30608,61 +30608,61 @@ font-weight: 700; } - .terminal-4159010911-matrix { + .terminal-425817926-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4159010911-title { + .terminal-425817926-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4159010911-r1 { fill: #7b7e82 } - .terminal-4159010911-r2 { fill: #dde6ed } - .terminal-4159010911-r3 { fill: #e2e3e3 } - .terminal-4159010911-r4 { fill: #c5c8c6 } - .terminal-4159010911-r5 { fill: #004578 } - .terminal-4159010911-r6 { fill: #e4e5e6 } - .terminal-4159010911-r7 { fill: #1b1b1b } + .terminal-425817926-r1 { fill: #86898c } + .terminal-425817926-r2 { fill: #dde6ed } + .terminal-425817926-r3 { fill: #e2e3e3 } + .terminal-425817926-r4 { fill: #c5c8c6 } + .terminal-425817926-r5 { fill: #004578 } + .terminal-425817926-r6 { fill: #e4e5e6 } + .terminal-425817926-r7 { fill: #151515 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - - 1  I am a line. - 2   - 3  I am another line. - 4   - 5  I am the final line. + + 1  I am a line. + 2   + 3  I am another line. + 4   + 5  I am the final line. @@ -30692,59 +30692,59 @@ font-weight: 700; } - .terminal-2537073741-matrix { + .terminal-1848814388-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2537073741-title { + .terminal-1848814388-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2537073741-r1 { fill: #7b7e82 } - .terminal-2537073741-r2 { fill: #e2e3e3 } - .terminal-2537073741-r3 { fill: #c5c8c6 } - .terminal-2537073741-r4 { fill: #e4e5e6 } - .terminal-2537073741-r5 { fill: #1b1b1b } + .terminal-1848814388-r1 { fill: #86898c } + .terminal-1848814388-r2 { fill: #e2e3e3 } + .terminal-1848814388-r3 { fill: #c5c8c6 } + .terminal-1848814388-r4 { fill: #e4e5e6 } + .terminal-1848814388-r5 { fill: #151515 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - - 1  I am a line.          - 2   - 3  I am another line.    - 4   - 5  I am the final line.  + + 1  I am a line.          + 2   + 3  I am another line.    + 4   + 5  I am the final line.  @@ -30774,59 +30774,59 @@ font-weight: 700; } - .terminal-3397495885-matrix { + .terminal-2863377204-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3397495885-title { + .terminal-2863377204-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3397495885-r1 { fill: #7b7e82 } - .terminal-3397495885-r2 { fill: #e2e3e3 } - .terminal-3397495885-r3 { fill: #c5c8c6 } - .terminal-3397495885-r4 { fill: #e4e5e6 } - .terminal-3397495885-r5 { fill: #1b1b1b } + .terminal-2863377204-r1 { fill: #86898c } + .terminal-2863377204-r2 { fill: #e2e3e3 } + .terminal-2863377204-r3 { fill: #c5c8c6 } + .terminal-2863377204-r4 { fill: #e4e5e6 } + .terminal-2863377204-r5 { fill: #151515 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - - 1  I am a line.          - 2   - 3  I am another line.          - 4   - 5  I am the final line.  + + 1  I am a line.          + 2   + 3  I am another line.          + 4   + 5  I am the final line.  From cc136fd72e1a798ad5a23edfa17546947d361a6f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Aug 2023 14:40:17 +0100 Subject: [PATCH 263/366] A whole bunch of TextArea testing --- tests/text_area/test_selection.py | 230 +++------------- tests/text_area/test_selection_bindings.py | 294 +++++++++++++++++++++ 2 files changed, 333 insertions(+), 191 deletions(-) create mode 100644 tests/text_area/test_selection_bindings.py diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index fddeab5f66..968aba1aed 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -1,8 +1,7 @@ import pytest from textual.app import App, ComposeResult -from textual.document import Document, Selection -from textual.geometry import Offset +from textual.document import Selection from textual.widgets import TextArea TEXT = """I must not fear. @@ -34,6 +33,27 @@ async def test_cursor_location_get(): async def test_cursor_location_set(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + target = (1, 2) + text_area.cursor_location = target + assert text_area.selection == Selection.cursor(target) + + +async def test_cursor_location_set_while_selecting(): + """If you set the cursor_location while a selection is in progress, + the start/anchor point of the selection will remain where it is.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((0, 0), (0, 2)) + target = (1, 2) + text_area.cursor_location = target + assert text_area.selection == Selection((0, 0), target) + + +async def test_move_cursor_select(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) @@ -42,6 +62,21 @@ async def test_cursor_location_set(): assert text_area.selection == Selection((1, 1), (2, 3)) +async def test_move_cursor_relative(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + + text_area.move_cursor_relative(rows=1, columns=2) + assert text_area.selection == Selection.cursor((1, 2)) + + text_area.move_cursor_relative(rows=-1, columns=-2) + assert text_area.selection == Selection.cursor((0, 0)) + + text_area.move_cursor_relative(rows=1000, columns=1000) + assert text_area.selection == Selection.cursor((4, 0)) + + async def test_selected_text_forward(): """Selecting text from top to bottom results in the correct selected_text.""" app = TextAreaApp() @@ -90,150 +125,6 @@ async def test_selection_clamp(): assert text_area.selection == Selection(start=(4, 0), end=(4, 0)) -async def test_mouse_click(): - """When you click the TextArea, the cursor moves to the expected location.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - await pilot.click(TextArea, Offset(x=5, y=2)) - assert text_area.selection == Selection.cursor((2, 2)) - - -async def test_mouse_click_clamp_from_right(): - """When you click to the right of the document bounds, the cursor is clamped - to within the document bounds.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - await pilot.click(TextArea, Offset(x=8, y=20)) - assert text_area.selection == Selection.cursor((4, 0)) - - -async def test_mouse_click_gutter_clamp(): - """When you click the gutter, it selects the start of the line.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - await pilot.click(TextArea, Offset(x=0, y=3)) - assert text_area.selection == Selection.cursor((3, 0)) - - -async def test_cursor_selection_right(): - """When you press shift+right the selection is updated correctly.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - await pilot.press(*["shift+right"] * 3) - assert text_area.selection == Selection((0, 0), (0, 3)) - - -async def test_cursor_selection_right_to_previous_line(): - """When you press shift+right resulting in the cursor moving to the next line, - the selection is updated correctly.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.selection = Selection.cursor((0, 15)) - await pilot.press(*["shift+right"] * 4) - assert text_area.selection == Selection((0, 15), (1, 2)) - - -async def test_cursor_selection_left(): - """When you press shift+left the selection is updated correctly.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.selection = Selection.cursor((2, 5)) - await pilot.press(*["shift+left"] * 3) - assert text_area.selection == Selection((2, 5), (2, 2)) - - -async def test_cursor_selection_left_to_previous_line(): - """When you press shift+left resulting in the cursor moving back to the previous line, - the selection is updated correctly.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.selection = Selection.cursor((2, 2)) - await pilot.press(*["shift+left"] * 3) - - # The cursor jumps up to the end of the line above. - end_of_previous_line = len(TEXT.splitlines()[1]) - assert text_area.selection == Selection((2, 2), (1, end_of_previous_line)) - - -async def test_cursor_selection_up(): - """When you press shift+up the selection is updated correctly.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.move_cursor((2, 3)) - - await pilot.press("shift+up") - assert text_area.selection == Selection((2, 3), (1, 3)) - - -async def test_cursor_selection_up_when_cursor_on_first_line(): - """When you press shift+up the on the first line, it selects to the start.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.move_cursor((0, 4)) - - await pilot.press("shift+up") - assert text_area.selection == Selection((0, 4), (0, 0)) - await pilot.press("shift+up") - assert text_area.selection == Selection((0, 4), (0, 0)) - - -async def test_cursor_selection_down(): - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.move_cursor((2, 5)) - - await pilot.press("shift+down") - assert text_area.selection == Selection((2, 5), (3, 5)) - - -async def test_cursor_selection_down_when_cursor_on_last_line(): - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.load_text("ABCDEF\nGHIJK") - text_area.move_cursor((1, 2)) - - await pilot.press("shift+down") - assert text_area.selection == Selection((1, 2), (1, 5)) - await pilot.press("shift+down") - assert text_area.selection == Selection((1, 2), (1, 5)) - - -@pytest.mark.parametrize("key", ["end", "ctrl+e"]) -async def test_cursor_to_line_end(key): - """You can use the keyboard to jump the cursor to the end of the current line.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.selection = Selection.cursor((2, 2)) - await pilot.press(key) - eol_index = len(TEXT.splitlines()[2]) - assert text_area.cursor_location == (2, eol_index) - assert text_area.selection.is_empty - - -@pytest.mark.parametrize("key", ["home", "ctrl+a"]) -async def test_cursor_to_line_home(key): - """You can use the keyboard to jump the cursor to the start of the current line.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.selection = Selection.cursor((2, 2)) - await pilot.press(key) - assert text_area.cursor_location == (2, 0) - assert text_area.selection.is_empty - - @pytest.mark.parametrize( "start,end", [ @@ -256,6 +147,7 @@ async def test_get_cursor_left_location(start, end): ((0, 0), (0, 1)), ((0, 16), (1, 0)), ((3, 20), (4, 0)), + ((4, 0), (4, 0)), ], ) async def test_get_cursor_right_location(start, end): @@ -364,51 +256,6 @@ async def test_cursor_word_right_location(start, end): assert text_area.get_cursor_word_right_location() == end -async def test_cursor_page_down(): - """Pagedown moves the cursor down 1 page, retaining column index.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.load_text("XXX\n" * 200) - text_area.selection = Selection.cursor((0, 1)) - await pilot.press("pagedown") - assert text_area.selection == Selection.cursor((app.console.height - 1, 1)) - - -async def test_cursor_page_up(): - """Pageup moves the cursor up 1 page, retaining column index.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.load_text("XXX\n" * 200) - text_area.selection = Selection.cursor((100, 1)) - await pilot.press("pageup") - assert text_area.selection == Selection.cursor( - (100 - app.console.height + 1, 1) - ) - - -async def test_cursor_vertical_movement_visual_alignment_snapping(): - """When you move the cursor vertically, it should stay vertically - aligned even when double-width characters are used.""" - app = TextAreaApp() - async with app.run_test() as pilot: - text_area = app.query_one(TextArea) - text_area.load_document(Document("こんにちは\n012345")) - text_area.move_cursor((1, 3), record_width=True) - - # The '3' is aligned with ん at (0, 1) - # こんにちは - # 012345 - # Pressing `up` takes us from (1, 3) to (0, 1) because record_width=True. - await pilot.press("up") - assert text_area.selection == Selection.cursor((0, 1)) - - # Pressing `down` takes us from (0, 1) to (1, 3) - await pilot.press("down") - assert text_area.selection == Selection.cursor((1, 3)) - - @pytest.mark.parametrize( "content,expected_selection", [ @@ -434,6 +281,7 @@ async def test_select_all(content, expected_selection): (1, "123\n456\n789\n", Selection((1, 0), (1, 3))), (2, "123\n456\n789\n", Selection((2, 0), (2, 3))), (3, "123\n456\n789\n", Selection((3, 0), (3, 0))), + (1000, "123\n456\n789\n", Selection.cursor((0, 0))), (0, "", Selection((0, 0), (0, 0))), ], ) diff --git a/tests/text_area/test_selection_bindings.py b/tests/text_area/test_selection_bindings.py new file mode 100644 index 0000000000..2363befe31 --- /dev/null +++ b/tests/text_area/test_selection_bindings.py @@ -0,0 +1,294 @@ +import pytest + +from textual.app import App, ComposeResult +from textual.document import Document, Selection +from textual.geometry import Offset +from textual.widgets import TextArea + +TEXT = """I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.load_text(TEXT) + yield text_area + + +async def test_mouse_click(): + """When you click the TextArea, the cursor moves to the expected location.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(TextArea, Offset(x=5, y=2)) + assert text_area.selection == Selection.cursor((2, 2)) + + +async def test_mouse_click_clamp_from_right(): + """When you click to the right of the document bounds, the cursor is clamped + to within the document bounds.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(TextArea, Offset(x=8, y=20)) + assert text_area.selection == Selection.cursor((4, 0)) + + +async def test_mouse_click_gutter_clamp(): + """When you click the gutter, it selects the start of the line.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(TextArea, Offset(x=0, y=3)) + assert text_area.selection == Selection.cursor((3, 0)) + + +async def test_cursor_movement_basic(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("01234567\n012345\n0123456789") + + await pilot.press("right") + assert text_area.selection == Selection.cursor((0, 1)) + + await pilot.press("down") + assert text_area.selection == Selection.cursor((1, 1)) + + await pilot.press("left") + assert text_area.selection == Selection.cursor((1, 0)) + + await pilot.press("up") + assert text_area.selection == Selection.cursor((0, 0)) + + +async def test_cursor_selection_right(): + """When you press shift+right the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.press(*["shift+right"] * 3) + assert text_area.selection == Selection((0, 0), (0, 3)) + + +async def test_cursor_selection_right_to_previous_line(): + """When you press shift+right resulting in the cursor moving to the next line, + the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((0, 15)) + await pilot.press(*["shift+right"] * 4) + assert text_area.selection == Selection((0, 15), (1, 2)) + + +async def test_cursor_selection_left(): + """When you press shift+left the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 5)) + await pilot.press(*["shift+left"] * 3) + assert text_area.selection == Selection((2, 5), (2, 2)) + + +async def test_cursor_selection_left_to_previous_line(): + """When you press shift+left resulting in the cursor moving back to the previous line, + the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 2)) + await pilot.press(*["shift+left"] * 3) + + # The cursor jumps up to the end of the line above. + end_of_previous_line = len(TEXT.splitlines()[1]) + assert text_area.selection == Selection((2, 2), (1, end_of_previous_line)) + + +async def test_cursor_selection_up(): + """When you press shift+up the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.move_cursor((2, 3)) + + await pilot.press("shift+up") + assert text_area.selection == Selection((2, 3), (1, 3)) + + +async def test_cursor_selection_up_when_cursor_on_first_line(): + """When you press shift+up the on the first line, it selects to the start.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.move_cursor((0, 4)) + + await pilot.press("shift+up") + assert text_area.selection == Selection((0, 4), (0, 0)) + await pilot.press("shift+up") + assert text_area.selection == Selection((0, 4), (0, 0)) + + +async def test_cursor_selection_down(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.move_cursor((2, 5)) + + await pilot.press("shift+down") + assert text_area.selection == Selection((2, 5), (3, 5)) + + +async def test_cursor_selection_down_when_cursor_on_last_line(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("ABCDEF\nGHIJK") + text_area.move_cursor((1, 2)) + + await pilot.press("shift+down") + assert text_area.selection == Selection((1, 2), (1, 5)) + await pilot.press("shift+down") + assert text_area.selection == Selection((1, 2), (1, 5)) + + +async def test_cursor_word_right(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("ABC DEF\nGHIJK") + + await pilot.press("ctrl+right") + + assert text_area.selection == Selection.cursor((0, 3)) + + +async def test_cursor_word_right_select(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("ABC DEF\nGHIJK") + + await pilot.press("ctrl+shift+right") + + assert text_area.selection == Selection((0, 0), (0, 3)) + + +async def test_cursor_word_left(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("ABC DEF\nGHIJK") + text_area.move_cursor((0, 7)) + + await pilot.press("ctrl+left") + + assert text_area.selection == Selection.cursor((0, 4)) + + +async def test_cursor_word_left_select(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("ABC DEF\nGHIJK") + text_area.move_cursor((0, 7)) + + await pilot.press("ctrl+shift+left") + + assert text_area.selection == Selection((0, 7), (0, 4)) + + +@pytest.mark.parametrize("key", ["end", "ctrl+e"]) +async def test_cursor_to_line_end(key): + """You can use the keyboard to jump the cursor to the end of the current line.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 2)) + await pilot.press(key) + eol_index = len(TEXT.splitlines()[2]) + assert text_area.cursor_location == (2, eol_index) + assert text_area.selection.is_empty + + +@pytest.mark.parametrize("key", ["home", "ctrl+a"]) +async def test_cursor_to_line_home(key): + """You can use the keyboard to jump the cursor to the start of the current line.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 2)) + await pilot.press(key) + assert text_area.cursor_location == (2, 0) + assert text_area.selection.is_empty + + +async def test_cursor_page_down(): + """Pagedown moves the cursor down 1 page, retaining column index.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("XXX\n" * 200) + text_area.selection = Selection.cursor((0, 1)) + await pilot.press("pagedown") + assert text_area.selection == Selection.cursor((app.console.height - 1, 1)) + + +async def test_cursor_page_up(): + """Pageup moves the cursor up 1 page, retaining column index.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("XXX\n" * 200) + text_area.selection = Selection.cursor((100, 1)) + await pilot.press("pageup") + assert text_area.selection == Selection.cursor( + (100 - app.console.height + 1, 1) + ) + + +async def test_cursor_vertical_movement_visual_alignment_snapping(): + """When you move the cursor vertically, it should stay vertically + aligned even when double-width characters are used.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_document(Document("こんにちは\n012345")) + text_area.move_cursor((1, 3), record_width=True) + + # The '3' is aligned with ん at (0, 1) + # こんにちは + # 012345 + # Pressing `up` takes us from (1, 3) to (0, 1) because record_width=True. + await pilot.press("up") + assert text_area.selection == Selection.cursor((0, 1)) + + # Pressing `down` takes us from (0, 1) to (1, 3) + await pilot.press("down") + assert text_area.selection == Selection.cursor((1, 3)) + + +async def test_select_line_binding(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.move_cursor((2, 2)) + + await pilot.press("f6") + + assert text_area.selection == Selection((2, 0), (2, 56)) + + +async def test_select_all_binding(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + + await pilot.press("f7") + + assert text_area.selection == Selection((0, 0), (4, 0)) From fc76403cd7b1c282224374fc7c2891835abb1096 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Aug 2023 14:43:18 +0100 Subject: [PATCH 264/366] Simplify delete_left and delete_right --- src/textual/widgets/_text_area.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index d78ca5b08f..8877c86204 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1143,18 +1143,10 @@ def action_delete_left(self) -> None: If there's a selection, then the selected range is deleted.""" selection = self.selection - start, end = selection - end_row, end_column = end if selection.is_empty: - if self.cursor_at_start_of_document: - return - - if self.cursor_at_start_of_row: - end = (end_row - 1, len(self.document[end_row - 1])) - else: - end = (end_row, end_column - 1) + end = self.get_cursor_left_location() self.delete(start, end, maintain_selection_offset=False) @@ -1165,15 +1157,9 @@ def action_delete_right(self) -> None: selection = self.selection start, end = selection - end_row, end_column = end if selection.is_empty: - if self.cursor_at_end_of_document: - return - if self.cursor_at_end_of_row: - end = (end_row + 1, 0) - else: - end = (end_row, end_column + 1) + end = self.get_cursor_right_location() self.delete(start, end, maintain_selection_offset=False) From afad2076f81a62c58e423190d08d8b529f24da4e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Aug 2023 15:00:37 +0100 Subject: [PATCH 265/366] Testing hiding line numbers in snapshot --- .../__snapshots__/test_snapshots.ambr | 262 +++++++++--------- tests/snapshot_tests/test_snapshots.py | 1 + 2 files changed, 129 insertions(+), 134 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 52b3e24053..63c4b81c46 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -30356,61 +30356,60 @@ font-weight: 700; } - .terminal-3971625736-matrix { + .terminal-488877296-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3971625736-title { + .terminal-488877296-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3971625736-r1 { fill: #86898c } - .terminal-3971625736-r2 { fill: #dde6ed } - .terminal-3971625736-r3 { fill: #e2e3e3 } - .terminal-3971625736-r4 { fill: #c5c8c6 } - .terminal-3971625736-r5 { fill: #004578 } - .terminal-3971625736-r6 { fill: #e4e5e6 } - .terminal-3971625736-r7 { fill: #151515 } + .terminal-488877296-r1 { fill: #e2e3e3 } + .terminal-488877296-r2 { fill: #dde6ed } + .terminal-488877296-r3 { fill: #c5c8c6 } + .terminal-488877296-r4 { fill: #004578 } + .terminal-488877296-r5 { fill: #151515 } + .terminal-488877296-r6 { fill: #e4e5e6 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  I am a line. - 2   - 3  I am another line.          - 4   - 5  I am the final line.  + + + + I am a line. + + I am another line.             + + I am the final line.  @@ -30440,61 +30439,60 @@ font-weight: 700; } - .terminal-572256114-matrix { + .terminal-291251717-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-572256114-title { + .terminal-291251717-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-572256114-r1 { fill: #e4e5e6 } - .terminal-572256114-r2 { fill: #151515 } - .terminal-572256114-r3 { fill: #dde6ed } - .terminal-572256114-r4 { fill: #c5c8c6 } - .terminal-572256114-r5 { fill: #86898c } - .terminal-572256114-r6 { fill: #004578 } - .terminal-572256114-r7 { fill: #e2e3e3 } + .terminal-291251717-r1 { fill: #e2e3e3 } + .terminal-291251717-r2 { fill: #151515 } + .terminal-291251717-r3 { fill: #dde6ed } + .terminal-291251717-r4 { fill: #e4e5e6 } + .terminal-291251717-r5 { fill: #c5c8c6 } + .terminal-291251717-r6 { fill: #004578 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  I am a line. - 2   - 3  I am another line.    - 4   - 5  I am the final line.  + + + + I am a line. + + I am another line.    + + I am the final line.  @@ -30524,61 +30522,60 @@ font-weight: 700; } - .terminal-247215039-matrix { + .terminal-1080619275-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-247215039-title { + .terminal-1080619275-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-247215039-r1 { fill: #e4e5e6 } - .terminal-247215039-r2 { fill: #151515 } - .terminal-247215039-r3 { fill: #dde6ed } - .terminal-247215039-r4 { fill: #c5c8c6 } - .terminal-247215039-r5 { fill: #86898c } - .terminal-247215039-r6 { fill: #004578 } - .terminal-247215039-r7 { fill: #e2e3e3 } + .terminal-1080619275-r1 { fill: #e2e3e3 } + .terminal-1080619275-r2 { fill: #151515 } + .terminal-1080619275-r3 { fill: #dde6ed } + .terminal-1080619275-r4 { fill: #e4e5e6 } + .terminal-1080619275-r5 { fill: #c5c8c6 } + .terminal-1080619275-r6 { fill: #004578 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  I am a line. - 2   - 3  I am another line. - 4   - 5  I am the final line.  + + + + I am a line. + + I am another line. + + I am the final line.  @@ -30608,61 +30605,60 @@ font-weight: 700; } - .terminal-425817926-matrix { + .terminal-3301472261-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-425817926-title { + .terminal-3301472261-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-425817926-r1 { fill: #86898c } - .terminal-425817926-r2 { fill: #dde6ed } - .terminal-425817926-r3 { fill: #e2e3e3 } - .terminal-425817926-r4 { fill: #c5c8c6 } - .terminal-425817926-r5 { fill: #004578 } - .terminal-425817926-r6 { fill: #e4e5e6 } - .terminal-425817926-r7 { fill: #151515 } + .terminal-3301472261-r1 { fill: #e2e3e3 } + .terminal-3301472261-r2 { fill: #dde6ed } + .terminal-3301472261-r3 { fill: #c5c8c6 } + .terminal-3301472261-r4 { fill: #004578 } + .terminal-3301472261-r5 { fill: #151515 } + .terminal-3301472261-r6 { fill: #e4e5e6 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  I am a line. - 2   - 3  I am another line. - 4   - 5  I am the final line. + + + + I am a line. + + I am another line. + + I am the final line. @@ -30692,59 +30688,58 @@ font-weight: 700; } - .terminal-1848814388-matrix { + .terminal-2211880532-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1848814388-title { + .terminal-2211880532-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1848814388-r1 { fill: #86898c } - .terminal-1848814388-r2 { fill: #e2e3e3 } - .terminal-1848814388-r3 { fill: #c5c8c6 } - .terminal-1848814388-r4 { fill: #e4e5e6 } - .terminal-1848814388-r5 { fill: #151515 } + .terminal-2211880532-r1 { fill: #e2e3e3 } + .terminal-2211880532-r2 { fill: #c5c8c6 } + .terminal-2211880532-r3 { fill: #151515 } + .terminal-2211880532-r4 { fill: #e4e5e6 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  I am a line.          - 2   - 3  I am another line.    - 4   - 5  I am the final line.  + + + + I am a line.          + + I am another line.    + + I am the final line.  @@ -30774,59 +30769,58 @@ font-weight: 700; } - .terminal-2863377204-matrix { + .terminal-2103108078-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2863377204-title { + .terminal-2103108078-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2863377204-r1 { fill: #86898c } - .terminal-2863377204-r2 { fill: #e2e3e3 } - .terminal-2863377204-r3 { fill: #c5c8c6 } - .terminal-2863377204-r4 { fill: #e4e5e6 } - .terminal-2863377204-r5 { fill: #151515 } + .terminal-2103108078-r1 { fill: #e2e3e3 } + .terminal-2103108078-r2 { fill: #c5c8c6 } + .terminal-2103108078-r3 { fill: #e4e5e6 } + .terminal-2103108078-r4 { fill: #151515 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  I am a line.          - 2   - 3  I am another line.          - 4   - 5  I am the final line.  + + + + I am a line.          + + I am another line.             + + I am the final line.  diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index ff3d3e18ff..1fb51ccb41 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -680,6 +680,7 @@ def test_text_area_selection_rendering(snap_compare, selection): def setup_selection(pilot): text_area = pilot.app.query_one(TextArea) text_area.load_text(text) + text_area.show_line_numbers = False text_area.selection = selection assert snap_compare( From 7c5ab849365b7bcd493cb99259c79760693cc238 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Aug 2023 15:12:28 +0100 Subject: [PATCH 266/366] Adding snapshot test for unfocus styling --- .../{text_area_languages.py => text_area.py} | 0 .../snapshot_apps/text_area_unfocus.py | 17 ++++++++++++++ tests/snapshot_tests/test_snapshots.py | 23 +++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) rename tests/snapshot_tests/snapshot_apps/{text_area_languages.py => text_area.py} (100%) create mode 100644 tests/snapshot_tests/snapshot_apps/text_area_unfocus.py diff --git a/tests/snapshot_tests/snapshot_apps/text_area_languages.py b/tests/snapshot_tests/snapshot_apps/text_area.py similarity index 100% rename from tests/snapshot_tests/snapshot_apps/text_area_languages.py rename to tests/snapshot_tests/snapshot_apps/text_area.py diff --git a/tests/snapshot_tests/snapshot_apps/text_area_unfocus.py b/tests/snapshot_tests/snapshot_apps/text_area_unfocus.py new file mode 100644 index 0000000000..e092f16721 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/text_area_unfocus.py @@ -0,0 +1,17 @@ +"""Tests the rendering of the TextArea for all supported languages.""" +from textual.app import App, ComposeResult +from textual.widgets import TextArea + + +class TextAreaUnfocusSnapshot(App): + AUTO_FOCUS = None + + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.cursor_blink = False + yield text_area + + +app = TextAreaUnfocusSnapshot() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 1fb51ccb41..c1aa06e7a2 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -653,7 +653,7 @@ def setup_language(pilot) -> None: text_area.language = language assert snap_compare( - SNAPSHOT_APPS_DIR / "text_area_languages.py", + SNAPSHOT_APPS_DIR / "text_area.py", run_before=setup_language, terminal_size=(80, snippet.count("\n") + 2), ) @@ -684,7 +684,26 @@ def setup_selection(pilot): text_area.selection = selection assert snap_compare( - SNAPSHOT_APPS_DIR / "text_area_languages.py", + SNAPSHOT_APPS_DIR / "text_area.py", + run_before=setup_selection, + terminal_size=(30, text.count("\n") + 1), + ) + + +def test_text_area_unfocus_rendering(snap_compare): + text = """I am a line. + + I am another line. + + I am the final line.""" + + async def setup_selection(pilot): + text_area = pilot.app.query_one(TextArea) + text_area.load_text(text) + text_area.selection = Selection((0, 0), (2, 8)) + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area_unfocus.py", run_before=setup_selection, terminal_size=(30, text.count("\n") + 1), ) From 60a8ed50da8e9e84280cebeff61a11c95f1e251d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 09:53:51 +0100 Subject: [PATCH 267/366] Create initial snapshot for text-area unfocused --- .../__snapshots__/test_snapshots.ambr | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 63c4b81c46..af9da82727 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -30827,6 +30827,91 @@ ''' # --- +# name: test_text_area_unfocus_rendering + ''' + + + + + + + + + + + + + + + + + + + + + + + TextAreaUnfocusSnapshot + + + + + + + + + + 1  I am a line. + 2   + 3      I amanother line.      + 4   + 5      I am the final line.  + + + + + ''' +# --- # name: test_text_log_blank_write ''' From ae2683d2f1072e951eed54467d6f7211d984b1b4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 10:10:39 +0100 Subject: [PATCH 268/366] Support shift+home, shift+end --- src/textual/widgets/_text_area.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 8877c86204..44d0be65ff 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -117,6 +117,15 @@ class TextArea(ScrollView, can_focus=True): ), Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False), Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False), + Binding( + "shift+home", + "cursor_line_start(True)", + "cursor line start select", + show=False, + ), + Binding( + "shift+end", "cursor_line_end(True)", "cursor line end select", show=False + ), Binding("pageup", "cursor_page_up", "cursor page up", show=False), Binding("pagedown", "cursor_page_down", "cursor page down", show=False), # Selection with the cursor @@ -922,10 +931,10 @@ def get_cursor_up_location(self) -> Location: target_column = clamp(target_column, 0, len(self.document[target_row])) return target_row, target_column - def action_cursor_line_end(self) -> None: + def action_cursor_line_end(self, select: bool = False) -> None: """Move the cursor to the end of the line.""" location = self.get_cursor_line_end_location() - self.move_cursor(location) + self.move_cursor(location, select=select) def get_cursor_line_end_location(self) -> Location: """Get the location of the end of the current line. @@ -938,10 +947,10 @@ def get_cursor_line_end_location(self) -> Location: target_column = len(self.document[cursor_row]) return cursor_row, target_column - def action_cursor_line_start(self) -> None: + def action_cursor_line_start(self, select: bool = False) -> None: """Move the cursor to the start of the line.""" target = self.get_cursor_line_start_location() - self.move_cursor(target) + self.move_cursor(target, select=select) def get_cursor_line_start_location(self) -> Location: """Get the location of the start of the current line. From ccf4ebad2796592d06e734fd291b0783fc079fb8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 10:11:37 +0100 Subject: [PATCH 269/366] Document shift+home, shift+end --- src/textual/widgets/_text_area.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 44d0be65ff..4f113ee97a 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -167,6 +167,8 @@ class TextArea(ScrollView, can_focus=True): | ctrl+shift+right | Move the cursor to the end of the word and select. | | home,ctrl+a | Move the cursor to the start of the line. | | end,ctrl+e | Move the cursor to the end of the line. | + | shift+home | Move the cursor to the start of the line and select. | + | shift+end | Move the cursor to the end of the line and select. | | pageup | Move the cursor one page up. | | pagedown | Move the cursor one page down. | | shift+up | Select while moving the cursor up. | From cf33d772d9c89fbb78742168efc59d0ef1151aa7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 12:28:59 +0100 Subject: [PATCH 270/366] Add Dracula syntax highlighting theme --- src/textual/document/_syntax_theme.py | 45 +++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index 13cadf573b..b3b7d7af8b 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -38,7 +38,7 @@ "regex.operator": Style(color="#F92672"), # "error": Style(color="black", bgcolor="red"), "json.error": _NULL_STYLE, - "html.end_tag_error": Style(color="black", bgcolor="red"), + "html.end_tag_error": Style(color="red", underline=True), "tag": Style(color="#F92672"), "yaml.field": Style(color="#F92672", bold=True), "json.label": Style(color="#F92672", bold=True), @@ -47,9 +47,50 @@ "toml.error": _NULL_STYLE, } +_DRACULA = { + "string": Style(color="#f1fa8c"), + "string.documentation": Style(color="#f1fa8c"), + "comment": Style(color="#6272a4"), + "keyword": Style(color="#ff79c6"), + "operator": Style(color="#ff79c6"), + "repeat": Style(color="#ff79c6"), + "exception": Style(color="#ff79c6"), + "include": Style(color="#ff79c6"), + "keyword.function": Style(color="#ff79c6"), + "keyword.return": Style(color="#ff79c6"), + "keyword.operator": Style(color="#ff79c6"), + "conditional": Style(color="#ff79c6"), + "number": Style(color="#bd93f9"), + "float": Style(color="#bd93f9"), + "class": Style(color="#50fa7b"), + "function": Style(color="#50fa7b"), + "function.call": Style(color="#50fa7b"), + "method": Style(color="#50fa7b"), + "method.call": Style(color="#50fa7b"), + "boolean": Style(color="#bd93f9"), + "json.null": Style(color="#bd93f9"), + # "constant": Style(color="#bd93f9"), + # "variable": Style(color="white"), + # "parameter": Style(color="cyan"), + # "type": Style(color="cyan"), + # "escape": Style(bgcolor="magenta"), + "heading": Style(color="#ff79c6", bold=True), + "regex.punctuation.bracket": Style(color="#ff79c6"), + "regex.operator": Style(color="#ff79c6"), + # "error": Style(color="black", bgcolor="red"), + "json.error": _NULL_STYLE, + "html.end_tag_error": Style(color="#F83333", underline=True), + "tag": Style(color="#ff79c6"), + "yaml.field": Style(color="#ff79c6", bold=True), + "json.label": Style(color="#ff79c6", bold=True), + "toml.type": Style(color="#ff79c6"), + "toml.datetime": Style(color="#bd93f9"), + "toml.error": _NULL_STYLE, +} + _BUILTIN_THEMES = { "monokai": _MONOKAI, - "bluokai": {**_MONOKAI, "string": Style.parse("cyan")}, + "dracula": _DRACULA, } From d1222c408cdf58dfdaa0b1f30270af04d8693dcf Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 15:19:42 +0100 Subject: [PATCH 271/366] Small change to delete_line behaviour when multiple lines selected to match vscode/pycharm behaviour --- src/textual/widgets/_text_area.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 4f113ee97a..46351727a8 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1177,10 +1177,14 @@ def action_delete_right(self) -> None: def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" start, end = self.selection - start, end = _sort_ascending(start, end) start_row, start_column = start end_row, end_column = end + # Generally editors will only delete line the end line of the + # selection if the cursor is not at column 0 of that line. + if end_column == 0: + end_row -= 1 + from_location = (start_row, 0) to_location = (end_row + 1, 0) From 0784690663a84dbe6eb41aa416d338acac626ffd Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 15:23:48 +0100 Subject: [PATCH 272/366] Add test for new delete line logic --- src/textual/widgets/_text_area.py | 2 +- tests/text_area/test_edit_via_bindings.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 46351727a8..c4bd5f32e6 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1182,7 +1182,7 @@ def action_delete_line(self) -> None: # Generally editors will only delete line the end line of the # selection if the cursor is not at column 0 of that line. - if end_column == 0: + if start_row != end_row and end_column == 0 and end_row > 0: end_row -= 1 from_location = (start_row, 0) diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index db1b14eef6..b47eb24fd3 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -170,6 +170,10 @@ async def test_delete_line(selection, expected_result): # Selections (Selection((1, 1), (1, 2)), "012\n678\n9\n"), # non-empty single line selection (Selection((1, 2), (2, 1)), "012\n9\n"), # delete lines selection touches + ( + Selection((1, 2), (3, 0)), + "012\n9\n", + ), # cursor at column 0 of line 3, should not be deleted! (Selection((0, 0), (4, 0)), ""), # delete all lines ], ) From 2ecba74d52f59fcf95e1b3e108db9adb9e743f89 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 15:30:46 +0100 Subject: [PATCH 273/366] Delete line improvement --- src/textual/widgets/_text_area.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index c4bd5f32e6..d7471c5f33 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1177,12 +1177,13 @@ def action_delete_right(self) -> None: def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" start, end = self.selection + start, end = _sort_ascending(start, end) start_row, start_column = start end_row, end_column = end # Generally editors will only delete line the end line of the # selection if the cursor is not at column 0 of that line. - if start_row != end_row and end_column == 0 and end_row > 0: + if start_row != end_row and end_column == 0 and end_row >= 0: end_row -= 1 from_location = (start_row, 0) From f47c6de0bd1ed1ff23b93306d28adb691469b3f6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 15:31:46 +0100 Subject: [PATCH 274/366] Add extra test for delete_line multiple selection --- tests/text_area/test_edit_via_bindings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index b47eb24fd3..a412405cb8 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -174,6 +174,10 @@ async def test_delete_line(selection, expected_result): Selection((1, 2), (3, 0)), "012\n9\n", ), # cursor at column 0 of line 3, should not be deleted! + ( + Selection((3, 0), (1, 2)), + "012\n9\n", + ), # opposite direction (Selection((0, 0), (4, 0)), ""), # delete all lines ], ) From efd1abc33869c596b96be3b01facb1ede56cffad Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 15:56:27 +0100 Subject: [PATCH 275/366] Test cursor "smart" home behaviour --- src/textual/widgets/_text_area.py | 20 ++++++++++++++--- tests/text_area/test_selection_bindings.py | 26 +++++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index d7471c5f33..82a91fa4b5 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -951,8 +951,22 @@ def get_cursor_line_end_location(self) -> Location: def action_cursor_line_start(self, select: bool = False) -> None: """Move the cursor to the start of the line.""" - target = self.get_cursor_line_start_location() - self.move_cursor(target, select=select) + + cursor_row, cursor_column = self.cursor_location + line = self.document[cursor_row] + + first_non_whitespace = 0 + for index, code_point in enumerate(line): + if not code_point.isspace(): + first_non_whitespace = index + break + + if cursor_column <= first_non_whitespace and cursor_column != 0: + target = self.get_cursor_line_start_location() + self.move_cursor(target, select=select) + else: + target = cursor_row, first_non_whitespace + self.move_cursor(target, select=select) def get_cursor_line_start_location(self) -> Location: """Get the location of the start of the current line. @@ -989,7 +1003,7 @@ def get_cursor_word_left_location(self) -> Location: # Staying on the same row line = self.document[cursor_row][:cursor_column] search_string = line.rstrip() - + "soet" matches = list(re.finditer(self._word_pattern, search_string)) cursor_column = matches[-1].start() if matches else 0 return cursor_row, cursor_column diff --git a/tests/text_area/test_selection_bindings.py b/tests/text_area/test_selection_bindings.py index 2363befe31..d8def8857a 100644 --- a/tests/text_area/test_selection_bindings.py +++ b/tests/text_area/test_selection_bindings.py @@ -217,7 +217,7 @@ async def test_cursor_to_line_end(key): @pytest.mark.parametrize("key", ["home", "ctrl+a"]) -async def test_cursor_to_line_home(key): +async def test_cursor_to_line_home_basic_behaviour(key): """You can use the keyboard to jump the cursor to the start of the current line.""" app = TextAreaApp() async with app.run_test() as pilot: @@ -228,6 +228,30 @@ async def test_cursor_to_line_home(key): assert text_area.selection.is_empty +@pytest.mark.parametrize( + "cursor_start,cursor_destination", + [ + ((0, 0), (0, 4)), + ((0, 2), (0, 0)), + ((0, 4), (0, 0)), + ((0, 5), (0, 4)), + ((0, 9), (0, 4)), + ((0, 15), (0, 4)), + ], +) +async def test_cursor_line_home_smart_home(cursor_start, cursor_destination): + """If the line begins with whitespace, pressing home firstly goes + to the start of the (non-whitespace) content. Pressing it again takes you to column + 0. If you press it again, it goes back to the first non-whitespace column.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text(" hello world") + text_area.move_cursor(cursor_start) + await pilot.press("home") + assert text_area.selection == Selection.cursor(cursor_destination) + + async def test_cursor_page_down(): """Pagedown moves the cursor down 1 page, retaining column index.""" app = TextAreaApp() From eb9120a03e21f141d5fbff6dc084e9c5fd297b1d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 16:57:54 +0100 Subject: [PATCH 276/366] Fix typo --- src/textual/widgets/_text_area.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 82a91fa4b5..052137ed4f 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1003,7 +1003,6 @@ def get_cursor_word_left_location(self) -> Location: # Staying on the same row line = self.document[cursor_row][:cursor_column] search_string = line.rstrip() - "soet" matches = list(re.finditer(self._word_pattern, search_string)) cursor_column = matches[-1].start() if matches else 0 return cursor_row, cursor_column From 610ad9b9ac3ddf986870a4a4b891d165d9b8e10f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 19:18:39 +0100 Subject: [PATCH 277/366] Highlight matching brackets --- src/textual/widgets/_text_area.py | 152 +++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 052137ed4f..8065362ce3 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -2,7 +2,7 @@ import re from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar, Iterable from rich.style import Style from rich.text import Text @@ -29,6 +29,9 @@ from textual.scroll_view import ScrollView from textual.strip import Strip +_OPENING_BRACKETS = {"{": "}", "[": "]", "(": ")"} +_CLOSING_BRACKETS = {v: k for k, v in _OPENING_BRACKETS.items()} + class TextArea(ScrollView, can_focus=True): DEFAULT_CSS = """\ @@ -76,6 +79,15 @@ class TextArea(ScrollView, can_focus=True): TextArea > .text-area--selection { background: $primary 65%; } +TextArea:focus > .text-area--matching-bracket { + color: $text; + background: white 20%; + text-style: bold underline; +} +TextArea > .text-area--matching-bracket { + background: ; + text-style: ; +} """ COMPONENT_CLASSES: ClassVar[set[str]] = { @@ -84,6 +96,7 @@ class TextArea(ScrollView, can_focus=True): "text-area--cursor-line", "text-area--cursor-line-gutter", "text-area--selection", + "text-area--matching-bracket", } """| Class | Description | |:--------------------------------|:-------------------------------------------------| @@ -92,6 +105,7 @@ class TextArea(ScrollView, can_focus=True): | `text-area--cursor-line` | Targets the line of text the cursor is on. | | `text-area--cursor-line-gutter` | Targets the gutter of the line the cursor is on. | | `text-area--selection` | Targets the selected text. | +| `text-area--matching-bracket` | Targets the bracket matching the cursor bracket. | """ BINDINGS = [ @@ -232,11 +246,15 @@ class TextArea(ScrollView, can_focus=True): altering this value will immediately change the display width of the visible tabs. """ + match_cursor_bracket: Reactive[bool] = reactive(True) + """If the cursor is at a bracket, highlight the matching bracket if found.""" + cursor_blink: Reactive[bool] = reactive(True) """True if the cursor should blink.""" _cursor_blink_visible: Reactive[bool] = reactive(True, repaint=False) - """True if the cursor should be rendered """ + """Indicates where the cursor is in the blink cycle. If it's currently + not visible due to blinking, this is False.""" def __init__( self, @@ -276,9 +294,62 @@ def __init__( self._selecting = False """True if we're currently selecting text using the mouse, otherwise False.""" - def _watch_selection(self) -> None: + self._matching_bracket_location: Location | None = None + """The location (row, column) of the bracket which matches the bracket the + cursor is currently at. If the cursor is at a bracket, or there's no matching + bracket, this will be `None`.""" + + def _watch_selection(self, selection: Selection) -> None: """When the cursor moves, scroll it into view.""" self.scroll_cursor_visible() + cursor_location = selection.end + cursor_row, cursor_column = cursor_location + + try: + character = self.document[cursor_row][cursor_column] + except IndexError: + character = None + + # Record the location of a matching closing/opening bracket. + match_location = None + bracket_stack = [] + if character in _OPENING_BRACKETS: + for candidate, candidate_location in self._yield_character_locations( + cursor_location + ): + if candidate in _OPENING_BRACKETS: + bracket_stack.append(candidate) + elif candidate in _CLOSING_BRACKETS: + if ( + bracket_stack + and bracket_stack[-1] == _CLOSING_BRACKETS[candidate] + ): + bracket_stack.pop() + if not bracket_stack: + match_location = candidate_location + break + elif character in _CLOSING_BRACKETS: + for ( + candidate, + candidate_location, + ) in self._yield_character_locations_reverse(cursor_location): + if candidate in _CLOSING_BRACKETS: + bracket_stack.append(candidate) + elif candidate in _OPENING_BRACKETS: + if ( + bracket_stack + and bracket_stack[-1] == _OPENING_BRACKETS[candidate] + ): + bracket_stack.pop() + if not bracket_stack: + match_location = candidate_location + break + + self._matching_bracket_location = match_location + if match_location is not None: + match_row, match_column = match_location + if match_row in range(*self._visible_line_indices): + self.refresh_lines(match_row) def _validate_selection(self, selection: Selection) -> Selection: """Clamp the selection to valid locations.""" @@ -320,6 +391,15 @@ def _reload_document(self) -> None: log.warning("Syntax highlighting isn't available on Python 3.7.") self.document = Document(text) + @property + def _visible_line_indices(self) -> tuple[int, int]: + """Return the visible line indices as a tuple (top, bottom). + + Returns: + A tuple (top, bottom) indicating the top and bottom visible line indices. + """ + return self.scroll_offset.y, self.scroll_offset.y + self.size.height + def load_text(self, text: str) -> None: """Load text from a string into the TextArea. @@ -341,6 +421,47 @@ def load_document(self, document: DocumentBase) -> None: self.move_cursor((0, 0)) self._refresh_size() + def _yield_character_locations( + self, start: Location + ) -> Iterable[tuple[str, Location]]: + """Yields character locations starting from the given location. + + Does not yield location of line separator characters like `\\n`. + + Args: + start: The location to start yielding from. + + Returns: + Yields tuples of (character, (row, column)). + """ + row, column = start + document = self.document + line_count = document.line_count + + while 0 <= row < line_count: + line = document[row] + while column < len(line): + yield line[column], (row, column) + column += 1 + column = 0 + row += 1 + + def _yield_character_locations_reverse( + self, start: Location + ) -> Iterable[tuple[str, Location]]: + row, column = start + document = self.document + line_count = document.line_count + + while line_count > row >= 0: + line = document[row] + if column == -1: + column = len(line) - 1 + while column >= 0: + yield line[column], (row, column) + column -= 1 + row -= 1 + def _refresh_size(self) -> None: """Update the virtual size of the TextArea.""" width, height = self.document.get_size(self.indent_width) @@ -412,6 +533,22 @@ def render_line(self, widget_y: int) -> Strip: virtual_width, virtual_height = self.virtual_size + # Highlight the partner opening/closing bracket. + matching_bracket = self._matching_bracket_location + match_cursor_bracket = self.match_cursor_bracket + draw_matched_brackets = match_cursor_bracket and matching_bracket + if draw_matched_brackets: + bracket_match_row, bracket_match_column = self._matching_bracket_location + if bracket_match_row == line_index: + matching_bracket_style = self.get_component_rich_style( + "text-area--matching-bracket" + ) + line.stylize( + matching_bracket_style, + bracket_match_column, + bracket_match_column + 1, + ) + # Highlight the cursor cursor_row, cursor_column = end active_line_style = self.get_component_rich_style("text-area--cursor-line") @@ -419,6 +556,15 @@ def render_line(self, widget_y: int) -> Strip: draw_cursor = not self.cursor_blink or ( self.cursor_blink and self._cursor_blink_visible ) + if draw_matched_brackets: + matching_bracket_style = self.get_component_rich_style( + "text-area--matching-bracket" + ) + line.stylize( + matching_bracket_style, + cursor_column, + cursor_column + 1, + ) if draw_cursor: cursor_style = self.get_component_rich_style("text-area--cursor") line.stylize(cursor_style, cursor_column, cursor_column + 1) From 961d371db49e7b4ff15d4f2c62dca869e188f1ef Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Aug 2023 19:25:46 +0100 Subject: [PATCH 278/366] Update snapshot --- src/textual/widgets/_text_area.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 8065362ce3..a5471b6685 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -536,9 +536,9 @@ def render_line(self, widget_y: int) -> Strip: # Highlight the partner opening/closing bracket. matching_bracket = self._matching_bracket_location match_cursor_bracket = self.match_cursor_bracket - draw_matched_brackets = match_cursor_bracket and matching_bracket + draw_matched_brackets = match_cursor_bracket and matching_bracket is not None if draw_matched_brackets: - bracket_match_row, bracket_match_column = self._matching_bracket_location + bracket_match_row, bracket_match_column = matching_bracket if bracket_match_row == line_index: matching_bracket_style = self.get_component_rich_style( "text-area--matching-bracket" From d6b43d91f882a83816ae73a18a19d8651620270c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Aug 2023 10:14:49 +0100 Subject: [PATCH 279/366] Update snapshot --- .../__snapshots__/test_snapshots.ambr | 157 +++++++++--------- 1 file changed, 79 insertions(+), 78 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index af9da82727..9190b206f4 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -28622,170 +28622,171 @@ font-weight: 700; } - .terminal-703968804-matrix { + .terminal-3995077928-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-703968804-title { + .terminal-3995077928-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-703968804-r1 { fill: #e4e5e6 } - .terminal-703968804-r2 { fill: #151515 } - .terminal-703968804-r3 { fill: #c5c8c6 } - .terminal-703968804-r4 { fill: #86898c } - .terminal-703968804-r5 { fill: #e2e3e3 } - .terminal-703968804-r6 { fill: #f92672;font-weight: bold } - .terminal-703968804-r7 { fill: #e6db74 } - .terminal-703968804-r8 { fill: #ae81ff } - .terminal-703968804-r9 { fill: #66d9ef;font-style: italic; } + .terminal-3995077928-r1 { fill: #e4e5e6 } + .terminal-3995077928-r2 { fill: #151515;font-weight: bold;text-decoration: underline; } + .terminal-3995077928-r3 { fill: #c5c8c6 } + .terminal-3995077928-r4 { fill: #86898c } + .terminal-3995077928-r5 { fill: #e2e3e3 } + .terminal-3995077928-r6 { fill: #f92672;font-weight: bold } + .terminal-3995077928-r7 { fill: #e6db74 } + .terminal-3995077928-r8 { fill: #ae81ff } + .terminal-3995077928-r9 { fill: #66d9ef;font-style: italic; } + .terminal-3995077928-r10 { fill: #e8e8e9;font-weight: bold;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  { -  2  "name""John Doe",                            -  3  "age"30,                                     -  4  "isStudent"false,                            -  5  "address": {                                   -  6  "street""123 Main St",                   -  7  "city""Anytown",                         -  8  "state""CA",                             -  9  "zip""12345" - 10      },                                             - 11  "phoneNumbers": [                              - 12          {                                          - 13  "type""home",                        - 14  "number""555-555-1234" - 15          },                                         - 16          {                                          - 17  "type""work",                        - 18  "number""555-555-5678" - 19          }                                          - 20      ],                                             - 21  "hobbies": ["reading""hiking""swimming"],  - 22  "pets": [                                      - 23          {                                          - 24  "type""dog",                         - 25  "name""Fido" - 26          },                                         - 27      ],                                             - 28  "graduationYear"null - 29  }                                                  - 30   - 31   + + + +  1  { +  2  "name""John Doe",                            +  3  "age"30,                                     +  4  "isStudent"false,                            +  5  "address": {                                   +  6  "street""123 Main St",                   +  7  "city""Anytown",                         +  8  "state""CA",                             +  9  "zip""12345" + 10      },                                             + 11  "phoneNumbers": [                              + 12          {                                          + 13  "type""home",                        + 14  "number""555-555-1234" + 15          },                                         + 16          {                                          + 17  "type""work",                        + 18  "number""555-555-5678" + 19          }                                          + 20      ],                                             + 21  "hobbies": ["reading""hiking""swimming"],  + 22  "pets": [                                      + 23          {                                          + 24  "type""dog",                         + 25  "name""Fido" + 26          },                                         + 27      ],                                             + 28  "graduationYear"null + 29  } + 30   + 31   From ed8e875be0146c920c6bee333c73075be0855d2c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Aug 2023 11:34:14 +0100 Subject: [PATCH 280/366] Fix xfails --- tests/text_area/test_edit_via_bindings.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index a412405cb8..d597053042 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -257,11 +257,10 @@ async def test_delete_to_start_of_line(selection, expected_result): (Selection.cursor((0, 0)), " 012 345 6789", Selection.cursor((0, 0))), (Selection.cursor((0, 4)), " 2 345 6789", Selection.cursor((0, 2))), (Selection.cursor((0, 5)), " 345 6789", Selection.cursor((0, 2))), - pytest.param( + ( Selection.cursor((0, 6)), - " 345 6789", + " 345 6789", Selection.cursor((0, 2)), - marks=pytest.mark.xfail(reason="Should skip initial whitespace."), ), (Selection.cursor((0, 14)), " 012 345 ", Selection.cursor((0, 10))), # When there's a selection and you "delete word left", it just deletes the selection @@ -287,11 +286,10 @@ async def test_delete_word_left(selection, expected_result, final_selection): (Selection.cursor((0, 0)), "\t012 \t 345\t6789", Selection.cursor((0, 0))), (Selection.cursor((0, 4)), "\t \t 345\t6789", Selection.cursor((0, 1))), (Selection.cursor((0, 5)), "\t012\t 345\t6789", Selection.cursor((0, 4))), - pytest.param( + ( Selection.cursor((0, 6)), - "\t 345\t6789", - Selection.cursor((0, 1)), - marks=pytest.mark.xfail(reason="Should skip initial whitespace."), + "\t012 345\t6789", + Selection.cursor((0, 4)), ), (Selection.cursor((0, 15)), "\t012 \t 345\t", Selection.cursor((0, 11))), # When there's a selection and you "delete word left", it just deletes the selection From 5cd9a55adf8ccedc7b45e4d31d2aa22ed0305611 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Aug 2023 12:01:04 +0100 Subject: [PATCH 281/366] Simplify delete_word_left --- src/textual/widgets/_text_area.py | 22 ++++++++-------------- tests/text_area/test_edit_via_bindings.py | 6 +++--- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index a5471b6685..86acd86f9a 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1376,22 +1376,16 @@ def action_delete_word_left(self) -> None: self.delete(start, end, maintain_selection_offset=False) return - cursor_row, cursor_column = end - - line = self.document[cursor_row][:cursor_column] - matches = list(re.finditer(self._word_pattern, line)) - - if matches: - from_location = (cursor_row, matches[-1].start()) - elif cursor_row > 0 and cursor_column == 0: - from_location = (cursor_row - 1, len(self.document[cursor_row - 1])) - else: - from_location = (cursor_row, 0) - - self.delete(from_location, self.selection.end, maintain_selection_offset=False) + to_location = self.get_cursor_word_left_location() + self.delete(self.selection.end, to_location, maintain_selection_offset=False) def action_delete_word_right(self) -> None: - """Deletes the word to the right of the cursor and keeps the cursor at the same location.""" + """Deletes the word to the right of the cursor and keeps the cursor at the same location. + + Note that the location that we delete to using this action is not the same + as the location we move to when we move the cursor one word to the right. + This action does not skip leading whitespace, whereas cursor movement does. + """ if self.cursor_at_end_of_document: return diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index d597053042..4c6d1e1872 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -285,11 +285,11 @@ async def test_delete_word_left(selection, expected_result, final_selection): [ (Selection.cursor((0, 0)), "\t012 \t 345\t6789", Selection.cursor((0, 0))), (Selection.cursor((0, 4)), "\t \t 345\t6789", Selection.cursor((0, 1))), - (Selection.cursor((0, 5)), "\t012\t 345\t6789", Selection.cursor((0, 4))), + (Selection.cursor((0, 5)), "\t\t 345\t6789", Selection.cursor((0, 1))), ( Selection.cursor((0, 6)), - "\t012 345\t6789", - Selection.cursor((0, 4)), + "\t 345\t6789", + Selection.cursor((0, 1)), ), (Selection.cursor((0, 15)), "\t012 \t 345\t", Selection.cursor((0, 11))), # When there's a selection and you "delete word left", it just deletes the selection From e29229e7c32437b6db707bf7595e36068128c29e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Aug 2023 13:22:37 +0100 Subject: [PATCH 282/366] Catch correct exception to ensure support for Python 3.7 --- src/textual/widgets/_text_area.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 86acd86f9a..30b2d8c24f 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -385,7 +385,7 @@ def _reload_document(self) -> None: from textual.document._syntax_aware_document import SyntaxAwareDocument self.document = SyntaxAwareDocument(text, language, self.theme) - except ImportError: + except RuntimeError: # SyntaxAwareDocument isn't available on Python 3.7. # Fall back to the standard document. log.warning("Syntax highlighting isn't available on Python 3.7.") From f7d5ef9f31bc128dd808d75c64823e8643431730 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Aug 2023 13:51:05 +0100 Subject: [PATCH 283/366] Add styling for Markdown --- src/textual/document/_syntax_theme.py | 7 +- .../__snapshots__/test_snapshots.ambr | 308 +++++++++--------- tree-sitter/highlights/markdown.scm | 6 +- 3 files changed, 167 insertions(+), 154 deletions(-) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index b3b7d7af8b..879175f22d 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -33,7 +33,6 @@ # "parameter": Style(color="cyan"), # "type": Style(color="cyan"), # "escape": Style(bgcolor="magenta"), - "heading": Style(color="#F92672", bold=True), "regex.punctuation.bracket": Style(color="#F92672"), "regex.operator": Style(color="#F92672"), # "error": Style(color="black", bgcolor="red"), @@ -45,6 +44,12 @@ "toml.type": Style(color="#F92672"), "toml.datetime": Style(color="#AE81FF"), "toml.error": _NULL_STYLE, + "heading": Style(color="#F92672", bold=True), + "bold": Style(bold=True), + "italic": Style(italic=True), + "strikethrough": Style(strike=True), + "link": Style(color="#66D9EF", underline=True), + "inline_code": Style(color="#F92672"), } _DRACULA = { diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 9190b206f4..7a9aa8c1b8 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -28817,318 +28817,322 @@ font-weight: 700; } - .terminal-3326201444-matrix { + .terminal-2031465495-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3326201444-title { + .terminal-2031465495-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3326201444-r1 { fill: #e4e5e6 } - .terminal-3326201444-r2 { fill: #151515;font-weight: bold } - .terminal-3326201444-r3 { fill: #f92672;font-weight: bold } - .terminal-3326201444-r4 { fill: #c5c8c6 } - .terminal-3326201444-r5 { fill: #86898c } - .terminal-3326201444-r6 { fill: #e2e3e3 } - .terminal-3326201444-r7 { fill: #75715e } - .terminal-3326201444-r8 { fill: #23568b } + .terminal-2031465495-r1 { fill: #e4e5e6 } + .terminal-2031465495-r2 { fill: #151515;font-weight: bold } + .terminal-2031465495-r3 { fill: #f92672;font-weight: bold } + .terminal-2031465495-r4 { fill: #c5c8c6 } + .terminal-2031465495-r5 { fill: #86898c } + .terminal-2031465495-r6 { fill: #e2e3e3 } + .terminal-2031465495-r7 { fill: #e2e3e3;font-style: italic; } + .terminal-2031465495-r8 { fill: #e2e3e3;font-weight: bold } + .terminal-2031465495-r9 { fill: #f92672 } + .terminal-2031465495-r10 { fill: #75715e } + .terminal-2031465495-r11 { fill: #66d9ef;text-decoration: underline; } + .terminal-2031465495-r12 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  Heading -  2  =======                                                                      -  3   -  4  Sub-heading -  5  -----------                                                                  -  6   -  7  ### Heading -  8   -  9  #### H4 Heading - 10   - 11  ##### H5 Heading - 12   - 13  ###### H6 Heading - 14   - 15   - 16  Paragraphs are separated                                                     - 17  by a blank line.                                                             - 18   - 19  Two spaces at the end of a line                                              - 20  produces a line break.                                                       - 21   - 22  Text attributes _italic_,                                                    - 23  **bold**, `monospace`.                                                       - 24   - 25  Horizontal rule:                                                             - 26   - 27  ---                                                                          - 28   - 29  Bullet list:                                                                 - 30   - 31  * apples                                                                   - 32  * oranges                                                                  - 33  * pears                                                                    - 34   - 35  Numbered list:                                                               - 36   - 37  1. lather                                                                  - 38  2. rinse                                                                   - 39  3. repeat                                                                  - 40   - 41  An [example](http://example.com).                                            - 42   - 43  > Markdown uses email-style > characters for blockquoting.                   - 44  >                                                                            - 45  > Lorem ipsum                                                                - 46   - 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) - 48   - 49   - 50  ```                                                                          - 51  a=1                                                                          - 52  ```                                                                          - 53   - 54  ```python                                                                    - 55  import this                                                                  - 56  ```                                                                          - 57   - 58  ```somelang                                                                  - 59  foobar                                                                       - 60  ```                                                                          - 61   - 62      import this                                                              - 63   - 64   - 65  1. List item                                                                 - 66   - 67         Code block                                                            - 68   - + + + +  1  Heading +  2  =======                                                                      +  3   +  4  Sub-heading +  5  -----------                                                                  +  6   +  7  ### Heading +  8   +  9  #### H4 Heading + 10   + 11  ##### H5 Heading + 12   + 13  ###### H6 Heading + 14   + 15   + 16  Paragraphs are separated                                                     + 17  by a blank line.                                                             + 18   + 19  Two spaces at the end of a line                                              + 20  produces a line break.                                                       + 21   + 22  Text attributes _italic_,                                                    + 23  **bold**`monospace`.                                                       + 24   + 25  Horizontal rule:                                                             + 26   + 27  ---                                                                          + 28   + 29  Bullet list:                                                                 + 30   + 31  * apples                                                                   + 32  * oranges                                                                  + 33  * pears                                                                    + 34   + 35  Numbered list:                                                               + 36   + 37  1. lather                                                                  + 38  2. rinse                                                                   + 39  3. repeat                                                                  + 40   + 41  An [example](http://example.com).                                            + 42   + 43  > Markdown uses email-style > characters for blockquoting.                   + 44  >                                                                            + 45  > Lorem ipsum                                                                + 46   + 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) + 48   + 49   + 50  ```                                                                          + 51  a=1                                                                          + 52  ```                                                                          + 53   + 54  ```python                                                                    + 55  import this                                                                  + 56  ```                                                                          + 57   + 58  ```somelang                                                                  + 59  foobar                                                                       + 60  ```                                                                          + 61   + 62      import this                                                              + 63   + 64   + 65  1. List item                                                                 + 66   + 67         Code block                                                            + 68   + diff --git a/tree-sitter/highlights/markdown.scm b/tree-sitter/highlights/markdown.scm index 497d1d227d..0912f52892 100644 --- a/tree-sitter/highlights/markdown.scm +++ b/tree-sitter/highlights/markdown.scm @@ -1,3 +1,7 @@ (heading_content) @heading - (list_marker) @comment +(strong_emphasis) @bold +(emphasis) @italic +(strikethrough) @strikethrough +(link) @link +(code_span) @inline_code From 6954c484b1fac4f9004b057eff9eebe1dd32c79e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Aug 2023 14:00:32 +0100 Subject: [PATCH 284/366] Add styles for Dracula for Markdown --- src/textual/document/_syntax_theme.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py index 879175f22d..792250a97d 100644 --- a/src/textual/document/_syntax_theme.py +++ b/src/textual/document/_syntax_theme.py @@ -79,7 +79,6 @@ # "parameter": Style(color="cyan"), # "type": Style(color="cyan"), # "escape": Style(bgcolor="magenta"), - "heading": Style(color="#ff79c6", bold=True), "regex.punctuation.bracket": Style(color="#ff79c6"), "regex.operator": Style(color="#ff79c6"), # "error": Style(color="black", bgcolor="red"), @@ -91,6 +90,12 @@ "toml.type": Style(color="#ff79c6"), "toml.datetime": Style(color="#bd93f9"), "toml.error": _NULL_STYLE, + "heading": Style(color="#ff79c6", bold=True), + "bold": Style(bold=True), + "italic": Style(italic=True), + "strikethrough": Style(strike=True), + "link": Style(color="#bd93f9", underline=True), + "inline_code": Style(color="#ff79c6"), } _BUILTIN_THEMES = { From 578fde0218ca29c4240ae85c38bcf60bb3e42885 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Aug 2023 14:25:18 +0100 Subject: [PATCH 285/366] Remove unused _fix_direction.py --- src/textual/_fix_direction.py | 19 ------------------- src/textual/document/_document.py | 7 +++---- .../document/_syntax_aware_document.py | 3 +-- src/textual/widgets/_text_area.py | 9 ++++----- 4 files changed, 8 insertions(+), 30 deletions(-) delete mode 100644 src/textual/_fix_direction.py diff --git a/src/textual/_fix_direction.py b/src/textual/_fix_direction.py deleted file mode 100644 index 26dd01fea8..0000000000 --- a/src/textual/_fix_direction.py +++ /dev/null @@ -1,19 +0,0 @@ -from __future__ import annotations - - -def _sort_ascending( - start: tuple[int, int], end: tuple[int, int] -) -> tuple[tuple[int, int], tuple[int, int]]: - """Given a range, return a new range (x, y) such - that x <= y which covers the same characters. - - Args: - start: The start of the range. - end: The end of the range. - - Returns: - A tuple (lesser_point, greater_point). - """ - if start > end: - return end, start - return start, end diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 0275c0d77f..c952820542 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -8,7 +8,6 @@ from rich.text import Text from textual._cells import cell_len -from textual._fix_direction import _sort_ascending from textual._types import Literal, get_args from textual.geometry import Size @@ -209,7 +208,7 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult The EditResult containing information about the completed replace operation. """ - top, bottom = _sort_ascending(start, end) + top, bottom = sorted((start, end)) top_row, top_column = top bottom_row, bottom_column = bottom @@ -263,7 +262,7 @@ def get_text_range(self, start: Location, end: Location) -> str: if start == end: return "" - top, bottom = _sort_ascending(start, end) + top, bottom = sorted((start, end)) top_row, top_column = top bottom_row, bottom_column = bottom lines = self._lines @@ -363,4 +362,4 @@ def range(self) -> tuple[Location, Location]: """Return the Selection as a "standard" range, from top to bottom i.e. (minimum point, maximum point) where the minimum point is inclusive and the maximum point is exclusive.""" start, end = self - return _sort_ascending(start, end) + return tuple(sorted((start, end))) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index ec8d129d07..092d10fa93 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -19,7 +19,6 @@ except ImportError: TREE_SITTER = False -from textual._fix_direction import _sort_ascending from textual.document._document import Document, EditResult, Location, _utf8_encode from textual.document._languages import VALID_LANGUAGES from textual.document._syntax_theme import SyntaxTheme @@ -117,7 +116,7 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult Returns: The new end location after the edit is complete. """ - top, bottom = _sort_ascending(start, end) + top, bottom = sorted((start, end)) # An optimisation would be finding the byte offsets as a single operation rather # than doing two passes over the document content. diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 30b2d8c24f..94328919e1 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -12,7 +12,6 @@ from textual import events, log from textual._cells import cell_len -from textual._fix_direction import _sort_ascending from textual._types import Literal, Protocol, runtime_checkable from textual.binding import Binding from textual.document import ( @@ -633,7 +632,7 @@ def get_text_range(self, start: Location, end: Location) -> str: Returns: The text between start and end. """ - start, end = _sort_ascending(start, end) + start, end = sorted((start, end)) return self.document.get_text_range(start, end) def edit(self, edit: Edit) -> Any: @@ -1274,7 +1273,7 @@ def delete( Returns: An `EditResult` containing information about the edit. """ - top, bottom = _sort_ascending(start, end) + top, bottom = sorted((start, end)) return self.edit(Edit("", top, bottom, maintain_selection_offset)) def replace( @@ -1336,7 +1335,7 @@ def action_delete_right(self) -> None: def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" start, end = self.selection - start, end = _sort_ascending(start, end) + start, end = sorted((start, end)) start_row, start_column = start end_row, end_column = end @@ -1449,7 +1448,7 @@ def do(self, text_area: TextArea) -> EditResult: # position in the document even if an insert happens before # their cursor position. - edit_top, edit_bottom = _sort_ascending(edit_from, edit_to) + edit_top, edit_bottom = sorted((edit_from, edit_to)) edit_bottom_row, edit_bottom_column = edit_bottom selection_start, selection_end = text_area.selection From f0ab9625bf35230eae8b31261f195a05b9f548a4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Aug 2023 14:25:49 +0100 Subject: [PATCH 286/366] Add docstring to EditResult --- src/textual/document/_document.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index c952820542..60eb889491 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -19,6 +19,8 @@ @dataclass class EditResult: + """Contains information about an edit that has occurred.""" + end_location: Location """The new end Location after the selection is complete.""" replaced_text: str From cee8757ca2dd1b401ba2d9b45aa2ade4dfa67fb1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Aug 2023 14:32:58 +0100 Subject: [PATCH 287/366] Use default=0 in max inside Document --- src/textual/document/_document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 60eb889491..c442652ac8 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -194,7 +194,7 @@ def get_size(self, tab_width: int) -> Size: """ lines = self._lines cell_lengths = [cell_len(line.expandtabs(tab_width)) for line in lines] - max_cell_length = max(cell_lengths or [0]) + max_cell_length = max(cell_lengths, default=0) height = len(lines) return Size(max_cell_length, height) From 0fc626be4c75b775d80f1d26249a6d9fdd3e0c61 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Aug 2023 14:51:01 +0100 Subject: [PATCH 288/366] Remove redundant actions --- src/textual/widgets/_text_area.py | 83 ++++++++++++++----------------- 1 file changed, 36 insertions(+), 47 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 94328919e1..a09804ac7b 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -113,23 +113,26 @@ class TextArea(ScrollView, can_focus=True): Binding("up", "cursor_up", "cursor up", show=False), Binding("down", "cursor_down", "cursor down", show=False), Binding("left", "cursor_left", "cursor left", show=False), + Binding("right", "cursor_right", "cursor right", show=False), Binding("ctrl+left", "cursor_word_left", "cursor word left", show=False), + Binding("ctrl+right", "cursor_word_right", "cursor word right", show=False), + Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False), + Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False), + Binding("pageup", "cursor_page_up", "cursor page up", show=False), + Binding("pagedown", "cursor_page_down", "cursor page down", show=False), + # Making selections (generally holding the shift key and moving cursor) Binding( "ctrl+shift+left", "cursor_word_left(True)", "cursor left word select", show=False, ), - Binding("right", "cursor_right", "cursor right", show=False), - Binding("ctrl+right", "cursor_word_right", "cursor word right", show=False), Binding( "ctrl+shift+right", "cursor_word_right(True)", "cursor right word select", show=False, ), - Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False), - Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False), Binding( "shift+home", "cursor_line_start(True)", @@ -139,15 +142,14 @@ class TextArea(ScrollView, can_focus=True): Binding( "shift+end", "cursor_line_end(True)", "cursor line end select", show=False ), - Binding("pageup", "cursor_page_up", "cursor page up", show=False), - Binding("pagedown", "cursor_page_down", "cursor page down", show=False), - # Selection with the cursor - Binding("shift+up", "cursor_up_select", "cursor up select", show=False), - Binding("shift+down", "cursor_down_select", "cursor down select", show=False), - Binding("shift+left", "cursor_left_select", "cursor left select", show=False), - Binding( - "shift+right", "cursor_right_select", "cursor right select", show=False - ), + Binding("shift+up", "cursor_up(True)", "cursor up select", show=False), + Binding("shift+down", "cursor_down(True)", "cursor down select", show=False), + Binding("shift+left", "cursor_left(True)", "cursor left select", show=False), + Binding("shift+right", "cursor_right(True)", "cursor right select", show=False), + # Shortcut ways of making selections + # Binding("f5", "select_word", "select word", show=False), + Binding("f6", "select_line", "select line", show=False), + Binding("f7", "select_all", "select all", show=False), # Deletion Binding("backspace", "delete_left", "delete left", show=False), Binding( @@ -162,9 +164,6 @@ class TextArea(ScrollView, can_focus=True): "ctrl+u", "delete_to_start_of_line", "delete to line start", show=False ), Binding("ctrl+k", "delete_to_end_of_line", "delete to line end", show=False), - # Binding("f5", "select_word", "select word", show=False), - Binding("f6", "select_line", "select line", show=False), - Binding("f7", "select_all", "select all", show=False), ] """ | Key(s) | Description | @@ -965,23 +964,17 @@ def cursor_at_end_of_document(self) -> bool: return self.cursor_at_last_row and self.cursor_at_end_of_row # ------ Cursor movement actions - def action_cursor_left(self) -> None: + def action_cursor_left(self, select: bool = False) -> None: """Move the cursor one location to the left. If the cursor is at the left edge of the document, try to move it to the end of the previous line. - """ - target = self.get_cursor_left_location() - self.selection = Selection.cursor(target) - self.record_cursor_width() - - def action_cursor_left_select(self): - """Move the end of the selection one location to the left. - This will expand or contract the selection. + Args: + select: If True, select the text while moving. """ new_cursor_location = self.get_cursor_left_location() - self.move_cursor(new_cursor_location, select=True) + self.move_cursor(new_cursor_location, select=select) def get_cursor_left_location(self) -> Location: """Get the location the cursor will move to if it moves left. @@ -997,18 +990,16 @@ def get_cursor_left_location(self) -> Location: target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above return target_row, target_column - def action_cursor_right(self) -> None: + def action_cursor_right(self, select: bool = False) -> None: """Move the cursor one location to the right. If the cursor is at the end of a line, attempt to go to the start of the next line. - """ - target = self.get_cursor_right_location() - self.move_cursor(target) - def action_cursor_right_select(self): - """Move the end of the selection one location to the right.""" + Args: + select: If True, select the text while moving. + """ target = self.get_cursor_right_location() - self.move_cursor(target, select=True) + self.move_cursor(target, select=select) def get_cursor_right_location(self) -> Location: """Get the location the cursor will move to if it moves right. @@ -1023,15 +1014,14 @@ def get_cursor_right_location(self) -> Location: target_column = 0 if self.cursor_at_end_of_row else cursor_column + 1 return target_row, target_column - def action_cursor_down(self) -> None: - """Move the cursor down one cell.""" - target = self.get_cursor_down_location() - self.move_cursor(target, record_width=False) + def action_cursor_down(self, select: bool = False) -> None: + """Move the cursor down one cell. - def action_cursor_down_select(self) -> None: - """Move the cursor down one cell, selecting the range between the old and new locations.""" + Args: + select: If True, select the text while moving. + """ target = self.get_cursor_down_location() - self.move_cursor(target, select=True, record_width=False) + self.move_cursor(target, record_width=False, select=select) def get_cursor_down_location(self) -> Location: """Get the location the cursor will move to if it moves down. @@ -1051,15 +1041,14 @@ def get_cursor_down_location(self) -> Location: target_column = clamp(target_column, 0, len(self.document[target_row])) return target_row, target_column - def action_cursor_up(self) -> None: - """Move the cursor up one cell.""" - target = self.get_cursor_up_location() - self.move_cursor(target, record_width=False) + def action_cursor_up(self, select: bool = False) -> None: + """Move the cursor up one cell. - def action_cursor_up_select(self) -> None: - """Move the cursor up one cell, selecting the range between the old and new locations.""" + Args: + select: If True, select the text while moving. + """ target = self.get_cursor_up_location() - self.move_cursor(target, select=True, record_width=False) + self.move_cursor(target, record_width=False, select=select) def get_cursor_up_location(self) -> Location: """Get the location the cursor will move to if it moves up. From 41ba10db9d2f0ecd620d12a9b32bc9ffe3b472de Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 30 Aug 2023 11:30:35 +0100 Subject: [PATCH 289/366] Use cell-width aware expand tabs implementation from @willmcgugan --- src/textual/app.py | 1 - src/textual/expand_tabs.py | 49 +++++++++++++++++++++++++++++++ src/textual/widgets/_text_area.py | 8 +++-- 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 src/textual/expand_tabs.py diff --git a/src/textual/app.py b/src/textual/app.py index dfea158ffe..f505bdfb62 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -379,7 +379,6 @@ def __init__( _environ=environ, force_terminal=True, safe_box=False, - tab_size=0, ) self._workers = WorkerManager(self) self.error_console = Console(markup=False, stderr=True) diff --git a/src/textual/expand_tabs.py b/src/textual/expand_tabs.py new file mode 100644 index 0000000000..9227f796c2 --- /dev/null +++ b/src/textual/expand_tabs.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import re + +from rich.cells import cell_len + +_TABS_SPLITTER_RE = re.compile(r"(.*?\t|.+?$)") + + +def expand_tabs_inline(line: str, tab_size: int = 4) -> str: + """Expands tabs, taking into account double cell characters. + + Args: + line: The text to expand tabs in. + tab_size: Number of cells in a tab. + Returns: + New string with tabs replaced with spaces. + """ + if "\t" not in line: + return line + new_line_parts: list[str] = [] + add_part = new_line_parts.append + cell_position = 0 + parts = _TABS_SPLITTER_RE.findall(line) + + for part in parts: + if part.endswith("\t"): + part = f"{part[:-1]} " + cell_position += cell_len(part) + tab_remainder = cell_position % tab_size + if tab_remainder: + spaces = tab_size - tab_remainder + part += spaces * " " + add_part(part) + + return "".join(new_line_parts) + + +if __name__ == "__main__": + print(expand_tabs_inline("\tbar")) + print(expand_tabs_inline("1\tbar")) + print(expand_tabs_inline("12\tbar")) + print(expand_tabs_inline("123\tbar")) + print(expand_tabs_inline("1234\tbar")) + print(expand_tabs_inline("💩\tbar")) + print(expand_tabs_inline("💩💩\tbar")) + print(expand_tabs_inline("💩💩💩\tbar")) + print(expand_tabs_inline("F💩\tbar")) + print(expand_tabs_inline("F💩O\tbar")) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index a09804ac7b..5cfef16fba 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -7,6 +7,8 @@ from rich.style import Style from rich.text import Text +from textual.expand_tabs import expand_tabs_inline + if TYPE_CHECKING: from tree_sitter import Language @@ -776,7 +778,7 @@ def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: total_cell_offset = 0 line = self.document[row_index] for column_index, character in enumerate(line): - total_cell_offset += cell_len(character.expandtabs(tab_width)) + total_cell_offset += cell_len(expand_tabs_inline(character, tab_width)) if total_cell_offset >= cell_width + 1: return column_index return len(line) @@ -818,7 +820,7 @@ def scroll_cursor_visible( """ row, column = self.selection.end text = self.document[row][:column] - column_offset = cell_len(text.expandtabs(self.indent_width)) + column_offset = cell_len(expand_tabs_inline(text, self.indent_width)) scroll_offset = self.scroll_to_region( Region(x=column_offset, y=row, width=3, height=1), spacing=Spacing(right=self.gutter_width), @@ -1205,7 +1207,7 @@ def get_column_width(self, row: int, column: int) -> int: The cell width of the column relative to the start of the row. """ line = self.document[row] - return cell_len(line[:column].expandtabs(self.indent_width)) + return cell_len(expand_tabs_inline(line[:column], self.indent_width)) def record_cursor_width(self) -> None: """Record the current cell width of the cursor. From 153da1ab5e57f32ac420421ac1c07e16589a079d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 30 Aug 2023 11:39:56 +0100 Subject: [PATCH 290/366] Construct strip with cell length --- src/textual/widgets/_text_area.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 5cfef16fba..e4ead299f5 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -571,6 +571,7 @@ def render_line(self, widget_y: int) -> Strip: line.stylize_before(active_line_style) # Build the gutter text for this line + gutter_width = self.gutter_width if self.show_line_numbers: if cursor_row == line_index: gutter_style = self.get_component_rich_style( @@ -579,7 +580,7 @@ def render_line(self, widget_y: int) -> Strip: else: gutter_style = self.get_component_rich_style("text-area--gutter") - gutter_width_no_margin = self.gutter_width - 2 + gutter_width_no_margin = gutter_width - 2 gutter = Text( f"{line_index + 1:>{gutter_width_no_margin}} ", style=gutter_style, @@ -596,9 +597,9 @@ def render_line(self, widget_y: int) -> Strip: ) # Crop the line to show only the visible part (some may be scrolled out of view) - gutter_strip = Strip(gutter_segments) + gutter_strip = Strip(gutter_segments, cell_length=gutter_width) text_strip = Strip(text_segments).crop( - scroll_x, scroll_x + virtual_width - self.gutter_width + scroll_x, scroll_x + virtual_width - gutter_width ) # Stylize the line the cursor is currently on. From efb4f4e829bcf0783928742d250080edbb49d302 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 30 Aug 2023 12:09:04 +0100 Subject: [PATCH 291/366] Some TextArea keyword-only arguments --- src/textual/widgets/_text_area.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index e4ead299f5..b7e18e248d 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -668,7 +668,7 @@ async def _on_key(self, event: events.Key) -> None: # None because we've checked that it's printable. assert insert is not None start, end = self.selection - self.replace(insert, start, end, False) + self.replace(insert, start, end, maintain_selection_offset=False) def get_target_document_location(self, event: MouseEvent) -> Location: """Given a MouseEvent, return the row and column offset of the event in document-space. @@ -1227,6 +1227,7 @@ def insert( self, text: str, location: Location | None = None, + *, maintain_selection_offset: bool = True, ) -> EditResult: """Insert text into the document. @@ -1250,6 +1251,7 @@ def delete( self, start: Location, end: Location, + *, maintain_selection_offset: bool = True, ) -> EditResult: """Delete the text between two locations in the document. @@ -1273,6 +1275,7 @@ def replace( insert: str, start: Location, end: Location, + *, maintain_selection_offset: bool = True, ) -> EditResult: """Replace text in the document with new text. From acf6228f8e0d0004e05416afdecc753502889b87 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 30 Aug 2023 16:52:48 +0100 Subject: [PATCH 292/366] Begin moving over to TextAreaTheme #skipci --- docs/widgets/text_area.md | 2 +- src/textual/_tree_sitter.py | 10 + src/textual/document/__init__.py | 4 +- .../document/_syntax_aware_document.py | 33 +-- src/textual/document/_syntax_theme.py | 191 -------------- src/textual/document/_text_area_theme.py | 244 ++++++++++++++++++ src/textual/widgets/_text_area.py | 96 ++++--- 7 files changed, 327 insertions(+), 253 deletions(-) create mode 100644 src/textual/_tree_sitter.py delete mode 100644 src/textual/document/_syntax_theme.py create mode 100644 src/textual/document/_text_area_theme.py diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 93dfd63d93..f9292cec6c 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -49,7 +49,7 @@ TODO | Name | Type | Default | Description | |---------------------|---------------------------|-------------------------|---------------------------------------------------| | `language` | `str \| Language \| None` | `None` | The language to use for syntax highlighting. | -| `theme` | `str \| SyntaxTheme` | `SyntaxTheme.default()` | The theme to use for syntax highlighting. | +| `theme` | `str \| TextAreaTheme` | `TextAreaTheme.default()` | The theme to use for syntax highlighting. | | `selection` | `Selection` | `Selection()` | The current selection. | | `show_line_numbers` | `bool` | `True` | Show or hide line numbers. | | `indent_width` | `int` | `4` | The number of spaces to indent and width of tabs. | diff --git a/src/textual/_tree_sitter.py b/src/textual/_tree_sitter.py new file mode 100644 index 0000000000..01e300115c --- /dev/null +++ b/src/textual/_tree_sitter.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +try: + from tree_sitter import Language, Parser, Tree + from tree_sitter.binding import Query + from tree_sitter_languages import get_language, get_parser + + TREE_SITTER = True +except ImportError: + TREE_SITTER = False diff --git a/src/textual/document/__init__.py b/src/textual/document/__init__.py index c395a77ff1..540c94079d 100644 --- a/src/textual/document/__init__.py +++ b/src/textual/document/__init__.py @@ -7,7 +7,7 @@ StartColumn, SyntaxAwareDocument, ) -from ._syntax_theme import DEFAULT_SYNTAX_THEME, SyntaxTheme +from ._text_area_theme import DEFAULT_SYNTAX_THEME, TextAreaTheme __all__ = [ "Document", @@ -20,6 +20,6 @@ "Selection", "StartColumn", "SyntaxAwareDocument", - "SyntaxTheme", + "TextAreaTheme", "VALID_LANGUAGES", ] diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 092d10fa93..7123720f7e 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -2,7 +2,6 @@ from collections import defaultdict from functools import lru_cache -from pathlib import Path from typing import Optional, Tuple from rich.text import Text @@ -21,10 +20,7 @@ from textual.document._document import Document, EditResult, Location, _utf8_encode from textual.document._languages import VALID_LANGUAGES -from textual.document._syntax_theme import SyntaxTheme - -TREE_SITTER_PATH = Path(__file__) / "../../../../tree-sitter/" -HIGHLIGHTS_PATH = TREE_SITTER_PATH / "highlights/" +from textual.document._text_area_theme import TextAreaTheme StartColumn = int EndColumn = Optional[int] @@ -48,7 +44,9 @@ class SyntaxAwareDocument(Document): """ def __init__( - self, text: str, language: str | Language, syntax_theme: str | SyntaxTheme + self, + text: str, + language: str | Language, ): """Construct a SyntaxAwareDocument. @@ -56,15 +54,12 @@ def __init__( text: The initial text contained in the document. language: The language to use. You can pass a string to use a supported language, or pass in your own tree-sitter `Language` object. - syntax_theme: The syntax highlighting theme to use. You can pass a string - to use a builtin theme, or construct your own custom SyntaxTheme and - provide that. """ if not TREE_SITTER: raise RuntimeError("SyntaxAwareDocument is unavailable on Python 3.7.") super().__init__(text) - self._language: Language | None = None + self.language: Language | None = None """The tree-sitter Language or None if tree-sitter is unavailable.""" self._parser: Parser | None = None @@ -73,7 +68,7 @@ def __init__( self._syntax_tree: Tree | None = None """The tree-sitter Tree (syntax tree) built from the document.""" - self._syntax_theme: SyntaxTheme | None = None + self._syntax_theme: TextAreaTheme | None = None """The syntax highlighting theme to use.""" self._highlights: dict[int, list[Highlight]] = defaultdict(list) @@ -83,26 +78,14 @@ def __init__( if isinstance(language, str): if language not in VALID_LANGUAGES: raise RuntimeError(f"Invalid language {language!r}") - self._language = get_language(language) + self.language = get_language(language) self._parser = get_parser(language) else: - self._language = language + self.language = language self._parser = Parser() self._parser.set_language(language) - highlight_query_path = ( - Path(HIGHLIGHTS_PATH.resolve()) / f"{self._language.name}.scm" - ) - if isinstance(syntax_theme, SyntaxTheme): - self._syntax_theme = syntax_theme - else: - self._syntax_theme = SyntaxTheme.get_theme(syntax_theme) - - self._syntax_theme.highlight_query = highlight_query_path.read_text() self._syntax_tree = self._parser.parse(self._read_callable) # type: ignore - self._query: Query = self._language.query( - self._syntax_theme.highlight_query - ) self._prepare_highlights() def replace_range(self, start: Location, end: Location, text: str) -> EditResult: diff --git a/src/textual/document/_syntax_theme.py b/src/textual/document/_syntax_theme.py deleted file mode 100644 index 792250a97d..0000000000 --- a/src/textual/document/_syntax_theme.py +++ /dev/null @@ -1,191 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field - -from rich.style import Style - -_NULL_STYLE = Style.null() - -_MONOKAI = { - "string": Style(color="#E6DB74"), - "string.documentation": Style(color="#E6DB74"), - "comment": Style(color="#75715E"), - "keyword": Style(color="#F92672"), - "operator": Style(color="#F92672"), - "repeat": Style(color="#F92672"), - "exception": Style(color="#F92672"), - "include": Style(color="#F92672"), - "keyword.function": Style(color="#F92672"), - "keyword.return": Style(color="#F92672"), - "keyword.operator": Style(color="#F92672"), - "conditional": Style(color="#F92672"), - "number": Style(color="#AE81FF"), - "float": Style(color="#AE81FF"), - "class": Style(color="#A6E22E"), - "function": Style(color="#A6E22E"), - "function.call": Style(color="#A6E22E"), - "method": Style(color="#A6E22E"), - "method.call": Style(color="#A6E22E"), - "boolean": Style(color="#66D9EF", italic=True), - "json.null": Style(color="#66D9EF", italic=True), - # "constant": Style(color="#AE81FF"), - # "variable": Style(color="white"), - # "parameter": Style(color="cyan"), - # "type": Style(color="cyan"), - # "escape": Style(bgcolor="magenta"), - "regex.punctuation.bracket": Style(color="#F92672"), - "regex.operator": Style(color="#F92672"), - # "error": Style(color="black", bgcolor="red"), - "json.error": _NULL_STYLE, - "html.end_tag_error": Style(color="red", underline=True), - "tag": Style(color="#F92672"), - "yaml.field": Style(color="#F92672", bold=True), - "json.label": Style(color="#F92672", bold=True), - "toml.type": Style(color="#F92672"), - "toml.datetime": Style(color="#AE81FF"), - "toml.error": _NULL_STYLE, - "heading": Style(color="#F92672", bold=True), - "bold": Style(bold=True), - "italic": Style(italic=True), - "strikethrough": Style(strike=True), - "link": Style(color="#66D9EF", underline=True), - "inline_code": Style(color="#F92672"), -} - -_DRACULA = { - "string": Style(color="#f1fa8c"), - "string.documentation": Style(color="#f1fa8c"), - "comment": Style(color="#6272a4"), - "keyword": Style(color="#ff79c6"), - "operator": Style(color="#ff79c6"), - "repeat": Style(color="#ff79c6"), - "exception": Style(color="#ff79c6"), - "include": Style(color="#ff79c6"), - "keyword.function": Style(color="#ff79c6"), - "keyword.return": Style(color="#ff79c6"), - "keyword.operator": Style(color="#ff79c6"), - "conditional": Style(color="#ff79c6"), - "number": Style(color="#bd93f9"), - "float": Style(color="#bd93f9"), - "class": Style(color="#50fa7b"), - "function": Style(color="#50fa7b"), - "function.call": Style(color="#50fa7b"), - "method": Style(color="#50fa7b"), - "method.call": Style(color="#50fa7b"), - "boolean": Style(color="#bd93f9"), - "json.null": Style(color="#bd93f9"), - # "constant": Style(color="#bd93f9"), - # "variable": Style(color="white"), - # "parameter": Style(color="cyan"), - # "type": Style(color="cyan"), - # "escape": Style(bgcolor="magenta"), - "regex.punctuation.bracket": Style(color="#ff79c6"), - "regex.operator": Style(color="#ff79c6"), - # "error": Style(color="black", bgcolor="red"), - "json.error": _NULL_STYLE, - "html.end_tag_error": Style(color="#F83333", underline=True), - "tag": Style(color="#ff79c6"), - "yaml.field": Style(color="#ff79c6", bold=True), - "json.label": Style(color="#ff79c6", bold=True), - "toml.type": Style(color="#ff79c6"), - "toml.datetime": Style(color="#bd93f9"), - "toml.error": _NULL_STYLE, - "heading": Style(color="#ff79c6", bold=True), - "bold": Style(bold=True), - "italic": Style(italic=True), - "strikethrough": Style(strike=True), - "link": Style(color="#bd93f9", underline=True), - "inline_code": Style(color="#ff79c6"), -} - -_BUILTIN_THEMES = { - "monokai": _MONOKAI, - "dracula": _DRACULA, -} - - -@dataclass -class SyntaxTheme: - """Maps tree-sitter names to Rich styles for syntax-highlighting in `TextArea`. - - For example, consider the following snippet from the `markdown.scm` highlight - query file. We've assigned the `heading_content` token type to the name `heading`. - - ``` - (heading_content) @heading - ``` - - Now, we can map this `heading` name to a Rich style, and it will be styled as - such in the `TextArea`, assuming a parser which returns a `heading_content` - node is used (as will be the case when language="markdown"): - - ``` - SyntaxTheme('my_theme', {'heading': Style(color='cyan', bold=True)}) - ``` - """ - - name: str | None = None - """The name of the theme.""" - - style_mapping: dict[str, Style] = field(default_factory=dict) - """The mapping of tree-sitter names from the `highlight_query` to Rich styles.""" - - highlight_query: str = "" - """The tree-sitter query to use for highlighting. - - See `*.scm` files in this repo for examples, as well as the tree-sitter docs. - - Note that the `highlight_query` must only refer to nodes which are defined in the - tree-sitter language/parser currently being used. If the query refers to nodes - that the parser does not declare, tree-sitter will raise an exception. - """ - - @classmethod - def get_theme(cls, theme_name: str) -> "SyntaxTheme": - """Get a `SyntaxTheme` by name. - - Given a `theme_name` return the corresponding `SyntaxTheme` object. - - Check the available `SyntaxTheme`s by calling `SyntaxTheme.available_themes()`. - - Args: - theme_name: The name of the theme. - - Returns: - The `SyntaxTheme` corresponding to the name. - """ - return cls(theme_name, _BUILTIN_THEMES.get(theme_name, {})) - - def get_highlight(self, name: str) -> Style: - """Return the Rich style corresponding to the name defined in the tree-sitter - highlight query for the current theme. - - Args: - name: The name of the highlight. - - Returns: - The `Style` to use for this highlight. - """ - return self.style_mapping.get(name, _NULL_STYLE) - - @classmethod - def available_themes(cls) -> list[SyntaxTheme]: - """Get a list of all available SyntaxThemes. - - Returns: - A list of all available SyntaxThemes. - """ - return [SyntaxTheme(name, mapping) for name, mapping in _BUILTIN_THEMES.items()] - - @classmethod - def default(cls) -> SyntaxTheme: - """Get the default syntax theme. - - Returns: - The default SyntaxTheme (probably Monokai). - """ - return DEFAULT_SYNTAX_THEME - - -DEFAULT_SYNTAX_THEME = SyntaxTheme.get_theme("monokai") -"""The default syntax highlighting theme used by Textual.""" diff --git a/src/textual/document/_text_area_theme.py b/src/textual/document/_text_area_theme.py new file mode 100644 index 0000000000..89105372a4 --- /dev/null +++ b/src/textual/document/_text_area_theme.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from rich.style import Style + +from textual.color import Color + + +@dataclass +class TextAreaTheme: + """Maps tree-sitter names to Rich styles for syntax-highlighting in `TextArea`. + + For example, consider the following snippet from the `markdown.scm` highlight + query file. We've assigned the `heading_content` token type to the name `heading`. + + ``` + (heading_content) @heading + ``` + + Now, we can map this `heading` name to a Rich style, and it will be styled as + such in the `TextArea`, assuming a parser which returns a `heading_content` + node is used (as will be the case when language="markdown"): + + ``` + SyntaxTheme('my_theme', {'heading': Style(color='cyan', bold=True)}) + ``` + """ + + name: str | None = None + """The name of the theme.""" + + token_styles: dict[str, TextAreaStyle] = field(default_factory=dict) + """The mapping of tree-sitter names from the `highlight_query` to Rich styles.""" + + base_style: TextAreaStyle | None = None + """The background style of the text area. If `None` the parent style will be used.""" + + gutter_style: TextAreaStyle | None = None + """The style of the gutter. If `None`, a legible TextAreaStyle will be generated.""" + + cursor_style: TextAreaStyle | None = None + """The style of the cursor. If `None`, the legible TextAreaStyle will be generated.""" + + cursor_line_style: TextAreaStyle | None = None + """The style to apply to the line the cursor is on. If `None`, a legible TextAreaStyle will be generated.""" + + cursor_line_gutter_style: TextAreaStyle | None = None + """The style to apply to the gutter of the line the cursor is on. If `None`, a legible TextAreaStyle will be + generated.""" + + bracket_matching_style: TextAreaStyle | None = None + """The style to apply to matching brackets. If `None`, a legible TextAreaStyle will be generated.""" + + selection_style: TextAreaStyle | None = None + """The style of the selection. If `None` a default selection TextAreaStyle will be generated.""" + + @classmethod + def get_theme(cls, theme_name: str) -> "TextAreaTheme": + """Get a `SyntaxTheme` by name. + + Given a `theme_name` return the corresponding `SyntaxTheme` object. + + Check the available `SyntaxTheme`s by calling `SyntaxTheme.available_themes()`. + + Args: + theme_name: The name of the theme. + + Returns: + The `SyntaxTheme` corresponding to the name. + """ + return cls(theme_name, _BUILTIN_THEMES.get(theme_name, {})) + + def get_highlight(self, name: str) -> TextAreaTheme: + """Return the Rich style corresponding to the name defined in the tree-sitter + highlight query for the current theme. + + Args: + name: The name of the highlight. + + Returns: + The `Style` to use for this highlight. + """ + return self.token_styles.get(name, _NULL_STYLE) + + @classmethod + def available_themes(cls) -> list[TextAreaTheme]: + """Get a list of all available SyntaxThemes. + + Returns: + A list of all available SyntaxThemes. + """ + return [ + TextAreaTheme(name, mapping) for name, mapping in _BUILTIN_THEMES.items() + ] + + @classmethod + def default(cls) -> TextAreaTheme: + """Get the default syntax theme. + + Returns: + The default SyntaxTheme (probably Monokai). + """ + return DEFAULT_SYNTAX_THEME + + +@dataclass +class TextAreaStyle: + foreground_color: str | Color = None + background_color: str | Color = None + bold: bool = False + italic: bool = False + strikethrough: bool = False + underline: bool = False + + def __post_init__(self) -> None: + self.background_color = Color.parse(self.background_color) + self.foreground_color = Color.parse(self.foreground_color).blend( + self.background_color + ) + + # The default for tree-sitter tokens which aren't mapped to styles. + self.default_style = Style( + color=self.foreground_color.rich_color, + bgcolor=self.background_color.rich_color, + bold=self.bold, + italic=self.italic, + strike=self.strikethrough, + underline=self.underline, + ) + + +_NULL_STYLE = TextAreaStyle() + +_MONOKAI = TextAreaTheme( + name="monokai", + base_style=TextAreaStyle("#f8f8f2", "#272822"), + gutter_style=TextAreaStyle("#90908a", "#272822"), + cursor_style=TextAreaStyle("#f8f8f0"), + cursor_line_style=TextAreaStyle(background_color="#3e3d32"), + cursor_line_gutter_style=TextAreaStyle("#c2c2bf", "#3e3d32"), + bracket_matching_style=TextAreaStyle("#414339"), + selection_style=TextAreaStyle(background_color="#878b9180"), + token_styles={ + "string": TextAreaStyle("#E6DB74"), + "string.documentation": TextAreaStyle("#E6DB74"), + "comment": TextAreaStyle("#75715E"), + "keyword": TextAreaStyle("#F92672"), + "operator": TextAreaStyle("#F92672"), + "repeat": TextAreaStyle("#F92672"), + "exception": TextAreaStyle("#F92672"), + "include": TextAreaStyle("#F92672"), + "keyword.function": TextAreaStyle("#F92672"), + "keyword.return": TextAreaStyle("#F92672"), + "keyword.operator": TextAreaStyle("#F92672"), + "conditional": TextAreaStyle("#F92672"), + "number": TextAreaStyle("#AE81FF"), + "float": TextAreaStyle("#AE81FF"), + "class": TextAreaStyle("#A6E22E"), + "function": TextAreaStyle("#A6E22E"), + "function.call": TextAreaStyle("#A6E22E"), + "method": TextAreaStyle("#A6E22E"), + "method.call": TextAreaStyle("#A6E22E"), + "boolean": TextAreaStyle("#66D9EF", italic=True), + "json.null": TextAreaStyle("#66D9EF", italic=True), + # "constant": TextAreaStyle("#AE81FF"), + # "variable": TextAreaStyle("white"), + # "parameter": TextAreaStyle("cyan"), + # "type": TextAreaStyle("cyan"), + # "escape": TextAreaStyle("magenta"), + # "error": TextAreaStyle("TextAreaStyle", "red"), + "regex.punctuation.bracket": TextAreaStyle("#F92672"), + "regex.operator": TextAreaStyle("#F92672"), + # "json.error": _NULL_STYLE, + "html.end_tag_error": TextAreaStyle("red", underline=True), + "tag": TextAreaStyle("#F92672"), + "yaml.field": TextAreaStyle("#F92672", bold=True), + "json.label": TextAreaStyle("#F92672", bold=True), + "toml.type": TextAreaStyle("#F92672"), + "toml.datetime": TextAreaStyle("#AE81FF"), + # "toml.error": _NULL_STYLE, + "heading": TextAreaStyle("#F92672", bold=True), + "bold": TextAreaStyle(bold=True), + "italic": TextAreaStyle(italic=True), + "strikethrough": TextAreaStyle(strikethrough=True), + "link": TextAreaStyle("#66D9EF", underline=True), + "inline_code": TextAreaStyle("#F92672"), + }, +) + +# _DRACULA = { +# "string": Style(color="#f1fa8c"), +# "string.documentation": Style(color="#f1fa8c"), +# "comment": Style(color="#6272a4"), +# "keyword": Style(color="#ff79c6"), +# "operator": Style(color="#ff79c6"), +# "repeat": Style(color="#ff79c6"), +# "exception": Style(color="#ff79c6"), +# "include": Style(color="#ff79c6"), +# "keyword.function": Style(color="#ff79c6"), +# "keyword.return": Style(color="#ff79c6"), +# "keyword.operator": Style(color="#ff79c6"), +# "conditional": Style(color="#ff79c6"), +# "number": Style(color="#bd93f9"), +# "float": Style(color="#bd93f9"), +# "class": Style(color="#50fa7b"), +# "function": Style(color="#50fa7b"), +# "function.call": Style(color="#50fa7b"), +# "method": Style(color="#50fa7b"), +# "method.call": Style(color="#50fa7b"), +# "boolean": Style(color="#bd93f9"), +# "json.null": Style(color="#bd93f9"), +# # "constant": Style(color="#bd93f9"), +# # "variable": Style(color="white"), +# # "parameter": Style(color="cyan"), +# # "type": Style(color="cyan"), +# # "escape": Style(bgcolor="magenta"), +# "regex.punctuation.bracket": Style(color="#ff79c6"), +# "regex.operator": Style(color="#ff79c6"), +# # "error": Style(color="black", bgcolor="red"), +# "json.error": _NULL_STYLE, +# "html.end_tag_error": Style(color="#F83333", underline=True), +# "tag": Style(color="#ff79c6"), +# "yaml.field": Style(color="#ff79c6", bold=True), +# "json.label": Style(color="#ff79c6", bold=True), +# "toml.type": Style(color="#ff79c6"), +# "toml.datetime": Style(color="#bd93f9"), +# "toml.error": _NULL_STYLE, +# "heading": Style(color="#ff79c6", bold=True), +# "bold": Style(bold=True), +# "italic": Style(italic=True), +# "strikethrough": Style(strike=True), +# "link": Style(color="#bd93f9", underline=True), +# "inline_code": Style(color="#ff79c6"), +# } + +_BUILTIN_THEMES = { + "monokai": _MONOKAI, + # "dracula": _DRACULA, +} + + +DEFAULT_SYNTAX_THEME = TextAreaTheme.get_theme("monokai") +"""The default syntax highlighting theme used by Textual.""" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index b7e18e248d..c7497e7414 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -2,7 +2,8 @@ import re from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, ClassVar, Iterable +from pathlib import Path +from typing import TYPE_CHECKING, Any, Iterable from rich.style import Style from rich.text import Text @@ -11,6 +12,7 @@ if TYPE_CHECKING: from tree_sitter import Language + from tree_sitter.binding import Query from textual import events, log from textual._cells import cell_len @@ -22,7 +24,8 @@ EditResult, Location, Selection, - SyntaxTheme, + SyntaxAwareDocument, + TextAreaTheme, ) from textual.events import MouseEvent from textual.geometry import Offset, Region, Size, Spacing, clamp @@ -32,6 +35,8 @@ _OPENING_BRACKETS = {"{": "}", "[": "]", "(": ")"} _CLOSING_BRACKETS = {v: k for k, v in _OPENING_BRACKETS.items()} +_TREE_SITTER_PATH = Path(__file__) / "../../../../tree-sitter/" +_HIGHLIGHTS_PATH = _TREE_SITTER_PATH / "highlights/" class TextArea(ScrollView, can_focus=True): @@ -91,24 +96,6 @@ class TextArea(ScrollView, can_focus=True): } """ - COMPONENT_CLASSES: ClassVar[set[str]] = { - "text-area--cursor", - "text-area--gutter", - "text-area--cursor-line", - "text-area--cursor-line-gutter", - "text-area--selection", - "text-area--matching-bracket", - } - """| Class | Description | -|:--------------------------------|:-------------------------------------------------| -| `text-area--cursor` | Targets the cursor. | -| `text-area--gutter` | Targets the gutter (line number column). | -| `text-area--cursor-line` | Targets the line of text the cursor is on. | -| `text-area--cursor-line-gutter` | Targets the gutter of the line the cursor is on. | -| `text-area--selection` | Targets the selected text. | -| `text-area--matching-bracket` | Targets the bracket matching the cursor bracket. | - """ - BINDINGS = [ Binding("escape", "screen.focus_next", "Shift Focus", show=False), # Cursor movement @@ -205,15 +192,13 @@ class TextArea(ScrollView, can_focus=True): This must be set to a valid, non-None value for syntax highlighting to work. - Check valid languages using the `TextArea.valid_languages` property. - If the value is a string, a built-in parser will be used. If you wish to add support for an unsupported language, you'll have to pass in the tree-sitter `Language` object directly rather than the string language name. """ - theme: Reactive[str | SyntaxTheme] = reactive(SyntaxTheme.default()) + theme: Reactive[str | TextAreaTheme] = reactive(TextAreaTheme.default()) """The theme to syntax highlight with. Supply a `SyntaxTheme` object to customise highlighting, or supply a builtin @@ -222,6 +207,12 @@ class TextArea(ScrollView, can_focus=True): Syntax highlighting is only possible when the `language` attribute is set. """ + highlight_query: Reactive[str | "Query" | None] = reactive(None) + """The tree-sitter query to use to retrieve syntax highlighting tokens. + + If `None`, the default highlighting query will be fetched for the current language. + """ + selection: Reactive[Selection] = reactive(Selection(), always_update=True) """The selection start and end locations (zero-based line_index, offset). @@ -258,6 +249,11 @@ class TextArea(ScrollView, can_focus=True): def __init__( self, + text: str = "", + *, + language: str | "Language" | None = None, + theme: str | TextAreaTheme | None = TextAreaTheme.default(), + highlight_query: str | None = None, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -271,12 +267,32 @@ def __init__( classes: One or more Textual CSS compatible class names separated by spaces. disabled: True if the widget is disabled. """ - super().__init__(name=name, id=id, classes=classes, disabled=disabled) - self.document: DocumentBase = Document("") + self.document: DocumentBase = self._set_document(text, language) """The document this widget is currently editing.""" + if isinstance(theme, str): + theme = TextAreaTheme.get_theme(theme) + + self.theme = theme + """The theme of the `TextArea`.""" + + self.highlight_query = None + """The query to run which returns tokens which will be highlighted using the theme.""" + + # TODO - create a method to configure all highlighting stuff in the same place. + if theme is not None and highlight_query is None: + language_name = self.document.language.name + highlight_query_path = ( + Path(_HIGHLIGHTS_PATH.resolve()) / f"{language_name}.scm" + ) + highlight_query = highlight_query_path.read_text() + + self._highlight_query: Query | None = None + if self.is_syntax_aware and highlight_query is not None: + self._query: Query | None = self.language.query(highlight_query) + self.indent_type: Literal["tabs", "spaces"] = "spaces" """Whether to indent using tabs or spaces.""" @@ -299,6 +315,10 @@ def __init__( cursor is currently at. If the cursor is at a bracket, or there's no matching bracket, this will be `None`.""" + def _configure_syntax_highlighting() -> None: + """Configure syntax highlighting based on the theme and highlighting query.""" + pass + def _watch_selection(self, selection: Selection) -> None: """When the cursor moves, scroll it into view.""" self.scroll_cursor_visible() @@ -359,11 +379,15 @@ def _validate_selection(self, selection: Selection) -> Selection: def _watch_language(self) -> None: """When the language is updated, update the type of document.""" - self._reload_document() + self._set_document(self.text, self.language) def _watch_theme(self) -> None: """When the theme is updated, update the document.""" - self._reload_document() + self._set_document(self.text, self.language) + + def _watch_highlight_query(self) -> None: + """When the highlight query is updated, refresh the document.""" + self._set_document(self.text, self.language) def _watch_show_line_numbers(self) -> None: """The line number gutter contributes to virtual size, so recalculate.""" @@ -373,10 +397,8 @@ def _watch_indent_width(self) -> None: """Changing width of tabs will change document display width.""" self._refresh_size() - def _reload_document(self) -> None: + def _set_document(self, text: str, language: str | None) -> DocumentBase: """Recreate the document based on the language and theme currently set.""" - language = self.language - text = self.document.text if not language: # If there's no language set, we don't need to use a SyntaxAwareDocument. self.document = Document(text) @@ -384,13 +406,15 @@ def _reload_document(self) -> None: try: from textual.document._syntax_aware_document import SyntaxAwareDocument - self.document = SyntaxAwareDocument(text, language, self.theme) + self.document = SyntaxAwareDocument(text, language) except RuntimeError: # SyntaxAwareDocument isn't available on Python 3.7. - # Fall back to the standard document. + # Fall back to the standard Document. log.warning("Syntax highlighting isn't available on Python 3.7.") self.document = Document(text) + return self.document + @property def _visible_line_indices(self) -> tuple[int, int]: """Return the visible line indices as a tuple (top, bottom). @@ -406,8 +430,7 @@ def load_text(self, text: str) -> None: Args: text: The text to load into the TextArea. """ - self.document = Document(text) - self._reload_document() + self._set_document(text, self.language) self.move_cursor((0, 0)) self._refresh_size() @@ -421,6 +444,11 @@ def load_document(self, document: DocumentBase) -> None: self.move_cursor((0, 0)) self._refresh_size() + @property + def is_syntax_aware(self) -> bool: + """True if the TextArea is currently syntax aware - i.e. it's parsing document content.""" + return isinstance(self.document, SyntaxAwareDocument) + def _yield_character_locations( self, start: Location ) -> Iterable[tuple[str, Location]]: From e4cd06f09a2e316a544f94c185ce6ae5219c02ca Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 30 Aug 2023 17:16:04 +0100 Subject: [PATCH 293/366] Prepare queries inside document #skip-ci --- .../document/_syntax_aware_document.py | 10 ++++++--- src/textual/document/_text_area_theme.py | 6 ++--- src/textual/widgets/_text_area.py | 22 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 7123720f7e..96d8ad3ea5 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -55,9 +55,6 @@ def __init__( language: The language to use. You can pass a string to use a supported language, or pass in your own tree-sitter `Language` object. """ - if not TREE_SITTER: - raise RuntimeError("SyntaxAwareDocument is unavailable on Python 3.7.") - super().__init__(text) self.language: Language | None = None """The tree-sitter Language or None if tree-sitter is unavailable.""" @@ -88,6 +85,13 @@ def __init__( self._syntax_tree = self._parser.parse(self._read_callable) # type: ignore self._prepare_highlights() + def prepare_query(self, query: str) -> Query | None: + if TREE_SITTER: + prepared_query = self.language.query(query) + else: + prepared_query = None + return prepared_query + def replace_range(self, start: Location, end: Location, text: str) -> EditResult: """Replace text at the given range. diff --git a/src/textual/document/_text_area_theme.py b/src/textual/document/_text_area_theme.py index 89105372a4..9155b98307 100644 --- a/src/textual/document/_text_area_theme.py +++ b/src/textual/document/_text_area_theme.py @@ -81,7 +81,7 @@ def get_highlight(self, name: str) -> TextAreaTheme: Returns: The `Style` to use for this highlight. """ - return self.token_styles.get(name, _NULL_STYLE) + return self.token_styles.get(name) @classmethod def available_themes(cls) -> list[TextAreaTheme]: @@ -116,7 +116,7 @@ class TextAreaStyle: def __post_init__(self) -> None: self.background_color = Color.parse(self.background_color) self.foreground_color = Color.parse(self.foreground_color).blend( - self.background_color + self.background_color, factor=1 ) # The default for tree-sitter tokens which aren't mapped to styles. @@ -130,8 +130,6 @@ def __post_init__(self) -> None: ) -_NULL_STYLE = TextAreaStyle() - _MONOKAI = TextAreaTheme( name="monokai", base_style=TextAreaStyle("#f8f8f2", "#272822"), diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index c7497e7414..cf6f9d95a0 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -269,7 +269,7 @@ def __init__( """ super().__init__(name=name, id=id, classes=classes, disabled=disabled) - self.document: DocumentBase = self._set_document(text, language) + self.document = SyntaxAwareDocument(text, language) """The document this widget is currently editing.""" if isinstance(theme, str): @@ -278,20 +278,16 @@ def __init__( self.theme = theme """The theme of the `TextArea`.""" - self.highlight_query = None - """The query to run which returns tokens which will be highlighted using the theme.""" - - # TODO - create a method to configure all highlighting stuff in the same place. - if theme is not None and highlight_query is None: - language_name = self.document.language.name + language_name = self.document.language.name + if highlight_query is None and language_name is not None: + # Try to retrieve the default highlight query for the language. highlight_query_path = ( Path(_HIGHLIGHTS_PATH.resolve()) / f"{language_name}.scm" ) highlight_query = highlight_query_path.read_text() - self._highlight_query: Query | None = None - if self.is_syntax_aware and highlight_query is not None: - self._query: Query | None = self.language.query(highlight_query) + self._highlight_query = self.document.prepare_query(highlight_query) + """The query to use for syntax highlighting, or `None` if not available.""" self.indent_type: Literal["tabs", "spaces"] = "spaces" """Whether to indent using tabs or spaces.""" @@ -315,9 +311,11 @@ def __init__( cursor is currently at. If the cursor is at a bracket, or there's no matching bracket, this will be `None`.""" - def _configure_syntax_highlighting() -> None: + def _configure_syntax_highlighting(self) -> None: """Configure syntax highlighting based on the theme and highlighting query.""" - pass + # When the language is modified, we should reset the highlighting query. + # When the highlighting query is modified, we should reset the language. + # If you want to modify both together, what do you do? def _watch_selection(self, selection: Selection) -> None: """When the cursor moves, scroll it into view.""" From 2d0a79659d097fff1b51fafee4a45855feb58fd0 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 30 Aug 2023 17:25:56 +0100 Subject: [PATCH 294/366] Add comment --- src/textual/widgets/_text_area.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index cf6f9d95a0..db6727b75e 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -207,12 +207,6 @@ class TextArea(ScrollView, can_focus=True): Syntax highlighting is only possible when the `language` attribute is set. """ - highlight_query: Reactive[str | "Query" | None] = reactive(None) - """The tree-sitter query to use to retrieve syntax highlighting tokens. - - If `None`, the default highlighting query will be fetched for the current language. - """ - selection: Reactive[Selection] = reactive(Selection(), always_update=True) """The selection start and end locations (zero-based line_index, offset). @@ -253,7 +247,6 @@ def __init__( *, language: str | "Language" | None = None, theme: str | TextAreaTheme | None = TextAreaTheme.default(), - highlight_query: str | None = None, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -278,6 +271,9 @@ def __init__( self.theme = theme """The theme of the `TextArea`.""" + # TODO - the highlight query can only be adjusted using a method. + # we need to do this because of the dependency between highlight query and + # tree-sitter parser. language_name = self.document.language.name if highlight_query is None and language_name is not None: # Try to retrieve the default highlight query for the language. From 1933ca74f03585e88d44339776ad8a89a302468a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 31 Aug 2023 17:53:27 +0100 Subject: [PATCH 295/366] Refactoring --- .../document/_syntax_aware_document.py | 197 +++++++++--------- src/textual/widgets/_text_area.py | 162 +++++++++----- 2 files changed, 214 insertions(+), 145 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 96d8ad3ea5..d1fb3d792f 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -2,7 +2,7 @@ from collections import defaultdict from functools import lru_cache -from typing import Optional, Tuple +from typing import Any, Optional, Tuple from rich.text import Text from typing_extensions import TYPE_CHECKING @@ -55,6 +55,10 @@ def __init__( language: The language to use. You can pass a string to use a supported language, or pass in your own tree-sitter `Language` object. """ + + if not TREE_SITTER: + raise RuntimeError("SyntaxAwareDocument unavailable.") + super().__init__(text) self.language: Language | None = None """The tree-sitter Language or None if tree-sitter is unavailable.""" @@ -71,19 +75,23 @@ def __init__( self._highlights: dict[int, list[Highlight]] = defaultdict(list) """Mapping line numbers to the set of highlights for that line.""" - if TREE_SITTER: - if isinstance(language, str): - if language not in VALID_LANGUAGES: - raise RuntimeError(f"Invalid language {language!r}") - self.language = get_language(language) - self._parser = get_parser(language) - else: - self.language = language - self._parser = Parser() - self._parser.set_language(language) - - self._syntax_tree = self._parser.parse(self._read_callable) # type: ignore - self._prepare_highlights() + # If the language is `None`, then avoid doing any parsing related stuff. + if isinstance(language, str): + if language not in VALID_LANGUAGES: + raise RuntimeError(f"Invalid language {language!r}") + self.language = get_language(language) + self._parser = get_parser(language) + else: + self.language = language + self._parser = Parser() + self._parser.set_language(language) + + self._syntax_tree = self._parser.parse(self._read_callable) # type: ignore + self._prepare_highlights() + + @property + def language_name(self) -> str | None: + return self.language.name if self.language else None def prepare_query(self, query: str) -> Query | None: if TREE_SITTER: @@ -92,6 +100,19 @@ def prepare_query(self, query: str) -> Query | None: prepared_query = None return prepared_query + def query_syntax_tree( + self, + query: Query, + start_point: tuple[int, int] | None = None, + end_point: tuple[int, int] | None = None, + ) -> Any: + captures_kwargs = {} + if start_point is not None: + captures_kwargs["start_point"] = start_point + if end_point is not None: + captures_kwargs["end_point"] = end_point + return query.captures(self._syntax_tree.root_node, **captures_kwargs) + def replace_range(self, start: Location, end: Location, text: str) -> EditResult: """Replace text at the given range. @@ -114,24 +135,23 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult replace_result = super().replace_range(start, end, text) - if TREE_SITTER: - text_byte_length = len(_utf8_encode(text)) - end_location = replace_result.end_location - assert self._syntax_tree is not None - assert self._parser is not None - self._syntax_tree.edit( - start_byte=start_byte, - old_end_byte=old_end_byte, - new_end_byte=start_byte + text_byte_length, - start_point=start_point, - old_end_point=old_end_point, - new_end_point=self._location_to_point(end_location), - ) - # Incrementally parse the document. - self._syntax_tree = self._parser.parse( - self._read_callable, self._syntax_tree # type: ignore[arg-type] - ) - self._prepare_highlights() + text_byte_length = len(_utf8_encode(text)) + end_location = replace_result.end_location + assert self._syntax_tree is not None + assert self._parser is not None + self._syntax_tree.edit( + start_byte=start_byte, + old_end_byte=old_end_byte, + new_end_byte=start_byte + text_byte_length, + start_point=start_point, + old_end_point=old_end_point, + new_end_point=self._location_to_point(end_location), + ) + # Incrementally parse the document. + self._syntax_tree = self._parser.parse( + self._read_callable, self._syntax_tree # type: ignore[arg-type] + ) + self._prepare_highlights() return replace_result @@ -146,8 +166,6 @@ def get_line_text(self, line_index: int) -> Text: """ line_string = self[line_index] line = Text(line_string, end="") - if not TREE_SITTER or self._syntax_theme is None: - return line highlights = self._highlights if highlights: @@ -174,9 +192,6 @@ def _location_to_byte_offset(self, location: Location) -> int: Returns: An integer byte offset for the given location. """ - if not TREE_SITTER: - return 0 - lines = self._lines row, column = location lines_above = lines[:row] @@ -202,9 +217,6 @@ def _location_to_point(self, location: Location) -> tuple[int, int]: Returns: The point corresponding to that location (row index, column byte offset). """ - if not TREE_SITTER: - return 0, 0 - lines = self._lines row, column = location if row < len(lines): @@ -213,60 +225,57 @@ def _location_to_point(self, location: Location) -> tuple[int, int]: bytes_on_left = 0 return row, bytes_on_left - def _prepare_highlights( - self, - start_point: tuple[int, int] | None = None, - end_point: tuple[int, int] | None = None, - ) -> None: - """Query the tree for ranges to highlights, and update the internal highlights mapping. - - Args: - start_point: The point to start looking for highlights from. - end_point: The point to look for highlights to. - """ - if not TREE_SITTER: - return None - - assert self._syntax_tree is not None - - highlights = self._highlights - highlights.clear() - - captures_kwargs = {} - if start_point is not None: - captures_kwargs["start_point"] = start_point - if end_point is not None: - captures_kwargs["end_point"] = end_point - - # We could optimise by only preparing highlights for a subset of lines here. - captures = self._query.captures(self._syntax_tree.root_node, **captures_kwargs) - - highlight_updates: dict[int, list[Highlight]] = defaultdict(list) - for capture in captures: - node, highlight_name = capture - node_start_row, node_start_column = node.start_point - node_end_row, node_end_column = node.end_point - - if node_start_row == node_end_row: - highlight = (node_start_column, node_end_column, highlight_name) - highlight_updates[node_start_row].append(highlight) - else: - # Add the first line of the node range - highlight_updates[node_start_row].append( - (node_start_column, None, highlight_name) - ) - - # Add the middle lines - entire row of this node is highlighted - for node_row in range(node_start_row + 1, node_end_row): - highlight_updates[node_row].append((0, None, highlight_name)) - - # Add the last line of the node range - highlight_updates[node_end_row].append( - (0, node_end_column, highlight_name) - ) - - for line_index, updated_highlights in highlight_updates.items(): - highlights[line_index] = updated_highlights + # def _prepare_highlights( + # self, + # start_point: tuple[int, int] | None = None, + # end_point: tuple[int, int] | None = None, + # ) -> None: + # """Query the tree for ranges to highlights, and update the internal highlights mapping. + # + # Args: + # start_point: The point to start looking for highlights from. + # end_point: The point to look for highlights to. + # """ + # assert self._syntax_tree is not None + # + # highlights = self._highlights + # highlights.clear() + # + # captures_kwargs = {} + # if start_point is not None: + # captures_kwargs["start_point"] = start_point + # if end_point is not None: + # captures_kwargs["end_point"] = end_point + # + # # We could optimise by only preparing highlights for a subset of lines here. + # captures = self._query.captures(self._syntax_tree.root_node, **captures_kwargs) + # + # highlight_updates: dict[int, list[Highlight]] = defaultdict(list) + # for capture in captures: + # node, highlight_name = capture + # node_start_row, node_start_column = node.start_point + # node_end_row, node_end_column = node.end_point + # + # if node_start_row == node_end_row: + # highlight = (node_start_column, node_end_column, highlight_name) + # highlight_updates[node_start_row].append(highlight) + # else: + # # Add the first line of the node range + # highlight_updates[node_start_row].append( + # (node_start_column, None, highlight_name) + # ) + # + # # Add the middle lines - entire row of this node is highlighted + # for node_row in range(node_start_row + 1, node_end_row): + # highlight_updates[node_row].append((0, None, highlight_name)) + # + # # Add the last line of the node range + # highlight_updates[node_end_row].append( + # (0, node_end_column, highlight_name) + # ) + # + # for line_index, updated_highlights in highlight_updates.items(): + # highlights[line_index] = updated_highlights def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes: """A callable which informs tree-sitter about the document content. diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index db6727b75e..5f399fcb27 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -3,11 +3,12 @@ import re from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any, Iterable +from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, Optional from rich.style import Style from rich.text import Text +from textual._tree_sitter import TREE_SITTER from textual.expand_tabs import expand_tabs_inline if TYPE_CHECKING: @@ -39,6 +40,13 @@ _HIGHLIGHTS_PATH = _TREE_SITTER_PATH / "highlights/" +@dataclass +class TextAreaLanguage: + name: str + language: "Language" + highlight_query: str | "Query" + + class TextArea(ScrollView, can_focus=True): DEFAULT_CSS = """\ $text-area-active-line-bg: white 8%; @@ -187,15 +195,15 @@ class TextArea(ScrollView, can_focus=True): | f7 | Select all text in the document. | """ - language: Reactive[str | "Language" | None] = reactive(None, always_update=True) + language: Reactive[str | None] = reactive(None, always_update=True) """The language to use. This must be set to a valid, non-None value for syntax highlighting to work. - If the value is a string, a built-in parser will be used. + If the value is a string, a built-in language parser will be used if available. - If you wish to add support for an unsupported language, you'll have to pass in the - tree-sitter `Language` object directly rather than the string language name. + If you wish to use an unsupported language, you'll have to register + it first using `register_language`. """ theme: Reactive[str | TextAreaTheme] = reactive(TextAreaTheme.default()) @@ -232,7 +240,7 @@ class TextArea(ScrollView, can_focus=True): """ match_cursor_bracket: Reactive[bool] = reactive(True) - """If the cursor is at a bracket, highlight the matching bracket if found.""" + """If the cursor is at a bracket, highlight the matching bracket (if found).""" cursor_blink: Reactive[bool] = reactive(True) """True if the cursor should blink.""" @@ -245,7 +253,7 @@ def __init__( self, text: str = "", *, - language: str | "Language" | None = None, + language: str | None = None, theme: str | TextAreaTheme | None = TextAreaTheme.default(), name: str | None = None, id: str | None = None, @@ -262,29 +270,18 @@ def __init__( """ super().__init__(name=name, id=id, classes=classes, disabled=disabled) - self.document = SyntaxAwareDocument(text, language) + self.document = self._document_factory(text, language) """The document this widget is currently editing.""" + self._languages: dict[str, TextAreaLanguage] = {} + """Maps language names to their TextAreaLanguage metadata.""" + if isinstance(theme, str): theme = TextAreaTheme.get_theme(theme) self.theme = theme """The theme of the `TextArea`.""" - # TODO - the highlight query can only be adjusted using a method. - # we need to do this because of the dependency between highlight query and - # tree-sitter parser. - language_name = self.document.language.name - if highlight_query is None and language_name is not None: - # Try to retrieve the default highlight query for the language. - highlight_query_path = ( - Path(_HIGHLIGHTS_PATH.resolve()) / f"{language_name}.scm" - ) - highlight_query = highlight_query_path.read_text() - - self._highlight_query = self.document.prepare_query(highlight_query) - """The query to use for syntax highlighting, or `None` if not available.""" - self.indent_type: Literal["tabs", "spaces"] = "spaces" """Whether to indent using tabs or spaces.""" @@ -307,11 +304,16 @@ def __init__( cursor is currently at. If the cursor is at a bracket, or there's no matching bracket, this will be `None`.""" - def _configure_syntax_highlighting(self) -> None: - """Configure syntax highlighting based on the theme and highlighting query.""" - # When the language is modified, we should reset the highlighting query. - # When the highlighting query is modified, we should reset the language. - # If you want to modify both together, what do you do? + def _get_builtin_highlight_query(self, language_name: str) -> str: + try: + highlight_query_path = ( + Path(_HIGHLIGHTS_PATH.resolve()) / f"{language_name}.scm" + ) + highlight_query = highlight_query_path.read_text() + except OSError: + highlight_query = "" + + return highlight_query def _watch_selection(self, selection: Selection) -> None: """When the cursor moves, scroll it into view.""" @@ -371,17 +373,9 @@ def _validate_selection(self, selection: Selection) -> Selection: clamp_visitable = self.clamp_visitable return Selection(clamp_visitable(start), clamp_visitable(end)) - def _watch_language(self) -> None: + def _watch_language(self, language: str | None) -> None: """When the language is updated, update the type of document.""" - self._set_document(self.text, self.language) - - def _watch_theme(self) -> None: - """When the theme is updated, update the document.""" - self._set_document(self.text, self.language) - - def _watch_highlight_query(self) -> None: - """When the highlight query is updated, refresh the document.""" - self._set_document(self.text, self.language) + self.document = self._document_factory(self.text, language) def _watch_show_line_numbers(self) -> None: """The line number gutter contributes to virtual size, so recalculate.""" @@ -391,23 +385,89 @@ def _watch_indent_width(self) -> None: """Changing width of tabs will change document display width.""" self._refresh_size() - def _set_document(self, text: str, language: str | None) -> DocumentBase: - """Recreate the document based on the language and theme currently set.""" - if not language: - # If there's no language set, we don't need to use a SyntaxAwareDocument. - self.document = Document(text) + def register_language( + self, + language: str | "Language", + highlight_query: str, + ) -> None: + """Register a language and corresponding highlight query. + + Calling this method does not change the language of the `TextArea`. + On switching to this language (via the `language` reactive attribute), + syntax highlighting will be performed using the given highlight query. + + If a string `name` is supplied for a builtin supported language, then + this method will update the default highlight query for that language. + + Registering a language only registers it to this instance of `TextArea`. + + Args: + language: A string referring to a builtin language or a tree-sitter `Language` object. + highlight_query: The highlight query to use for syntax highlighting this language. + """ + + # If tree-sitter is unavailable, do nothing. + if not TREE_SITTER: + return + + from tree_sitter_languages import get_language + + if isinstance(language, str): + language_name = language + language = get_language(language_name) else: - try: - from textual.document._syntax_aware_document import SyntaxAwareDocument + language_name = language.name + + # Update the custom languages. When changing the document, + # we should first look in here for a language specification. + # If nothing is found, then we can go to the builtin languages. + self._languages[language_name] = TextAreaLanguage( + name=language_name, + language=language, + highlight_query=highlight_query, + ) + + # def _set_document(self, text: str, language: str | None) -> DocumentBase: + # if language in self._languages: + # # Load the custom language if it exists + # language_spec = self._languages[language] + # document_language = language_spec.language + # highlight_query = language_spec.highlight_query + # else: + # document_language = language + # highlight_query = self._get_builtin_highlight_query(language) + # + # # Update the document and prepare the new query. + # self.document = SyntaxAwareDocument(text, document_language) + # if highlight_query: + # self._highlight_query = self.document.prepare_query(highlight_query) + # return self.document + + def _document_factory(self, text: str, language: str | None) -> DocumentBase: + """Construct and return an appropriate document.""" + if TREE_SITTER and language: + text_area_language = self._languages.get(language, None) + if text_area_language: + document_language = text_area_language.language + highlight_query = text_area_language.highlight_query + else: + document_language = language + highlight_query = "" - self.document = SyntaxAwareDocument(text, language) + try: + document = SyntaxAwareDocument(text, document_language) except RuntimeError: - # SyntaxAwareDocument isn't available on Python 3.7. - # Fall back to the standard Document. - log.warning("Syntax highlighting isn't available on Python 3.7.") - self.document = Document(text) + document = Document(text) + else: + self._highlight_query = document.prepare_query(highlight_query) + + elif language and not TREE_SITTER: + log.warning("Syntax highlighting not available on this architecture.") + document = Document(text) + else: + document = Document(text) - return self.document + return document @property def _visible_line_indices(self) -> tuple[int, int]: @@ -424,7 +484,7 @@ def load_text(self, text: str) -> None: Args: text: The text to load into the TextArea. """ - self._set_document(text, self.language) + self.document = self._document_factory(text, self.language) self.move_cursor((0, 0)) self._refresh_size() From f1a17ed796a6d698cebfd066be3fe189d05e7f6f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 4 Sep 2023 14:59:33 +0100 Subject: [PATCH 296/366] TextAreaTheme styling --- .../document/_syntax_aware_document.py | 28 ++- src/textual/document/_text_area_theme.py | 200 +++++++++--------- src/textual/widgets/_text_area.py | 97 +++++---- 3 files changed, 174 insertions(+), 151 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index d1fb3d792f..9818968468 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -87,7 +87,6 @@ def __init__( self._parser.set_language(language) self._syntax_tree = self._parser.parse(self._read_callable) # type: ignore - self._prepare_highlights() @property def language_name(self) -> str | None: @@ -151,7 +150,6 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult self._syntax_tree = self._parser.parse( self._read_callable, self._syntax_tree # type: ignore[arg-type] ) - self._prepare_highlights() return replace_result @@ -167,19 +165,19 @@ def get_line_text(self, line_index: int) -> Text: line_string = self[line_index] line = Text(line_string, end="") - highlights = self._highlights - if highlights: - line_bytes = _utf8_encode(line_string) - byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) - get_highlight_from_theme = self._syntax_theme.get_highlight - line_highlights = highlights[line_index] - for start, end, highlight_name in line_highlights: - node_style = get_highlight_from_theme(highlight_name) - line.stylize( - node_style, - byte_to_codepoint.get(start, 0), - byte_to_codepoint.get(end) if end else None, - ) + # highlights = self._highlights + # if highlights: + # line_bytes = _utf8_encode(line_string) + # byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) + # get_highlight_from_theme = self._syntax_theme.get_highlight + # line_highlights = highlights[line_index] + # for start, end, highlight_name in line_highlights: + # node_style = get_highlight_from_theme(highlight_name) + # line.stylize( + # node_style, + # byte_to_codepoint.get(start, 0), + # byte_to_codepoint.get(end) if end else None, + # ) return line def _location_to_byte_offset(self, location: Location) -> int: diff --git a/src/textual/document/_text_area_theme.py b/src/textual/document/_text_area_theme.py index 9155b98307..1ec39a9e13 100644 --- a/src/textual/document/_text_area_theme.py +++ b/src/textual/document/_text_area_theme.py @@ -4,7 +4,9 @@ from rich.style import Style +from textual.app import DEFAULT_COLORS from textual.color import Color +from textual.design import DEFAULT_DARK_SURFACE @dataclass @@ -30,33 +32,69 @@ class TextAreaTheme: name: str | None = None """The name of the theme.""" - token_styles: dict[str, TextAreaStyle] = field(default_factory=dict) - """The mapping of tree-sitter names from the `highlight_query` to Rich styles.""" - - base_style: TextAreaStyle | None = None + base_style: Style | None = None """The background style of the text area. If `None` the parent style will be used.""" - gutter_style: TextAreaStyle | None = None + gutter_style: Style | None = None """The style of the gutter. If `None`, a legible TextAreaStyle will be generated.""" - cursor_style: TextAreaStyle | None = None + cursor_style: Style | None = None """The style of the cursor. If `None`, the legible TextAreaStyle will be generated.""" - cursor_line_style: TextAreaStyle | None = None - """The style to apply to the line the cursor is on. If `None`, a legible TextAreaStyle will be generated.""" + cursor_line_style: Style | None = None + """The style to apply to the line the cursor is on.""" - cursor_line_gutter_style: TextAreaStyle | None = None - """The style to apply to the gutter of the line the cursor is on. If `None`, a legible TextAreaStyle will be + cursor_line_gutter_style: Style | None = None + """The style to apply to the gutter of the line the cursor is on. If `None`, a legible Style will be generated.""" - bracket_matching_style: TextAreaStyle | None = None - """The style to apply to matching brackets. If `None`, a legible TextAreaStyle will be generated.""" + bracket_matching_style: Style | None = None + """The style to apply to matching brackets. If `None`, a legible Style will be generated.""" + + selection_style: Style | None = None + """The style of the selection. If `None` a default selection Style will be generated.""" + + token_styles: dict[str, Style] = field(default_factory=dict) + """The mapping of tree-sitter names from the `highlight_query` to Rich styles.""" - selection_style: TextAreaStyle | None = None - """The style of the selection. If `None` a default selection TextAreaStyle will be generated.""" + def __post_init__(self) -> None: + """Generate some styles if they haven't been supplied.""" + if self.base_style is None: + self.base_style = Style(color="#f3f3f3", bgcolor=DEFAULT_DARK_SURFACE) + + if self.gutter_style is None: + self.gutter_style = self.base_style.copy() + + background_color = Color.from_rich_color( + self.base_style.background_style.bgcolor + ) + if self.cursor_style is None: + self.cursor_style = Style( + color=background_color.rich_color, + bgcolor=background_color.inverse.rich_color, + ) + + if self.cursor_line_gutter_style is None and self.cursor_line_style is not None: + self.cursor_line_gutter_style = self.cursor_line_style.copy() + + if self.bracket_matching_style is None: + bracket_matching_background = background_color.blend( + background_color.inverse, factor=0.05 + ) + self.bracket_matching_style = Style( + bgcolor=bracket_matching_background.rich_color + ) + + if self.selection_style is None: + selection_background_color = background_color.blend( + DEFAULT_COLORS["dark"].primary, factor=0.75 + ) + self.selection_style = Style.from_color( + bgcolor=selection_background_color.rich_color + ) @classmethod - def get_theme(cls, theme_name: str) -> "TextAreaTheme": + def get_by_name(cls, theme_name: str) -> "TextAreaTheme": """Get a `SyntaxTheme` by name. Given a `theme_name` return the corresponding `SyntaxTheme` object. @@ -69,9 +107,9 @@ def get_theme(cls, theme_name: str) -> "TextAreaTheme": Returns: The `SyntaxTheme` corresponding to the name. """ - return cls(theme_name, _BUILTIN_THEMES.get(theme_name, {})) + return _BUILTIN_THEMES.get(theme_name, TextAreaTheme()) - def get_highlight(self, name: str) -> TextAreaTheme: + def get_highlight(self, name: str) -> Style: """Return the Rich style corresponding to the name defined in the tree-sitter highlight query for the current theme. @@ -90,9 +128,7 @@ def available_themes(cls) -> list[TextAreaTheme]: Returns: A list of all available SyntaxThemes. """ - return [ - TextAreaTheme(name, mapping) for name, mapping in _BUILTIN_THEMES.items() - ] + return list(_BUILTIN_THEMES.values()) @classmethod def default(cls) -> TextAreaTheme: @@ -104,85 +140,59 @@ def default(cls) -> TextAreaTheme: return DEFAULT_SYNTAX_THEME -@dataclass -class TextAreaStyle: - foreground_color: str | Color = None - background_color: str | Color = None - bold: bool = False - italic: bool = False - strikethrough: bool = False - underline: bool = False - - def __post_init__(self) -> None: - self.background_color = Color.parse(self.background_color) - self.foreground_color = Color.parse(self.foreground_color).blend( - self.background_color, factor=1 - ) - - # The default for tree-sitter tokens which aren't mapped to styles. - self.default_style = Style( - color=self.foreground_color.rich_color, - bgcolor=self.background_color.rich_color, - bold=self.bold, - italic=self.italic, - strike=self.strikethrough, - underline=self.underline, - ) - - _MONOKAI = TextAreaTheme( name="monokai", - base_style=TextAreaStyle("#f8f8f2", "#272822"), - gutter_style=TextAreaStyle("#90908a", "#272822"), - cursor_style=TextAreaStyle("#f8f8f0"), - cursor_line_style=TextAreaStyle(background_color="#3e3d32"), - cursor_line_gutter_style=TextAreaStyle("#c2c2bf", "#3e3d32"), - bracket_matching_style=TextAreaStyle("#414339"), - selection_style=TextAreaStyle(background_color="#878b9180"), + base_style=Style(color="#f8f8f2", bgcolor="#272822"), + gutter_style=Style(color="#90908a", bgcolor="#272822"), + cursor_style=Style(color="#272822", bgcolor="#f8f8f0"), + cursor_line_style=Style(bgcolor="#3e3d32"), + cursor_line_gutter_style=Style(color="#c2c2bf", bgcolor="#3e3d32"), + bracket_matching_style=Style(bold=True, underline=True), + selection_style=Style(bgcolor="#65686a"), token_styles={ - "string": TextAreaStyle("#E6DB74"), - "string.documentation": TextAreaStyle("#E6DB74"), - "comment": TextAreaStyle("#75715E"), - "keyword": TextAreaStyle("#F92672"), - "operator": TextAreaStyle("#F92672"), - "repeat": TextAreaStyle("#F92672"), - "exception": TextAreaStyle("#F92672"), - "include": TextAreaStyle("#F92672"), - "keyword.function": TextAreaStyle("#F92672"), - "keyword.return": TextAreaStyle("#F92672"), - "keyword.operator": TextAreaStyle("#F92672"), - "conditional": TextAreaStyle("#F92672"), - "number": TextAreaStyle("#AE81FF"), - "float": TextAreaStyle("#AE81FF"), - "class": TextAreaStyle("#A6E22E"), - "function": TextAreaStyle("#A6E22E"), - "function.call": TextAreaStyle("#A6E22E"), - "method": TextAreaStyle("#A6E22E"), - "method.call": TextAreaStyle("#A6E22E"), - "boolean": TextAreaStyle("#66D9EF", italic=True), - "json.null": TextAreaStyle("#66D9EF", italic=True), - # "constant": TextAreaStyle("#AE81FF"), - # "variable": TextAreaStyle("white"), - # "parameter": TextAreaStyle("cyan"), - # "type": TextAreaStyle("cyan"), - # "escape": TextAreaStyle("magenta"), - # "error": TextAreaStyle("TextAreaStyle", "red"), - "regex.punctuation.bracket": TextAreaStyle("#F92672"), - "regex.operator": TextAreaStyle("#F92672"), + "string": Style(color="#E6DB74"), + "string.documentation": Style(color="#E6DB74"), + "comment": Style(color="#75715E"), + "keyword": Style(color="#F92672"), + "operator": Style(color="#F92672"), + "repeat": Style(color="#F92672"), + "exception": Style(color="#F92672"), + "include": Style(color="#F92672"), + "keyword.function": Style(color="#F92672"), + "keyword.return": Style(color="#F92672"), + "keyword.operator": Style(color="#F92672"), + "conditional": Style(color="#F92672"), + "number": Style(color="#AE81FF"), + "float": Style(color="#AE81FF"), + "class": Style(color="#A6E22E"), + "function": Style(color="#A6E22E"), + "function.call": Style(color="#A6E22E"), + "method": Style(color="#A6E22E"), + "method.call": Style(color="#A6E22E"), + "boolean": Style(color="#66D9EF", italic=True), + "json.null": Style(color="#66D9EF", italic=True), + # "constant": Style(color="#AE81FF"), + # "variable": Style(color="white"), + # "parameter": Style(color="cyan"), + # "type": Style(color="cyan"), + # "escape": Style("magenta"), + # "error": Style(color="black", bgcolor="red"), + "regex.punctuation.bracket": Style(color="#F92672"), + "regex.operator": Style(color="#F92672"), # "json.error": _NULL_STYLE, - "html.end_tag_error": TextAreaStyle("red", underline=True), - "tag": TextAreaStyle("#F92672"), - "yaml.field": TextAreaStyle("#F92672", bold=True), - "json.label": TextAreaStyle("#F92672", bold=True), - "toml.type": TextAreaStyle("#F92672"), - "toml.datetime": TextAreaStyle("#AE81FF"), + "html.end_tag_error": Style(color="red", underline=True), + "tag": Style(color="#F92672"), + "yaml.field": Style(color="#F92672", bold=True), + "json.label": Style(color="#F92672", bold=True), + "toml.type": Style(color="#F92672"), + "toml.datetime": Style(color="#AE81FF"), # "toml.error": _NULL_STYLE, - "heading": TextAreaStyle("#F92672", bold=True), - "bold": TextAreaStyle(bold=True), - "italic": TextAreaStyle(italic=True), - "strikethrough": TextAreaStyle(strikethrough=True), - "link": TextAreaStyle("#66D9EF", underline=True), - "inline_code": TextAreaStyle("#F92672"), + "heading": Style(color="#F92672", bold=True), + "bold": Style(bold=True), + "italic": Style(italic=True), + "strikethrough": Style(strike=True), + "link": Style(color="#66D9EF", underline=True), + "inline_code": Style(color="#F92672"), }, ) @@ -238,5 +248,5 @@ def __post_init__(self) -> None: } -DEFAULT_SYNTAX_THEME = TextAreaTheme.get_theme("monokai") +DEFAULT_SYNTAX_THEME = TextAreaTheme.get_by_name("monokai") """The default syntax highlighting theme used by Textual.""" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 5f399fcb27..9c2e204ba2 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -206,7 +206,9 @@ class TextArea(ScrollView, can_focus=True): it first using `register_language`. """ - theme: Reactive[str | TextAreaTheme] = reactive(TextAreaTheme.default()) + theme: Reactive[str | TextAreaTheme] = reactive( + TextAreaTheme.default(), always_update=True, init=True + ) """The theme to syntax highlight with. Supply a `SyntaxTheme` object to customise highlighting, or supply a builtin @@ -277,9 +279,9 @@ def __init__( """Maps language names to their TextAreaLanguage metadata.""" if isinstance(theme, str): - theme = TextAreaTheme.get_theme(theme) + theme = TextAreaTheme.get_by_name(theme) - self.theme = theme + self.theme: TextAreaTheme = theme """The theme of the `TextArea`.""" self.indent_type: Literal["tabs", "spaces"] = "spaces" @@ -385,6 +387,11 @@ def _watch_indent_width(self) -> None: """Changing width of tabs will change document display width.""" self._refresh_size() + def _validate_theme(self, theme: str | TextAreaTheme) -> TextAreaTheme: + if isinstance(theme, str): + theme = TextAreaTheme.get_by_name(theme) + return theme + def register_language( self, language: str | "Language", @@ -446,6 +453,7 @@ def register_language( def _document_factory(self, text: str, language: str | None) -> DocumentBase: """Construct and return an appropriate document.""" if TREE_SITTER and language: + # Attempt to get the override language. text_area_language = self._languages.get(language, None) if text_area_language: document_language = text_area_language.language @@ -570,6 +578,9 @@ def render_line(self, widget_y: int) -> Strip: if out_of_bounds: return Strip.blank(self.size.width) + theme = self.theme + base_style = theme.base_style + # Get the (possibly highlighted) line from the Document. line = document.get_line_text(line_index) line_character_count = len(line) @@ -582,10 +593,19 @@ def render_line(self, widget_y: int) -> Strip: selection_top_row, selection_top_column = selection_top selection_bottom_row, selection_bottom_column = selection_bottom + matching_bracket = self._matching_bracket_location + match_cursor_bracket = self.match_cursor_bracket + draw_matched_brackets = match_cursor_bracket and matching_bracket is not None + + cursor_row, cursor_column = end + cursor_line_style = theme.cursor_line_style + if cursor_row == line_index: + line.stylize(cursor_line_style) + # Selection styling if start != end and selection_top_row <= line_index <= selection_bottom_row: # If this row intersects with the selection range - selection_style = self.get_component_rich_style("text-area--selection") + selection_style = theme.selection_style cursor_row, _ = end if line_character_count == 0 and line_index != cursor_row: # A simple highlight to show empty lines are included in the selection @@ -593,7 +613,7 @@ def render_line(self, widget_y: int) -> Strip: else: if line_index == selection_top_row == selection_bottom_row: # Selection within a single line - line.stylize_before( + line.stylize( selection_style, start=selection_top_column, end=selection_bottom_column, @@ -601,66 +621,63 @@ def render_line(self, widget_y: int) -> Strip: else: # Selection spanning multiple lines if line_index == selection_top_row: - line.stylize_before( + line.stylize( selection_style, start=selection_top_column, end=line_character_count, ) elif line_index == selection_bottom_row: - line.stylize_before( - selection_style, end=selection_bottom_column - ) + line.stylize(selection_style, end=selection_bottom_column) else: - line.stylize_before(selection_style, end=line_character_count) + line.stylize(selection_style, end=line_character_count) virtual_width, virtual_height = self.virtual_size - # Highlight the partner opening/closing bracket. - matching_bracket = self._matching_bracket_location - match_cursor_bracket = self.match_cursor_bracket - draw_matched_brackets = match_cursor_bracket and matching_bracket is not None - if draw_matched_brackets: - bracket_match_row, bracket_match_column = matching_bracket - if bracket_match_row == line_index: - matching_bracket_style = self.get_component_rich_style( - "text-area--matching-bracket" - ) - line.stylize( - matching_bracket_style, - bracket_match_column, - bracket_match_column + 1, - ) - # Highlight the cursor - cursor_row, cursor_column = end - active_line_style = self.get_component_rich_style("text-area--cursor-line") + # active_line_style = self.get_component_rich_style("text-area--cursor-line") if cursor_row == line_index: draw_cursor = not self.cursor_blink or ( self.cursor_blink and self._cursor_blink_visible ) if draw_matched_brackets: - matching_bracket_style = self.get_component_rich_style( - "text-area--matching-bracket" - ) + matching_bracket_style = theme.bracket_matching_style line.stylize( matching_bracket_style, cursor_column, cursor_column + 1, ) + if draw_cursor: - cursor_style = self.get_component_rich_style("text-area--cursor") + # cursor_style = self.get_component_rich_style("text-area--cursor") + cursor_style = theme.cursor_style line.stylize(cursor_style, cursor_column, cursor_column + 1) - line.stylize_before(active_line_style) + + # Highlight the partner opening/closing bracket. + if draw_matched_brackets: + bracket_match_row, bracket_match_column = matching_bracket + if bracket_match_row == line_index: + # matching_bracket_style = self.get_component_rich_style( + # "text-area--matching-bracket" + # ) + matching_bracket_style = theme.bracket_matching_style + if matching_bracket_style: + line.stylize( + matching_bracket_style, + bracket_match_column, + bracket_match_column + 1, + ) # Build the gutter text for this line gutter_width = self.gutter_width if self.show_line_numbers: if cursor_row == line_index: - gutter_style = self.get_component_rich_style( - "text-area--cursor-line-gutter" - ) + gutter_style = theme.cursor_line_gutter_style + # gutter_style = self.get_component_rich_style( + # "text-area--cursor-line-gutter" + # ) else: - gutter_style = self.get_component_rich_style("text-area--gutter") + # gutter_style = self.get_component_rich_style("text-area--gutter") + gutter_style = theme.gutter_style gutter_width_no_margin = gutter_width - 2 gutter = Text( @@ -687,13 +704,11 @@ def render_line(self, widget_y: int) -> Strip: # Stylize the line the cursor is currently on. if cursor_row == line_index: expanded_length = max(virtual_width, self.size.width) - text_strip = text_strip.extend_cell_length( - expanded_length, active_line_style - ) + text_strip = text_strip.extend_cell_length(expanded_length) # Join and return the gutter and the visible portion of this line strip = Strip.join([gutter_strip, text_strip]).simplify() - return strip.apply_style(self.rich_style) + return strip.apply_style(theme.base_style) @property def text(self) -> str: From e809470e75b8aeaaaf8c405f8da833e7f061a640 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 4 Sep 2023 15:05:26 +0100 Subject: [PATCH 297/366] Setting width of blank selected lines --- src/textual/widgets/_text_area.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 9c2e204ba2..c1839aadee 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -610,6 +610,7 @@ def render_line(self, widget_y: int) -> Strip: if line_character_count == 0 and line_index != cursor_row: # A simple highlight to show empty lines are included in the selection line = Text("▌", end="", style=Style(color=selection_style.bgcolor)) + line.set_length(self.virtual_size.width) else: if line_index == selection_top_row == selection_bottom_row: # Selection within a single line From b4da118efbe46f8af99f5940eaab7881a5e3cd6b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 4 Sep 2023 16:40:15 +0100 Subject: [PATCH 298/366] Building the highlight map in the text area --- src/textual/document/__init__.py | 12 +---- src/textual/document/_document.py | 17 +++++- .../document/_syntax_aware_document.py | 12 +---- src/textual/widgets/_text_area.py | 52 +++++++++++++++++-- src/textual/widgets/text_area.py | 12 ++++- 5 files changed, 77 insertions(+), 28 deletions(-) diff --git a/src/textual/document/__init__.py b/src/textual/document/__init__.py index 540c94079d..14dc915756 100644 --- a/src/textual/document/__init__.py +++ b/src/textual/document/__init__.py @@ -1,24 +1,14 @@ from ._document import Document, DocumentBase, EditResult, Location, Selection from ._languages import VALID_LANGUAGES -from ._syntax_aware_document import ( - EndColumn, - Highlight, - HighlightName, - StartColumn, - SyntaxAwareDocument, -) +from ._syntax_aware_document import SyntaxAwareDocument from ._text_area_theme import DEFAULT_SYNTAX_THEME, TextAreaTheme __all__ = [ "Document", "DocumentBase", - "EndColumn", - "Highlight", - "HighlightName", "Location", "EditResult", "Selection", - "StartColumn", "SyntaxAwareDocument", "TextAreaTheme", "VALID_LANGUAGES", diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index c442652ac8..c6d87cf88b 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -3,10 +3,13 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from functools import lru_cache -from typing import NamedTuple, Tuple, overload +from typing import TYPE_CHECKING, Any, NamedTuple, Tuple, overload from rich.text import Text +if TYPE_CHECKING: + from tree_sitter.binding import Query + from textual._cells import cell_len from textual._types import Literal, get_args from textual.geometry import Size @@ -124,6 +127,18 @@ def get_size(self, indent_width: int) -> Size: The Size of the document bounding box. """ + def query_syntax_tree( + self, + query: "Query", + start_point: tuple[int, int] | None = None, + end_point: tuple[int, int] | None = None, + ) -> Any: + """Query the tree-sitter syntax tree.""" + return [] + + def prepare_query(self, query: str) -> "Query" | None: + return None + @property @abstractmethod def line_count(self) -> int: diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 9818968468..769893a87f 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -1,8 +1,7 @@ from __future__ import annotations -from collections import defaultdict from functools import lru_cache -from typing import Any, Optional, Tuple +from typing import Any from rich.text import Text from typing_extensions import TYPE_CHECKING @@ -22,12 +21,6 @@ from textual.document._languages import VALID_LANGUAGES from textual.document._text_area_theme import TextAreaTheme -StartColumn = int -EndColumn = Optional[int] -HighlightName = str -Highlight = Tuple[StartColumn, EndColumn, HighlightName] -"""A tuple representing a syntax highlight within one line.""" - class SyntaxAwareDocument(Document): """A wrapper around a Document which also maintains a tree-sitter syntax @@ -72,9 +65,6 @@ def __init__( self._syntax_theme: TextAreaTheme | None = None """The syntax highlighting theme to use.""" - self._highlights: dict[int, list[Highlight]] = defaultdict(list) - """Mapping line numbers to the set of highlights for that line.""" - # If the language is `None`, then avoid doing any parsing related stuff. if isinstance(language, str): if language not in VALID_LANGUAGES: diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index c1839aadee..209d47d04a 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1,9 +1,10 @@ from __future__ import annotations import re +from collections import defaultdict from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, Optional +from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple from rich.style import Style from rich.text import Text @@ -40,6 +41,13 @@ _HIGHLIGHTS_PATH = _TREE_SITTER_PATH / "highlights/" +StartColumn = int +EndColumn = Optional[int] +HighlightName = str +Highlight = Tuple[StartColumn, EndColumn, HighlightName] +"""A tuple representing a syntax highlight within one line.""" + + @dataclass class TextAreaLanguage: name: str @@ -177,7 +185,7 @@ class TextArea(ScrollView, can_focus=True): | home,ctrl+a | Move the cursor to the start of the line. | | end,ctrl+e | Move the cursor to the end of the line. | | shift+home | Move the cursor to the start of the line and select. | - | shift+end | Move the cursor to the end of the line and select. | + | shift+end | Move the cursor to the end of the line and select. | | pageup | Move the cursor one page up. | | pagedown | Move the cursor one page down. | | shift+up | Select while moving the cursor up. | @@ -306,6 +314,12 @@ def __init__( cursor is currently at. If the cursor is at a bracket, or there's no matching bracket, this will be `None`.""" + self._highlights: dict[int, list[Highlight]] = defaultdict(list) + """Mapping line numbers to the set of highlights for that line.""" + + self._highlight_query: "Query" | None = None + """The query that's currently being used for highlighting.""" + def _get_builtin_highlight_query(self, language_name: str) -> str: try: highlight_query_path = ( @@ -317,6 +331,36 @@ def _get_builtin_highlight_query(self, language_name: str) -> str: return highlight_query + def _build_highlight_map(self) -> None: + """Query the tree for ranges to highlights, and update the internal highlights mapping.""" + + highlights = self._highlights + highlights.clear() + if not self._highlight_query: + return + + captures = self.document.query_syntax_tree(self._highlight_query) + for capture in captures: + node, highlight_name = capture + node_start_row, node_start_column = node.start_point + node_end_row, node_end_column = node.end_point + + if node_start_row == node_end_row: + highlight = (node_start_column, node_end_column, highlight_name) + highlights[node_start_row].append(highlight) + else: + # Add the first line of the node range + highlights[node_start_row].append( + (node_start_column, None, highlight_name) + ) + + # Add the middle lines - entire row of this node is highlighted + for node_row in range(node_start_row + 1, node_end_row): + highlights[node_row].append((0, None, highlight_name)) + + # Add the last line of the node range + highlights[node_end_row].append((0, node_end_column, highlight_name)) + def _watch_selection(self, selection: Selection) -> None: """When the cursor moves, scroll it into view.""" self.scroll_cursor_visible() @@ -460,7 +504,7 @@ def _document_factory(self, text: str, language: str | None) -> DocumentBase: highlight_query = text_area_language.highlight_query else: document_language = language - highlight_query = "" + highlight_query = self._get_builtin_highlight_query(language) try: document = SyntaxAwareDocument(text, document_language) @@ -468,7 +512,6 @@ def _document_factory(self, text: str, language: str | None) -> DocumentBase: document = Document(text) else: self._highlight_query = document.prepare_query(highlight_query) - elif language and not TREE_SITTER: log.warning("Syntax highlighting not available on this architecture.") document = Document(text) @@ -748,6 +791,7 @@ def edit(self, edit: Edit) -> Any: result = edit.do(self) self._refresh_size() edit.after(self) + self._build_highlight_map() return result async def _on_key(self, event: events.Key) -> None: diff --git a/src/textual/widgets/text_area.py b/src/textual/widgets/text_area.py index b67dc051c9..a157ff7ec3 100644 --- a/src/textual/widgets/text_area.py +++ b/src/textual/widgets/text_area.py @@ -1,5 +1,15 @@ -from textual.widgets._text_area import Edit +from textual.widgets._text_area import ( + Edit, + EndColumn, + Highlight, + HighlightName, + StartColumn, +) __all__ = [ "Edit", + "EndColumn", + "Highlight", + "HighlightName", + "StartColumn", ] From 8a59506a9458d4e601df3cc03b415162aa26f4c3 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 4 Sep 2023 16:41:09 +0100 Subject: [PATCH 299/366] Remove unused default css from TextArea --- src/textual/widgets/_text_area.py | 49 ------------------------------- 1 file changed, 49 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 209d47d04a..955ae5e3b2 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -57,59 +57,10 @@ class TextAreaLanguage: class TextArea(ScrollView, can_focus=True): DEFAULT_CSS = """\ -$text-area-active-line-bg: white 8%; - TextArea { - background: $panel 70%; width: 1fr; height: 1fr; } -TextArea:focus { - background: $panel; -} -TextArea:focus > .text-area--cursor-line { - background: $text-area-active-line-bg; -} -TextArea > .text-area--cursor-line { - background: white 5%; -} -TextArea:focus > .text-area--cursor-line-gutter { - color: $text; - background: $text-area-active-line-bg; -} -TextArea > .text-area--cursor-line-gutter { - color: $text 65%; - background: $text-area-active-line-bg; -} -TextArea:focus > .text-area--gutter { - color: $text-muted 45%; -} -TextArea > .text-area--gutter { - color: $text-muted 35%; -} -TextArea:focus > .text-area--cursor { - color: black 90%; - background: white 80%; -} -TextArea > .text-area--cursor { - color: black 90%; - background: white 25%; -} -TextArea:focus > .text-area--selection { - background: $primary; -} -TextArea > .text-area--selection { - background: $primary 65%; -} -TextArea:focus > .text-area--matching-bracket { - color: $text; - background: white 20%; - text-style: bold underline; -} -TextArea > .text-area--matching-bracket { - background: ; - text-style: ; -} """ BINDINGS = [ From 11376c98b5c3500ea31827eaa3e44fde5d34ad91 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 4 Sep 2023 16:58:45 +0100 Subject: [PATCH 300/366] Moving highlighting stylize into widget --- src/textual/document/_document.py | 5 ++--- .../document/_syntax_aware_document.py | 6 +++--- src/textual/widgets/_text_area.py | 20 ++++++++++++++++--- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index c6d87cf88b..527f5cd76d 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -87,7 +87,7 @@ def text(self) -> str: """The text from the document as a string.""" @abstractmethod - def get_line_text(self, index: int) -> Text: + def get_line_text(self, index: int) -> str: """Returns the line with the given index from the document. This is used in rendering lines, and will be called by the @@ -97,8 +97,7 @@ def get_line_text(self, index: int) -> Text: index: The index of the line in the document. Returns: - The Text instance representing the line. When overriding - this method, ensure the returned Text instance has `end=""`. + The str instance representing the line. """ @abstractmethod diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 769893a87f..604a3f1456 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -143,14 +143,14 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult return replace_result - def get_line_text(self, line_index: int) -> Text: - """Apply syntax highlights and return the Text of the line. + def get_line_text(self, line_index: int) -> str: + """Return the string representing the line, not including new line characters. Args: line_index: The index of the line. Returns: - The syntax highlighted Text of the line. + The string representing the line. """ line_string = self[line_index] line = Text(line_string, end="") diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 955ae5e3b2..aa8183cd9e 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -10,6 +10,7 @@ from rich.text import Text from textual._tree_sitter import TREE_SITTER +from textual.document._document import _utf8_encode from textual.expand_tabs import expand_tabs_inline if TYPE_CHECKING: @@ -573,10 +574,23 @@ def render_line(self, widget_y: int) -> Strip: return Strip.blank(self.size.width) theme = self.theme - base_style = theme.base_style - # Get the (possibly highlighted) line from the Document. - line = document.get_line_text(line_index) + # Get the line from the Document. + line_string = document.get_line_text(line_index) + + if self._highlights: + line_bytes = _utf8_encode(line_string) + byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) + get_highlight_from_theme = self._syntax_theme.get_highlight + line_highlights = highlights[line_index] + for start, end, highlight_name in line_highlights: + node_style = get_highlight_from_theme(highlight_name) + line.stylize( + node_style, + byte_to_codepoint.get(start, 0), + byte_to_codepoint.get(end) if end else None, + ) + line_character_count = len(line) line.tab_size = self.indent_width line.set_length(self.virtual_size.width) From 4c931ec1a21f8a64856c8f8a914ca6d2559e120d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 5 Sep 2023 12:50:54 +0100 Subject: [PATCH 301/366] Moving syntax highlighting into TextArea widget --- src/textual/document/_document.py | 7 +-- .../document/_syntax_aware_document.py | 45 +------------- src/textual/widgets/_text_area.py | 61 ++++++++++++++++--- 3 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 527f5cd76d..f63fd707e8 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -87,7 +87,7 @@ def text(self) -> str: """The text from the document as a string.""" @abstractmethod - def get_line_text(self, index: int) -> str: + def get_line(self, index: int) -> str: """Returns the line with the given index from the document. This is used in rendering lines, and will be called by the @@ -303,7 +303,7 @@ def line_count(self) -> int: """Returns the number of lines in the document.""" return len(self._lines) - def get_line_text(self, index: int) -> Text: + def get_line(self, index: int) -> Text: """Returns the line with the given index from the document. Args: @@ -314,8 +314,7 @@ def get_line_text(self, index: int) -> Text: this method, ensure the returned Text instance has `end=""`. """ line_string = self[index] - line_string = line_string.replace("\n", "").replace("\r", "") - return Text(line_string, end="") + return line_string @overload def __getitem__(self, line_index: int) -> str: diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 604a3f1456..fa1dc85204 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -143,7 +143,7 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult return replace_result - def get_line_text(self, line_index: int) -> str: + def get_line(self, line_index: int) -> str: """Return the string representing the line, not including new line characters. Args: @@ -153,7 +153,7 @@ def get_line_text(self, line_index: int) -> str: The string representing the line. """ line_string = self[line_index] - line = Text(line_string, end="") + return line_string # highlights = self._highlights # if highlights: @@ -168,7 +168,6 @@ def get_line_text(self, line_index: int) -> str: # byte_to_codepoint.get(start, 0), # byte_to_codepoint.get(end) if end else None, # ) - return line def _location_to_byte_offset(self, location: Location) -> int: """Given a document coordinate, return the byte offset of that coordinate. @@ -303,43 +302,3 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes: return b"\n" return b"" - - -@lru_cache(maxsize=128) -def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]: - """Build a mapping of utf-8 byte offsets to codepoint offsets for the given data. - - Args: - data: utf-8 bytes. - - Returns: - A `dict[int, int]` mapping byte indices to codepoint indices within `data`. - """ - byte_to_codepoint = {} - current_byte_offset = 0 - code_point_offset = 0 - - while current_byte_offset < len(data): - byte_to_codepoint[current_byte_offset] = code_point_offset - first_byte = data[current_byte_offset] - - # Single-byte character - if (first_byte & 0b10000000) == 0: - current_byte_offset += 1 - # 2-byte character - elif (first_byte & 0b11100000) == 0b11000000: - current_byte_offset += 2 - # 3-byte character - elif (first_byte & 0b11110000) == 0b11100000: - current_byte_offset += 3 - # 4-byte character - elif (first_byte & 0b11111000) == 0b11110000: - current_byte_offset += 4 - else: - raise ValueError(f"Invalid UTF-8 byte: {first_byte}") - - code_point_offset += 1 - - # Mapping for the end of the string - byte_to_codepoint[current_byte_offset] = code_point_offset - return byte_to_codepoint diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index aa8183cd9e..aaf103a2a0 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -3,6 +3,7 @@ import re from collections import defaultdict from dataclasses import dataclass, field +from functools import lru_cache from pathlib import Path from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple @@ -374,6 +375,7 @@ def _validate_selection(self, selection: Selection) -> Selection: def _watch_language(self, language: str | None) -> None: """When the language is updated, update the type of document.""" self.document = self._document_factory(self.text, language) + self._build_highlight_map() def _watch_show_line_numbers(self) -> None: """The line number gutter contributes to virtual size, so recalculate.""" @@ -576,20 +578,23 @@ def render_line(self, widget_y: int) -> Strip: theme = self.theme # Get the line from the Document. - line_string = document.get_line_text(line_index) + line_string = document.get_line(line_index) + line = Text(line_string, end="") - if self._highlights: + highlights = self._highlights + if highlights: line_bytes = _utf8_encode(line_string) byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) - get_highlight_from_theme = self._syntax_theme.get_highlight + get_highlight_from_theme = self.theme.token_styles.get line_highlights = highlights[line_index] for start, end, highlight_name in line_highlights: node_style = get_highlight_from_theme(highlight_name) - line.stylize( - node_style, - byte_to_codepoint.get(start, 0), - byte_to_codepoint.get(end) if end else None, - ) + if node_style is not None: + line.stylize( + node_style, + byte_to_codepoint.get(start, 0), + byte_to_codepoint.get(end) if end else None, + ) line_character_count = len(line) line.tab_size = self.indent_width @@ -1641,3 +1646,43 @@ def undo(self, text_area: TextArea) -> Any: Returns: Anything. This protocol doesn't prescribe what is returned. """ + + +@lru_cache(maxsize=128) +def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]: + """Build a mapping of utf-8 byte offsets to codepoint offsets for the given data. + + Args: + data: utf-8 bytes. + + Returns: + A `dict[int, int]` mapping byte indices to codepoint indices within `data`. + """ + byte_to_codepoint = {} + current_byte_offset = 0 + code_point_offset = 0 + + while current_byte_offset < len(data): + byte_to_codepoint[current_byte_offset] = code_point_offset + first_byte = data[current_byte_offset] + + # Single-byte character + if (first_byte & 0b10000000) == 0: + current_byte_offset += 1 + # 2-byte character + elif (first_byte & 0b11100000) == 0b11000000: + current_byte_offset += 2 + # 3-byte character + elif (first_byte & 0b11110000) == 0b11100000: + current_byte_offset += 3 + # 4-byte character + elif (first_byte & 0b11111000) == 0b11110000: + current_byte_offset += 4 + else: + raise ValueError(f"Invalid UTF-8 byte: {first_byte}") + + code_point_offset += 1 + + # Mapping for the end of the string + byte_to_codepoint[current_byte_offset] = code_point_offset + return byte_to_codepoint From 5183e7522ef711d99cf8e031e740b48196b14fb3 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 5 Sep 2023 13:10:34 +0100 Subject: [PATCH 302/366] Remove unused code --- src/textual/document/_document.py | 5 +- .../document/_syntax_aware_document.py | 66 ------------------- 2 files changed, 2 insertions(+), 69 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index f63fd707e8..52b699aa39 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -303,15 +303,14 @@ def line_count(self) -> int: """Returns the number of lines in the document.""" return len(self._lines) - def get_line(self, index: int) -> Text: + def get_line(self, index: int) -> str: """Returns the line with the given index from the document. Args: index: The index of the line in the document. Returns: - The Text instance representing the line. When overriding - this method, ensure the returned Text instance has `end=""`. + The string representing the line. """ line_string = self[index] return line_string diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index fa1dc85204..ae72d2efeb 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -155,20 +155,6 @@ def get_line(self, line_index: int) -> str: line_string = self[line_index] return line_string - # highlights = self._highlights - # if highlights: - # line_bytes = _utf8_encode(line_string) - # byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) - # get_highlight_from_theme = self._syntax_theme.get_highlight - # line_highlights = highlights[line_index] - # for start, end, highlight_name in line_highlights: - # node_style = get_highlight_from_theme(highlight_name) - # line.stylize( - # node_style, - # byte_to_codepoint.get(start, 0), - # byte_to_codepoint.get(end) if end else None, - # ) - def _location_to_byte_offset(self, location: Location) -> int: """Given a document coordinate, return the byte offset of that coordinate. This method only does work if tree-sitter was imported, otherwise it returns 0. @@ -212,58 +198,6 @@ def _location_to_point(self, location: Location) -> tuple[int, int]: bytes_on_left = 0 return row, bytes_on_left - # def _prepare_highlights( - # self, - # start_point: tuple[int, int] | None = None, - # end_point: tuple[int, int] | None = None, - # ) -> None: - # """Query the tree for ranges to highlights, and update the internal highlights mapping. - # - # Args: - # start_point: The point to start looking for highlights from. - # end_point: The point to look for highlights to. - # """ - # assert self._syntax_tree is not None - # - # highlights = self._highlights - # highlights.clear() - # - # captures_kwargs = {} - # if start_point is not None: - # captures_kwargs["start_point"] = start_point - # if end_point is not None: - # captures_kwargs["end_point"] = end_point - # - # # We could optimise by only preparing highlights for a subset of lines here. - # captures = self._query.captures(self._syntax_tree.root_node, **captures_kwargs) - # - # highlight_updates: dict[int, list[Highlight]] = defaultdict(list) - # for capture in captures: - # node, highlight_name = capture - # node_start_row, node_start_column = node.start_point - # node_end_row, node_end_column = node.end_point - # - # if node_start_row == node_end_row: - # highlight = (node_start_column, node_end_column, highlight_name) - # highlight_updates[node_start_row].append(highlight) - # else: - # # Add the first line of the node range - # highlight_updates[node_start_row].append( - # (node_start_column, None, highlight_name) - # ) - # - # # Add the middle lines - entire row of this node is highlighted - # for node_row in range(node_start_row + 1, node_end_row): - # highlight_updates[node_row].append((0, None, highlight_name)) - # - # # Add the last line of the node range - # highlight_updates[node_end_row].append( - # (0, node_end_column, highlight_name) - # ) - # - # for line_index, updated_highlights in highlight_updates.items(): - # highlights[line_index] = updated_highlights - def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes: """A callable which informs tree-sitter about the document content. From 814503e071f8bb0a35e579edcdbd0ce43df41e67 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 5 Sep 2023 13:12:20 +0100 Subject: [PATCH 303/366] Optimise imports --- src/textual/document/_syntax_aware_document.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index ae72d2efeb..d8cfec59ee 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -1,9 +1,7 @@ from __future__ import annotations -from functools import lru_cache from typing import Any -from rich.text import Text from typing_extensions import TYPE_CHECKING try: From 582367e46dd8cbb9bfea4cc89dd31cea92c2726f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 5 Sep 2023 15:33:36 +0100 Subject: [PATCH 304/366] Fix highlighting when initial text supplied to TextArea --- .../document/_syntax_aware_document.py | 8 +-- src/textual/widgets/_text_area.py | 61 +++++++------------ 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index d8cfec59ee..1cad36b973 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -2,15 +2,11 @@ from typing import Any -from typing_extensions import TYPE_CHECKING - try: + from tree_sitter import Language, Parser, Tree + from tree_sitter.binding import Query from tree_sitter_languages import get_language, get_parser - if TYPE_CHECKING: - from tree_sitter import Language, Parser, Tree - from tree_sitter.binding import Query - TREE_SITTER = True except ImportError: TREE_SITTER = False diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index aaf103a2a0..c225220fb9 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -156,7 +156,7 @@ class TextArea(ScrollView, can_focus=True): | f7 | Select all text in the document. | """ - language: Reactive[str | None] = reactive(None, always_update=True) + language: Reactive[str | None] = reactive(None, always_update=True, init=False) """The language to use. This must be set to a valid, non-None value for syntax highlighting to work. @@ -232,9 +232,7 @@ def __init__( disabled: True if the widget is disabled. """ super().__init__(name=name, id=id, classes=classes, disabled=disabled) - - self.document = self._document_factory(text, language) - """The document this widget is currently editing.""" + self._initial_text = text self._languages: dict[str, TextAreaLanguage] = {} """Maps language names to their TextAreaLanguage metadata.""" @@ -273,6 +271,11 @@ def __init__( self._highlight_query: "Query" | None = None """The query that's currently being used for highlighting.""" + self.document: DocumentBase | None = None + """The document this widget is currently editing.""" + + self.language = language + def _get_builtin_highlight_query(self, language_name: str) -> str: try: highlight_query_path = ( @@ -287,9 +290,11 @@ def _get_builtin_highlight_query(self, language_name: str) -> str: def _build_highlight_map(self) -> None: """Query the tree for ranges to highlights, and update the internal highlights mapping.""" + print("building highlight map") highlights = self._highlights highlights.clear() if not self._highlight_query: + print("no highlight query") return captures = self.document.query_syntax_tree(self._highlight_query) @@ -374,8 +379,11 @@ def _validate_selection(self, selection: Selection) -> Selection: def _watch_language(self, language: str | None) -> None: """When the language is updated, update the type of document.""" - self.document = self._document_factory(self.text, language) - self._build_highlight_map() + self._set_document( + self.document.text if self.document is not None else self._initial_text, + language, + ) + self._initial_text = "" def _watch_show_line_numbers(self) -> None: """The line number gutter contributes to virtual size, so recalculate.""" @@ -432,24 +440,9 @@ def register_language( highlight_query=highlight_query, ) - # def _set_document(self, text: str, language: str | None) -> DocumentBase: - # if language in self._languages: - # # Load the custom language if it exists - # language_spec = self._languages[language] - # document_language = language_spec.language - # highlight_query = language_spec.highlight_query - # else: - # document_language = language - # highlight_query = self._get_builtin_highlight_query(language) - # - # # Update the document and prepare the new query. - # self.document = SyntaxAwareDocument(text, document_language) - # if highlight_query: - # self._highlight_query = self.document.prepare_query(highlight_query) - # return self.document - - def _document_factory(self, text: str, language: str | None) -> DocumentBase: + def _set_document(self, text: str, language: str | None) -> None: """Construct and return an appropriate document.""" + self._highlight_query = None if TREE_SITTER and language: # Attempt to get the override language. text_area_language = self._languages.get(language, None) @@ -459,7 +452,6 @@ def _document_factory(self, text: str, language: str | None) -> DocumentBase: else: document_language = language highlight_query = self._get_builtin_highlight_query(language) - try: document = SyntaxAwareDocument(text, document_language) except RuntimeError: @@ -472,7 +464,8 @@ def _document_factory(self, text: str, language: str | None) -> DocumentBase: else: document = Document(text) - return document + self.document = document + self._build_highlight_map() @property def _visible_line_indices(self) -> tuple[int, int]: @@ -489,7 +482,7 @@ def load_text(self, text: str) -> None: Args: text: The text to load into the TextArea. """ - self.document = self._document_factory(text, self.language) + self._set_document(text, self.language) self.move_cursor((0, 0)) self._refresh_size() @@ -648,7 +641,6 @@ def render_line(self, widget_y: int) -> Strip: virtual_width, virtual_height = self.virtual_size # Highlight the cursor - # active_line_style = self.get_component_rich_style("text-area--cursor-line") if cursor_row == line_index: draw_cursor = not self.cursor_blink or ( self.cursor_blink and self._cursor_blink_visible @@ -662,7 +654,6 @@ def render_line(self, widget_y: int) -> Strip: ) if draw_cursor: - # cursor_style = self.get_component_rich_style("text-area--cursor") cursor_style = theme.cursor_style line.stylize(cursor_style, cursor_column, cursor_column + 1) @@ -670,9 +661,6 @@ def render_line(self, widget_y: int) -> Strip: if draw_matched_brackets: bracket_match_row, bracket_match_column = matching_bracket if bracket_match_row == line_index: - # matching_bracket_style = self.get_component_rich_style( - # "text-area--matching-bracket" - # ) matching_bracket_style = theme.bracket_matching_style if matching_bracket_style: line.stylize( @@ -686,11 +674,7 @@ def render_line(self, widget_y: int) -> Strip: if self.show_line_numbers: if cursor_row == line_index: gutter_style = theme.cursor_line_gutter_style - # gutter_style = self.get_component_rich_style( - # "text-area--cursor-line-gutter" - # ) else: - # gutter_style = self.get_component_rich_style("text-area--gutter") gutter_style = theme.gutter_style gutter_width_no_margin = gutter_width - 2 @@ -703,10 +687,11 @@ def render_line(self, widget_y: int) -> Strip: gutter = Text("", end="") # Render the gutter and the text of this line - gutter_segments = self.app.console.render(gutter) - text_segments = self.app.console.render( + console = self.app.console + gutter_segments = console.render(gutter) + text_segments = console.render( line, - self.app.console.options.update_width(virtual_width), + console.options.update_width(virtual_width), ) # Crop the line to show only the visible part (some may be scrolled out of view) From ce38c95daacf62488ac22968cd3a4f33b3d44fe2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 5 Sep 2023 16:50:52 +0100 Subject: [PATCH 305/366] Rebuild highlight map when the theme changes --- src/textual/widgets/_text_area.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index c225220fb9..e027914d42 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -319,6 +319,10 @@ def _build_highlight_map(self) -> None: # Add the last line of the node range highlights[node_end_row].append((0, node_end_column, highlight_name)) + def _watch_theme(self) -> None: + """When the theme changes, update the highlight map""" + self._build_highlight_map() + def _watch_selection(self, selection: Selection) -> None: """When the cursor moves, scroll it into view.""" self.scroll_cursor_visible() From b90da6e861c4ee92c5ead63719a87e0e09d8c08e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 5 Sep 2023 16:57:14 +0100 Subject: [PATCH 306/366] Extending --- src/textual/widgets/_text_area.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index e027914d42..7edd98ddf0 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -168,7 +168,7 @@ class TextArea(ScrollView, can_focus=True): """ theme: Reactive[str | TextAreaTheme] = reactive( - TextAreaTheme.default(), always_update=True, init=True + TextAreaTheme.default(), always_update=True, init=False ) """The theme to syntax highlight with. @@ -237,12 +237,6 @@ def __init__( self._languages: dict[str, TextAreaLanguage] = {} """Maps language names to their TextAreaLanguage metadata.""" - if isinstance(theme, str): - theme = TextAreaTheme.get_by_name(theme) - - self.theme: TextAreaTheme = theme - """The theme of the `TextArea`.""" - self.indent_type: Literal["tabs", "spaces"] = "spaces" """Whether to indent using tabs or spaces.""" @@ -276,6 +270,12 @@ def __init__( self.language = language + if isinstance(theme, str): + theme = TextAreaTheme.get_by_name(theme) + + self.theme: TextAreaTheme = theme + """The theme of the `TextArea`.""" + def _get_builtin_highlight_query(self, language_name: str) -> str: try: highlight_query_path = ( @@ -593,9 +593,12 @@ def render_line(self, widget_y: int) -> Strip: byte_to_codepoint.get(end) if end else None, ) + virtual_width, virtual_height = self.virtual_size + line_character_count = len(line) line.tab_size = self.indent_width - line.set_length(self.virtual_size.width) + expanded_length = max(virtual_width, self.size.width) + line.set_length(expanded_length) selection = self.selection start, end = selection @@ -642,8 +645,6 @@ def render_line(self, widget_y: int) -> Strip: else: line.stylize(selection_style, end=line_character_count) - virtual_width, virtual_height = self.virtual_size - # Highlight the cursor if cursor_row == line_index: draw_cursor = not self.cursor_blink or ( @@ -695,7 +696,7 @@ def render_line(self, widget_y: int) -> Strip: gutter_segments = console.render(gutter) text_segments = console.render( line, - console.options.update_width(virtual_width), + console.options.update_width(expanded_length), ) # Crop the line to show only the visible part (some may be scrolled out of view) @@ -705,9 +706,9 @@ def render_line(self, widget_y: int) -> Strip: ) # Stylize the line the cursor is currently on. - if cursor_row == line_index: - expanded_length = max(virtual_width, self.size.width) - text_strip = text_strip.extend_cell_length(expanded_length) + # TODO - is this required or have we already expanded it? + # if cursor_row == line_index: + # text_strip = text_strip.extend_cell_length(expanded_length) # Join and return the gutter and the visible portion of this line strip = Strip.join([gutter_strip, text_strip]).simplify() From ef3e4a034910ca0d84dacb03ffdfe73e89116d7a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 6 Sep 2023 11:22:47 +0100 Subject: [PATCH 307/366] Restore themes --- src/textual/document/_text_area_theme.py | 63 +- src/textual/widgets/_text_area.py | 23 +- .../__snapshots__/test_snapshots.ambr | 2341 ++++++++--------- tree-sitter/highlights/python.scm | 2 +- 4 files changed, 1233 insertions(+), 1196 deletions(-) diff --git a/src/textual/document/_text_area_theme.py b/src/textual/document/_text_area_theme.py index 1ec39a9e13..6055a85d17 100644 --- a/src/textual/document/_text_area_theme.py +++ b/src/textual/document/_text_area_theme.py @@ -147,7 +147,7 @@ def default(cls) -> TextAreaTheme: cursor_style=Style(color="#272822", bgcolor="#f8f8f0"), cursor_line_style=Style(bgcolor="#3e3d32"), cursor_line_gutter_style=Style(color="#c2c2bf", bgcolor="#3e3d32"), - bracket_matching_style=Style(bold=True, underline=True), + bracket_matching_style=Style(bgcolor="#838889", bold=True), selection_style=Style(bgcolor="#65686a"), token_styles={ "string": Style(color="#E6DB74"), @@ -165,28 +165,21 @@ def default(cls) -> TextAreaTheme: "number": Style(color="#AE81FF"), "float": Style(color="#AE81FF"), "class": Style(color="#A6E22E"), + "type.class": Style(color="#A6E22E"), "function": Style(color="#A6E22E"), "function.call": Style(color="#A6E22E"), "method": Style(color="#A6E22E"), "method.call": Style(color="#A6E22E"), "boolean": Style(color="#66D9EF", italic=True), "json.null": Style(color="#66D9EF", italic=True), - # "constant": Style(color="#AE81FF"), - # "variable": Style(color="white"), - # "parameter": Style(color="cyan"), - # "type": Style(color="cyan"), - # "escape": Style("magenta"), - # "error": Style(color="black", bgcolor="red"), "regex.punctuation.bracket": Style(color="#F92672"), "regex.operator": Style(color="#F92672"), - # "json.error": _NULL_STYLE, "html.end_tag_error": Style(color="red", underline=True), "tag": Style(color="#F92672"), "yaml.field": Style(color="#F92672", bold=True), "json.label": Style(color="#F92672", bold=True), "toml.type": Style(color="#F92672"), "toml.datetime": Style(color="#AE81FF"), - # "toml.error": _NULL_STYLE, "heading": Style(color="#F92672", bold=True), "bold": Style(bold=True), "italic": Style(italic=True), @@ -196,6 +189,55 @@ def default(cls) -> TextAreaTheme: }, ) +_DRACULA = TextAreaTheme( + name="dracula", + base_style=Style(color="#f8f8f2", bgcolor="#1E1F35"), + gutter_style=Style(color="#6272a4"), + cursor_style=Style(color="#282a36", bgcolor="#f8f8f0"), + cursor_line_style=Style(bgcolor="#282b45"), + cursor_line_gutter_style=Style(color="#c2c2bf", bgcolor="#282b45", bold=True), + bracket_matching_style=Style(bgcolor="#99999d", bold=True, underline=True), + selection_style=Style(bgcolor="#44475A"), + token_styles={ + "string": Style(color="#f1fa8c"), + "string.documentation": Style(color="#f1fa8c"), + "comment": Style(color="#6272a4"), + "keyword": Style(color="#ff79c6"), + "operator": Style(color="#ff79c6"), + "repeat": Style(color="#ff79c6"), + "exception": Style(color="#ff79c6"), + "include": Style(color="#ff79c6"), + "keyword.function": Style(color="#ff79c6"), + "keyword.return": Style(color="#ff79c6"), + "keyword.operator": Style(color="#ff79c6"), + "conditional": Style(color="#ff79c6"), + "number": Style(color="#bd93f9"), + "float": Style(color="#bd93f9"), + "class": Style(color="#50fa7b"), + "type.class": Style(color="#50fa7b"), + "function": Style(color="#50fa7b"), + "function.call": Style(color="#50fa7b"), + "method": Style(color="#50fa7b"), + "method.call": Style(color="#50fa7b"), + "boolean": Style(color="#bd93f9"), + "json.null": Style(color="#bd93f9"), + "regex.punctuation.bracket": Style(color="#ff79c6"), + "regex.operator": Style(color="#ff79c6"), + "html.end_tag_error": Style(color="#F83333", underline=True), + "tag": Style(color="#ff79c6"), + "yaml.field": Style(color="#ff79c6", bold=True), + "json.label": Style(color="#ff79c6", bold=True), + "toml.type": Style(color="#ff79c6"), + "toml.datetime": Style(color="#bd93f9"), + "heading": Style(color="#ff79c6", bold=True), + "bold": Style(bold=True), + "italic": Style(italic=True), + "strikethrough": Style(strike=True), + "link": Style(color="#bd93f9", underline=True), + "inline_code": Style(color="#ff79c6"), + }, +) + # _DRACULA = { # "string": Style(color="#f1fa8c"), # "string.documentation": Style(color="#f1fa8c"), @@ -244,9 +286,8 @@ def default(cls) -> TextAreaTheme: _BUILTIN_THEMES = { "monokai": _MONOKAI, - # "dracula": _DRACULA, + "dracula": _DRACULA, } - DEFAULT_SYNTAX_THEME = TextAreaTheme.get_by_name("monokai") """The default syntax highlighting theme used by Textual.""" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 7edd98ddf0..01f5efdf40 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -578,6 +578,12 @@ def render_line(self, widget_y: int) -> Strip: line_string = document.get_line(line_index) line = Text(line_string, end="") + line_character_count = len(line) + line.tab_size = self.indent_width + virtual_width, virtual_height = self.virtual_size + expanded_length = max(virtual_width, self.size.width) + line.set_length(expanded_length) + highlights = self._highlights if highlights: line_bytes = _utf8_encode(line_string) @@ -593,13 +599,6 @@ def render_line(self, widget_y: int) -> Strip: byte_to_codepoint.get(end) if end else None, ) - virtual_width, virtual_height = self.virtual_size - - line_character_count = len(line) - line.tab_size = self.indent_width - expanded_length = max(virtual_width, self.size.width) - line.set_length(expanded_length) - selection = self.selection start, end = selection selection_top, selection_bottom = selection.range @@ -707,8 +706,14 @@ def render_line(self, widget_y: int) -> Strip: # Stylize the line the cursor is currently on. # TODO - is this required or have we already expanded it? - # if cursor_row == line_index: - # text_strip = text_strip.extend_cell_length(expanded_length) + if cursor_row == line_index: + text_strip = text_strip.extend_cell_length( + expanded_length, theme.cursor_line_style + ) + else: + text_strip = text_strip.extend_cell_length( + expanded_length, theme.base_style + ) # Join and return the gutter and the visible portion of this line strip = Strip.join([gutter_strip, text_strip]).simplify() diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 7a9aa8c1b8..0ec72692d1 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -27982,319 +27982,319 @@ font-weight: 700; } - .terminal-1270666592-matrix { + .terminal-2963979561-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1270666592-title { + .terminal-2963979561-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1270666592-r1 { fill: #e4e5e6 } - .terminal-1270666592-r2 { fill: #151515 } - .terminal-1270666592-r3 { fill: #75715e } - .terminal-1270666592-r4 { fill: #c5c8c6 } - .terminal-1270666592-r5 { fill: #86898c } - .terminal-1270666592-r6 { fill: #e2e3e3 } - .terminal-1270666592-r7 { fill: #e6db74 } - .terminal-1270666592-r8 { fill: #ae81ff } - .terminal-1270666592-r9 { fill: #f92672 } - .terminal-1270666592-r10 { fill: #a6e22e } + .terminal-2963979561-r1 { fill: #c2c2bf } + .terminal-2963979561-r2 { fill: #272822 } + .terminal-2963979561-r3 { fill: #75715e } + .terminal-2963979561-r4 { fill: #f8f8f2 } + .terminal-2963979561-r5 { fill: #c5c8c6 } + .terminal-2963979561-r6 { fill: #90908a } + .terminal-2963979561-r7 { fill: #e6db74 } + .terminal-2963979561-r8 { fill: #ae81ff } + .terminal-2963979561-r9 { fill: #f92672 } + .terminal-2963979561-r10 { fill: #a6e22e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  /* This is a comment in CSS */ -  2   -  3  /* Basic selectors and properties */ -  4  body {                                 -  5      font-family: Arial, sans-serif;    -  6      background-color: #f4f4f4;         -  7      margin: 0;                         -  8      padding: 0;                        -  9  }                                      - 10   - 11  /* Class and ID selectors */ - 12  .header {                              - 13      background-color: #333;            - 14      color: #fff;                       - 15      padding: 10px0;                   - 16      text-align: center;                - 17  }                                      - 18   - 19  #logo {                                - 20      font-size: 24px;                   - 21      font-weight: bold;                 - 22  }                                      - 23   - 24  /* Descendant and child selectors */ - 25  .nav ul {                              - 26      list-style-type: none;             - 27      padding: 0;                        - 28  }                                      - 29   - 30  .nav > li {                            - 31      display: inline-block;             - 32      margin-right: 10px;                - 33  }                                      - 34   - 35  /* Pseudo-classes */ - 36  a:hover {                              - 37      text-decoration: underline;        - 38  }                                      - 39   - 40  input:focus {                          - 41      border-color: #007BFF;             - 42  }                                      - 43   - 44  /* Media query */ - 45  @media (max-width: 768px) {            - 46      body {                             - 47          font-size: 16px;               - 48      }                                  - 49   - 50      .header {                          - 51          padding: 5px0;                - 52      }                                  - 53  }                                      - 54   - 55  /* Keyframes animation */ - 56  @keyframes slideIn {                   - 57  from {                             - 58          transform: translateX(-100%);  - 59      }                                  - 60  to {                               - 61          transform: translateX(0);      - 62      }                                  - 63  }                                      - 64   - 65  .slide-in-element {                    - 66      animation: slideIn 0.5s forwards;  - 67  }                                      - 68   + + + +  1  /* This is a comment in CSS */ +  2   +  3  /* Basic selectors and properties */ +  4  body {                                 +  5      font-family: Arial, sans-serif;    +  6      background-color: #f4f4f4;         +  7      margin: 0;                         +  8      padding: 0;                        +  9  }                                      + 10   + 11  /* Class and ID selectors */ + 12  .header {                              + 13      background-color: #333;            + 14      color: #fff;                       + 15      padding: 10px0;                   + 16      text-align: center;                + 17  }                                      + 18   + 19  #logo {                                + 20      font-size: 24px;                   + 21      font-weight: bold;                 + 22  }                                      + 23   + 24  /* Descendant and child selectors */ + 25  .nav ul {                              + 26      list-style-type: none;             + 27      padding: 0;                        + 28  }                                      + 29   + 30  .nav > li {                            + 31      display: inline-block;             + 32      margin-right: 10px;                + 33  }                                      + 34   + 35  /* Pseudo-classes */ + 36  a:hover {                              + 37      text-decoration: underline;        + 38  }                                      + 39   + 40  input:focus {                          + 41      border-color: #007BFF;             + 42  }                                      + 43   + 44  /* Media query */ + 45  @media (max-width: 768px) {            + 46      body {                             + 47          font-size: 16px;               + 48      }                                  + 49   + 50      .header {                          + 51          padding: 5px0;                + 52      }                                  + 53  }                                      + 54   + 55  /* Keyframes animation */ + 56  @keyframes slideIn {                   + 57  from {                             + 58          transform: translateX(-100%);  + 59      }                                  + 60  to {                               + 61          transform: translateX(0);      + 62      }                                  + 63  }                                      + 64   + 65  .slide-in-element {                    + 66      animation: slideIn 0.5s forwards;  + 67  }                                      + 68   @@ -28325,273 +28325,273 @@ font-weight: 700; } - .terminal-2506971960-matrix { + .terminal-2763447717-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2506971960-title { + .terminal-2763447717-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2506971960-r1 { fill: #e4e5e6 } - .terminal-2506971960-r2 { fill: #151515 } - .terminal-2506971960-r3 { fill: #c5c8c6 } - .terminal-2506971960-r4 { fill: #86898c } - .terminal-2506971960-r5 { fill: #e2e3e3 } - .terminal-2506971960-r6 { fill: #f92672 } - .terminal-2506971960-r7 { fill: #e6db74 } - .terminal-2506971960-r8 { fill: #75715e } + .terminal-2763447717-r1 { fill: #c2c2bf } + .terminal-2763447717-r2 { fill: #272822 } + .terminal-2763447717-r3 { fill: #f8f8f2 } + .terminal-2763447717-r4 { fill: #c5c8c6 } + .terminal-2763447717-r5 { fill: #90908a } + .terminal-2763447717-r6 { fill: #f92672 } + .terminal-2763447717-r7 { fill: #e6db74 } + .terminal-2763447717-r8 { fill: #75715e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  <!DOCTYPE html>                                                              -  2  <html lang="en">                                                            -  3   -  4  <head>                                                                      -  5  <!-- Meta tags --> -  6      <meta charset="UTF-8">                                                  -  7      <meta name="viewport" content="width=device-width, initial-scale=1.0" -  8  <!-- Title --> -  9      <title>HTML Test Page</title>                                           - 10  <!-- Link to CSS --> - 11      <link rel="stylesheet" href="styles.css">                               - 12  </head>                                                                     - 13   - 14  <body>                                                                      - 15  <!-- Header section --> - 16      <header class="header">                                                 - 17          <h1 id="logo">HTML Test Page</h1>                                   - 18      </header>                                                               - 19   - 20  <!-- Navigation --> - 21      <nav class="nav">                                                       - 22          <ul>                                                                - 23              <li><a href="#">Home</a></li>                                   - 24              <li><a href="#">About</a></li>                                  - 25              <li><a href="#">Contact</a></li>                                - 26          </ul>                                                               - 27      </nav>                                                                  - 28   - 29  <!-- Main content area --> - 30      <main>                                                                  - 31          <article>                                                           - 32              <h2>Welcome to the Test Page</h2>                               - 33              <p>This is a paragraph to test the HTML structure.</p>          - 34              <img src="test-image.jpg" alt="Test Image" width="300">         - 35          </article>                                                          - 36      </main>                                                                 - 37   - 38  <!-- Form --> - 39      <section>                                                               - 40          <form action="/submit" method="post">                               - 41              <label for="name">Name:</label>                                 - 42              <input type="text" id="name" name="name">                       - 43              <input type="submit" value="Submit">                            - 44          </form>                                                             - 45      </section>                                                              - 46   - 47  <!-- Footer --> - 48      <footer>                                                                - 49          <p>&copy; 2023 HTML Test Page</p>                                   - 50      </footer>                                                               - 51   - 52  <!-- Script tag --> - 53      <script src="scripts.js"></script>                                      - 54  </body>                                                                     - 55   - 56  </html>                                                                     - 57   + + + +  1  <!DOCTYPE html>                                                              +  2  <html lang="en">                                                            +  3   +  4  <head>                                                                      +  5  <!-- Meta tags --> +  6      <meta charset="UTF-8">                                                  +  7      <meta name="viewport" content="width=device-width, initial-scale=1.0" +  8  <!-- Title --> +  9      <title>HTML Test Page</title>                                           + 10  <!-- Link to CSS --> + 11      <link rel="stylesheet" href="styles.css">                               + 12  </head>                                                                     + 13   + 14  <body>                                                                      + 15  <!-- Header section --> + 16      <header class="header">                                                 + 17          <h1 id="logo">HTML Test Page</h1>                                   + 18      </header>                                                               + 19   + 20  <!-- Navigation --> + 21      <nav class="nav">                                                       + 22          <ul>                                                                + 23              <li><a href="#">Home</a></li>                                   + 24              <li><a href="#">About</a></li>                                  + 25              <li><a href="#">Contact</a></li>                                + 26          </ul>                                                               + 27      </nav>                                                                  + 28   + 29  <!-- Main content area --> + 30      <main>                                                                  + 31          <article>                                                           + 32              <h2>Welcome to the Test Page</h2>                               + 33              <p>This is a paragraph to test the HTML structure.</p>          + 34              <img src="test-image.jpg" alt="Test Image" width="300">         + 35          </article>                                                          + 36      </main>                                                                 + 37   + 38  <!-- Form --> + 39      <section>                                                               + 40          <form action="/submit" method="post">                               + 41              <label for="name">Name:</label>                                 + 42              <input type="text" id="name" name="name">                       + 43              <input type="submit" value="Submit">                            + 44          </form>                                                             + 45      </section>                                                              + 46   + 47  <!-- Footer --> + 48      <footer>                                                                + 49          <p>&copy; 2023 HTML Test Page</p>                                   + 50      </footer>                                                               + 51   + 52  <!-- Script tag --> + 53      <script src="scripts.js"></script>                                      + 54  </body>                                                                     + 55   + 56  </html>                                                                     + 57   @@ -28622,171 +28622,171 @@ font-weight: 700; } - .terminal-3995077928-matrix { + .terminal-2458942-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3995077928-title { + .terminal-2458942-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3995077928-r1 { fill: #e4e5e6 } - .terminal-3995077928-r2 { fill: #151515;font-weight: bold;text-decoration: underline; } - .terminal-3995077928-r3 { fill: #c5c8c6 } - .terminal-3995077928-r4 { fill: #86898c } - .terminal-3995077928-r5 { fill: #e2e3e3 } - .terminal-3995077928-r6 { fill: #f92672;font-weight: bold } - .terminal-3995077928-r7 { fill: #e6db74 } - .terminal-3995077928-r8 { fill: #ae81ff } - .terminal-3995077928-r9 { fill: #66d9ef;font-style: italic; } - .terminal-3995077928-r10 { fill: #e8e8e9;font-weight: bold;text-decoration: underline; } + .terminal-2458942-r1 { fill: #c2c2bf } + .terminal-2458942-r2 { fill: #272822;font-weight: bold } + .terminal-2458942-r3 { fill: #f8f8f2 } + .terminal-2458942-r4 { fill: #c5c8c6 } + .terminal-2458942-r5 { fill: #90908a } + .terminal-2458942-r6 { fill: #f92672;font-weight: bold } + .terminal-2458942-r7 { fill: #e6db74 } + .terminal-2458942-r8 { fill: #ae81ff } + .terminal-2458942-r9 { fill: #66d9ef;font-style: italic; } + .terminal-2458942-r10 { fill: #f8f8f2;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  { -  2  "name""John Doe",                            -  3  "age"30,                                     -  4  "isStudent"false,                            -  5  "address": {                                   -  6  "street""123 Main St",                   -  7  "city""Anytown",                         -  8  "state""CA",                             -  9  "zip""12345" - 10      },                                             - 11  "phoneNumbers": [                              - 12          {                                          - 13  "type""home",                        - 14  "number""555-555-1234" - 15          },                                         - 16          {                                          - 17  "type""work",                        - 18  "number""555-555-5678" - 19          }                                          - 20      ],                                             - 21  "hobbies": ["reading""hiking""swimming"],  - 22  "pets": [                                      - 23          {                                          - 24  "type""dog",                         - 25  "name""Fido" - 26          },                                         - 27      ],                                             - 28  "graduationYear"null - 29  } - 30   - 31   + + + +  1  { +  2  "name""John Doe",                            +  3  "age"30,                                     +  4  "isStudent"false,                            +  5  "address": {                                   +  6  "street""123 Main St",                   +  7  "city""Anytown",                         +  8  "state""CA",                             +  9  "zip""12345" + 10      },                                             + 11  "phoneNumbers": [                              + 12          {                                          + 13  "type""home",                        + 14  "number""555-555-1234" + 15          },                                         + 16          {                                          + 17  "type""work",                        + 18  "number""555-555-5678" + 19          }                                          + 20      ],                                             + 21  "hobbies": ["reading""hiking""swimming"],  + 22  "pets": [                                      + 23          {                                          + 24  "type""dog",                         + 25  "name""Fido" + 26          },                                         + 27      ],                                             + 28  "graduationYear"null + 29  } + 30   + 31   @@ -28817,322 +28817,323 @@ font-weight: 700; } - .terminal-2031465495-matrix { + .terminal-3459552068-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2031465495-title { + .terminal-3459552068-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2031465495-r1 { fill: #e4e5e6 } - .terminal-2031465495-r2 { fill: #151515;font-weight: bold } - .terminal-2031465495-r3 { fill: #f92672;font-weight: bold } - .terminal-2031465495-r4 { fill: #c5c8c6 } - .terminal-2031465495-r5 { fill: #86898c } - .terminal-2031465495-r6 { fill: #e2e3e3 } - .terminal-2031465495-r7 { fill: #e2e3e3;font-style: italic; } - .terminal-2031465495-r8 { fill: #e2e3e3;font-weight: bold } - .terminal-2031465495-r9 { fill: #f92672 } - .terminal-2031465495-r10 { fill: #75715e } - .terminal-2031465495-r11 { fill: #66d9ef;text-decoration: underline; } - .terminal-2031465495-r12 { fill: #23568b } + .terminal-3459552068-r1 { fill: #c2c2bf } + .terminal-3459552068-r2 { fill: #272822;font-weight: bold } + .terminal-3459552068-r3 { fill: #f92672;font-weight: bold } + .terminal-3459552068-r4 { fill: #f8f8f2 } + .terminal-3459552068-r5 { fill: #c5c8c6 } + .terminal-3459552068-r6 { fill: #90908a } + .terminal-3459552068-r7 { fill: #f8f8f2;font-style: italic; } + .terminal-3459552068-r8 { fill: #f8f8f2;font-weight: bold } + .terminal-3459552068-r9 { fill: #f92672 } + .terminal-3459552068-r10 { fill: #75715e } + .terminal-3459552068-r11 { fill: #66d9ef;text-decoration: underline; } + .terminal-3459552068-r12 { fill: #e1e1e1 } + .terminal-3459552068-r13 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  Heading -  2  =======                                                                      -  3   -  4  Sub-heading -  5  -----------                                                                  -  6   -  7  ### Heading -  8   -  9  #### H4 Heading - 10   - 11  ##### H5 Heading - 12   - 13  ###### H6 Heading - 14   - 15   - 16  Paragraphs are separated                                                     - 17  by a blank line.                                                             - 18   - 19  Two spaces at the end of a line                                              - 20  produces a line break.                                                       - 21   - 22  Text attributes _italic_,                                                    - 23  **bold**`monospace`.                                                       - 24   - 25  Horizontal rule:                                                             - 26   - 27  ---                                                                          - 28   - 29  Bullet list:                                                                 - 30   - 31  * apples                                                                   - 32  * oranges                                                                  - 33  * pears                                                                    - 34   - 35  Numbered list:                                                               - 36   - 37  1. lather                                                                  - 38  2. rinse                                                                   - 39  3. repeat                                                                  - 40   - 41  An [example](http://example.com).                                            - 42   - 43  > Markdown uses email-style > characters for blockquoting.                   - 44  >                                                                            - 45  > Lorem ipsum                                                                - 46   - 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) - 48   - 49   - 50  ```                                                                          - 51  a=1                                                                          - 52  ```                                                                          - 53   - 54  ```python                                                                    - 55  import this                                                                  - 56  ```                                                                          - 57   - 58  ```somelang                                                                  - 59  foobar                                                                       - 60  ```                                                                          - 61   - 62      import this                                                              - 63   - 64   - 65  1. List item                                                                 - 66   - 67         Code block                                                            - 68   - + + + +  1  Heading +  2  =======                                                                      +  3   +  4  Sub-heading +  5  -----------                                                                  +  6   +  7  ### Heading +  8   +  9  #### H4 Heading + 10   + 11  ##### H5 Heading + 12   + 13  ###### H6 Heading + 14   + 15   + 16  Paragraphs are separated                                                     + 17  by a blank line.                                                             + 18   + 19  Two spaces at the end of a line                                              + 20  produces a line break.                                                       + 21   + 22  Text attributes _italic_,                                                    + 23  **bold**`monospace`.                                                       + 24   + 25  Horizontal rule:                                                             + 26   + 27  ---                                                                          + 28   + 29  Bullet list:                                                                 + 30   + 31  * apples                                                                   + 32  * oranges                                                                  + 33  * pears                                                                    + 34   + 35  Numbered list:                                                               + 36   + 37  1. lather                                                                  + 38  2. rinse                                                                   + 39  3. repeat                                                                  + 40   + 41  An [example](http://example.com).                                            + 42   + 43  > Markdown uses email-style > characters for blockquoting.                   + 44  >                                                                            + 45  > Lorem ipsum                                                                + 46   + 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) + 48   + 49   + 50  ```                                                                          + 51  a=1                                                                          + 52  ```                                                                          + 53   + 54  ```python                                                                    + 55  import this                                                                  + 56  ```                                                                          + 57   + 58  ```somelang                                                                  + 59  foobar                                                                       + 60  ```                                                                          + 61   + 62      import this                                                              + 63   + 64   + 65  1. List item                                                                 + 66   + 67         Code block                                                            + 68   + @@ -29162,363 +29163,363 @@ font-weight: 700; } - .terminal-1658030963-matrix { + .terminal-1854819181-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1658030963-title { + .terminal-1854819181-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1658030963-r1 { fill: #e4e5e6 } - .terminal-1658030963-r2 { fill: #151515 } - .terminal-1658030963-r3 { fill: #f92672 } - .terminal-1658030963-r4 { fill: #c5c8c6 } - .terminal-1658030963-r5 { fill: #86898c } - .terminal-1658030963-r6 { fill: #e2e3e3 } - .terminal-1658030963-r7 { fill: #75715e } - .terminal-1658030963-r8 { fill: #e6db74 } - .terminal-1658030963-r9 { fill: #ae81ff } - .terminal-1658030963-r10 { fill: #a6e22e } + .terminal-1854819181-r1 { fill: #c2c2bf } + .terminal-1854819181-r2 { fill: #272822 } + .terminal-1854819181-r3 { fill: #f92672 } + .terminal-1854819181-r4 { fill: #f8f8f2 } + .terminal-1854819181-r5 { fill: #c5c8c6 } + .terminal-1854819181-r6 { fill: #90908a } + .terminal-1854819181-r7 { fill: #75715e } + .terminal-1854819181-r8 { fill: #e6db74 } + .terminal-1854819181-r9 { fill: #ae81ff } + .terminal-1854819181-r10 { fill: #a6e22e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  import math                                                                  -  2  from os import path                                                          -  3   -  4  # I'm a comment :) -  5   -  6  string_var ="Hello, world!" -  7  int_var =42 -  8  float_var =3.14 -  9  complex_var =1+2j - 10   - 11  list_var = [12345]                                                   - 12  tuple_var = (12345)                                                  - 13  set_var = {12345}                                                    - 14  dict_var = {"a"1"b"2"c"3}                                          - 15   - 16  deffunction_no_args():                                                      - 17  return"No arguments" - 18   - 19  deffunction_with_args(a, b):                                                - 20  return a + b                                                             - 21   - 22  deffunction_with_default_args(a=0, b=0):                                    - 23  return a * b                                                             - 24   - 25  lambda_func =lambda x: x**2 - 26   - 27  if int_var ==42:                                                            - 28  print("It's the answer!")                                                - 29  elif int_var <42:                                                           - 30  print("Less than the answer.")                                           - 31  else:                                                                        - 32  print("Greater than the answer.")                                        - 33   - 34  for index, value inenumerate(list_var):                                     - 35  print(f"Index: {index}, Value: {value}")                                 - 36   - 37  counter =0 - 38  while counter <5:                                                           - 39  print(f"Counter value: {counter}")                                       - 40      counter +=1 - 41   - 42  squared_numbers = [x**2for x inrange(10if x %2==0]                    - 43   - 44  try:                                                                         - 45      result =10/0 - 46  except ZeroDivisionError:                                                    - 47  print("Cannot divide by zero!")                                          - 48  finally:                                                                     - 49  print("End of try-except block.")                                        - 50   - 51  class Animal:                                                                - 52  def__init__(self, name):                                                - 53          self.name = name                                                     - 54   - 55  defspeak(self):                                                         - 56  raiseNotImplementedError("Subclasses must implement this method." - 57   - 58  class Dog(Animal):                                                           - 59  defspeak(self):                                                         - 60  returnf"{self.name} says Woof!" - 61   - 62  deffibonacci(n):                                                            - 63      a, b =01 - 64  for _ inrange(n):                                                       - 65  yield a                                                              - 66          a, b = b, a + b                                                      - 67   - 68  for num infibonacci(5):                                                     - 69  print(num)                                                               - 70   - 71  withopen('test.txt''w'as f:                                             - 72      f.write("Testing with statement.")                                       - 73   - 74  @my_decorator                                                                - 75  defsay_hello():                                                             - 76  print("Hello!")                                                          - 77   - 78  say_hello()                                                                  - 79   + + + +  1  import math                                                                  +  2  from os import path                                                          +  3   +  4  # I'm a comment :) +  5   +  6  string_var ="Hello, world!" +  7  int_var =42 +  8  float_var =3.14 +  9  complex_var =1+2j + 10   + 11  list_var = [12345]                                                   + 12  tuple_var = (12345)                                                  + 13  set_var = {12345}                                                    + 14  dict_var = {"a"1"b"2"c"3}                                          + 15   + 16  deffunction_no_args():                                                      + 17  return"No arguments" + 18   + 19  deffunction_with_args(a, b):                                                + 20  return a + b                                                             + 21   + 22  deffunction_with_default_args(a=0, b=0):                                    + 23  return a * b                                                             + 24   + 25  lambda_func =lambda x: x**2 + 26   + 27  if int_var ==42:                                                            + 28  print("It's the answer!")                                                + 29  elif int_var <42:                                                           + 30  print("Less than the answer.")                                           + 31  else:                                                                        + 32  print("Greater than the answer.")                                        + 33   + 34  for index, value inenumerate(list_var):                                     + 35  print(f"Index: {index}, Value: {value}")                                 + 36   + 37  counter =0 + 38  while counter <5:                                                           + 39  print(f"Counter value: {counter}")                                       + 40      counter +=1 + 41   + 42  squared_numbers = [x**2for x inrange(10if x %2==0]                    + 43   + 44  try:                                                                         + 45      result =10/0 + 46  except ZeroDivisionError:                                                    + 47  print("Cannot divide by zero!")                                          + 48  finally:                                                                     + 49  print("End of try-except block.")                                        + 50   + 51  classAnimal:                                                                + 52  def__init__(self, name):                                                + 53          self.name = name                                                     + 54   + 55  defspeak(self):                                                         + 56  raiseNotImplementedError("Subclasses must implement this method." + 57   + 58  classDog(Animal):                                                           + 59  defspeak(self):                                                         + 60  returnf"{self.name} says Woof!" + 61   + 62  deffibonacci(n):                                                            + 63      a, b =01 + 64  for _ inrange(n):                                                       + 65  yield a                                                              + 66          a, b = b, a + b                                                      + 67   + 68  for num infibonacci(5):                                                     + 69  print(num)                                                               + 70   + 71  withopen('test.txt''w'as f:                                             + 72      f.write("Testing with statement.")                                       + 73   + 74  @my_decorator                                                                + 75  defsay_hello():                                                             + 76  print("Hello!")                                                          + 77   + 78  say_hello()                                                                  + 79   @@ -29549,145 +29550,146 @@ font-weight: 700; } - .terminal-2329209973-matrix { + .terminal-4250850736-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2329209973-title { + .terminal-4250850736-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2329209973-r1 { fill: #e4e5e6 } - .terminal-2329209973-r2 { fill: #151515 } - .terminal-2329209973-r3 { fill: #c5c8c6 } - .terminal-2329209973-r4 { fill: #86898c } - .terminal-2329209973-r5 { fill: #e2e3e3 } - .terminal-2329209973-r6 { fill: #f92672 } - .terminal-2329209973-r7 { fill: #23568b } + .terminal-4250850736-r1 { fill: #c2c2bf } + .terminal-4250850736-r2 { fill: #272822 } + .terminal-4250850736-r3 { fill: #f8f8f2 } + .terminal-4250850736-r4 { fill: #c5c8c6 } + .terminal-4250850736-r5 { fill: #90908a } + .terminal-4250850736-r6 { fill: #f92672 } + .terminal-4250850736-r7 { fill: #e1e1e1 } + .terminal-4250850736-r8 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  ^abc            # Matches any string that starts with "abc"                  -  2  abc$            # Matches any string that ends with "abc"                    -  3  ^abc$           # Matches the string "abc" and nothing else                  -  4  a.b             # Matches any string containing "a", any character, then "b" -  5  a[.]b           # Matches the string "a.b"                                   -  6  a|b             # Matches either "a" or "b"                                  -  7  a{2}            # Matches "aa"                                               -  8  a{2,}           # Matches two or more consecutive "a" characters             -  9  a{2,5}          # Matches between 2 and 5 consecutive "a" characters         - 10  a?              # Matches "a" or nothing (0 or 1 occurrence of "a") - 11  a*              # Matches zero or more consecutive "a" characters            - 12  a+              # Matches one or more consecutive "a" characters             - 13  \d              # Matches any digit (equivalent to [0-9]) - 14  \D              # Matches any non-digit                                      - 15  \w              # Matches any word character (equivalent to [a-zA-Z0-9_]) - 16  \W              # Matches any non-word character                             - 17  \s              # Matches any whitespace character (spaces, tabs, line break - 18  \S              # Matches any non-whitespace character                       - 19  (?i)abc         # Case-insensitive match for "abc"                           - 20  (?:a|b)         # Non-capturing group for either "a" or "b"                  - 21  (?<=a)b         # Positive lookbehind: matches "b" that is preceded by "a"   - 22  (?<!a)b         # Negative lookbehind: matches "b" that is not preceded by " - 23  a(?=b)          # Positive lookahead: matches "a" that is followed by "b"    - 24  a(?!b)          # Negative lookahead: matches "a" that is not followed by "b - 25   - + + + +  1  ^abc            # Matches any string that starts with "abc"                  +  2  abc$            # Matches any string that ends with "abc"                    +  3  ^abc$           # Matches the string "abc" and nothing else                  +  4  a.b             # Matches any string containing "a", any character, then "b" +  5  a[.]b           # Matches the string "a.b"                                   +  6  a|b             # Matches either "a" or "b"                                  +  7  a{2}            # Matches "aa"                                               +  8  a{2,}           # Matches two or more consecutive "a" characters             +  9  a{2,5}          # Matches between 2 and 5 consecutive "a" characters         + 10  a?              # Matches "a" or nothing (0 or 1 occurrence of "a") + 11  a*              # Matches zero or more consecutive "a" characters            + 12  a+              # Matches one or more consecutive "a" characters             + 13  \d              # Matches any digit (equivalent to [0-9]) + 14  \D              # Matches any non-digit                                      + 15  \w              # Matches any word character (equivalent to [a-zA-Z0-9_]) + 16  \W              # Matches any non-word character                             + 17  \s              # Matches any whitespace character (spaces, tabs, line break + 18  \S              # Matches any non-whitespace character                       + 19  (?i)abc         # Case-insensitive match for "abc"                           + 20  (?:a|b)         # Non-capturing group for either "a" or "b"                  + 21  (?<=a)b         # Positive lookbehind: matches "b" that is preceded by "a"   + 22  (?<!a)b         # Negative lookbehind: matches "b" that is not preceded by " + 23  a(?=b)          # Positive lookahead: matches "a" that is followed by "b"    + 24  a(?!b)          # Negative lookahead: matches "a" that is not followed by "b + 25   + @@ -29717,222 +29719,223 @@ font-weight: 700; } - .terminal-4125966786-matrix { + .terminal-1502683429-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4125966786-title { + .terminal-1502683429-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4125966786-r1 { fill: #e4e5e6 } - .terminal-4125966786-r2 { fill: #151515 } - .terminal-4125966786-r3 { fill: #75715e } - .terminal-4125966786-r4 { fill: #c5c8c6 } - .terminal-4125966786-r5 { fill: #86898c } - .terminal-4125966786-r6 { fill: #e2e3e3 } - .terminal-4125966786-r7 { fill: #f92672 } - .terminal-4125966786-r8 { fill: #ae81ff } - .terminal-4125966786-r9 { fill: #e6db74 } + .terminal-1502683429-r1 { fill: #c2c2bf } + .terminal-1502683429-r2 { fill: #272822 } + .terminal-1502683429-r3 { fill: #75715e } + .terminal-1502683429-r4 { fill: #f8f8f2 } + .terminal-1502683429-r5 { fill: #c5c8c6 } + .terminal-1502683429-r6 { fill: #90908a } + .terminal-1502683429-r7 { fill: #f92672 } + .terminal-1502683429-r8 { fill: #ae81ff } + .terminal-1502683429-r9 { fill: #e6db74 } + .terminal-1502683429-r10 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  -- This is a comment in SQL -  2   -  3  -- Create tables -  4  CREATETABLE Authors (                                                       -  5      AuthorID INT PRIMARY KEY,                                                -  6      Name VARCHAR(255NOT NULL,                                              -  7      Country VARCHAR(50)                                                      -  8  );                                                                           -  9   - 10  CREATETABLE Books (                                                         - 11      BookID INT PRIMARY KEY,                                                  - 12      Title VARCHAR(255NOT NULL,                                             - 13      AuthorID INT,                                                            - 14      PublishedDate DATE,                                                      - 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)                      - 16  );                                                                           - 17   - 18  -- Insert data - 19  INSERTINTO Authors (AuthorID, Name, Country) VALUES (1'George Orwell''U - 20   - 21  INSERTINTO Books (BookID, Title, AuthorID, PublishedDate) VALUES (1'1984' - 22   - 23  -- Update data - 24  UPDATE Authors SET Country ='United Kingdom'WHERE Country ='UK';          - 25   - 26  -- Select data with JOIN - 27  SELECT Books.Title, Authors.Name                                             - 28  FROM Books                                                                   - 29  JOIN Authors ON Books.AuthorID = Authors.AuthorID;                           - 30   - 31  -- Delete data (commented to preserve data for other examples) - 32  -- DELETE FROM Books WHERE BookID = 1; - 33   - 34  -- Alter table structure - 35  ALTER TABLE Authors ADD COLUMN BirthDate DATE;                               - 36   - 37  -- Create index - 38  CREATEINDEX idx_author_name ON Authors(Name);                               - 39   - 40  -- Drop index (commented to avoid actually dropping it) - 41  -- DROP INDEX idx_author_name ON Authors; - 42   - 43  -- End of script - 44   + + + +  1  -- This is a comment in SQL +  2   +  3  -- Create tables +  4  CREATETABLE Authors (                                                       +  5      AuthorID INT PRIMARY KEY,                                                +  6      Name VARCHAR(255NOT NULL,                                              +  7      Country VARCHAR(50)                                                      +  8  );                                                                           +  9   + 10  CREATETABLE Books (                                                         + 11      BookID INT PRIMARY KEY,                                                  + 12      Title VARCHAR(255NOT NULL,                                             + 13      AuthorID INT,                                                            + 14      PublishedDate DATE,                                                      + 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)                      + 16  );                                                                           + 17   + 18  -- Insert data + 19  INSERTINTO Authors (AuthorID, Name, Country) VALUES (1'George Orwell''U + 20   + 21  INSERTINTO Books (BookID, Title, AuthorID, PublishedDate) VALUES (1'1984' + 22   + 23  -- Update data + 24  UPDATE Authors SET Country ='United Kingdom'WHERE Country ='UK';          + 25   + 26  -- Select data with JOIN + 27  SELECT Books.Title, Authors.Name                                             + 28  FROM Books                                                                   + 29  JOIN Authors ON Books.AuthorID = Authors.AuthorID;                           + 30   + 31  -- Delete data (commented to preserve data for other examples) + 32  -- DELETE FROM Books WHERE BookID = 1; + 33   + 34  -- Alter table structure + 35  ALTER TABLE Authors ADD COLUMN BirthDate DATE;                               + 36   + 37  -- Create index + 38  CREATEINDEX idx_author_name ON Authors(Name);                               + 39   + 40  -- Drop index (commented to avoid actually dropping it) + 41  -- DROP INDEX idx_author_name ON Authors; + 42   + 43  -- End of script + 44   @@ -29963,151 +29966,151 @@ font-weight: 700; } - .terminal-1180609356-matrix { + .terminal-579508414-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1180609356-title { + .terminal-579508414-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1180609356-r1 { fill: #e4e5e6 } - .terminal-1180609356-r2 { fill: #151515 } - .terminal-1180609356-r3 { fill: #75715e } - .terminal-1180609356-r4 { fill: #c5c8c6 } - .terminal-1180609356-r5 { fill: #86898c } - .terminal-1180609356-r6 { fill: #e2e3e3 } - .terminal-1180609356-r7 { fill: #f92672 } - .terminal-1180609356-r8 { fill: #e6db74 } - .terminal-1180609356-r9 { fill: #ae81ff } - .terminal-1180609356-r10 { fill: #66d9ef;font-style: italic; } + .terminal-579508414-r1 { fill: #c2c2bf } + .terminal-579508414-r2 { fill: #272822 } + .terminal-579508414-r3 { fill: #75715e } + .terminal-579508414-r4 { fill: #f8f8f2 } + .terminal-579508414-r5 { fill: #c5c8c6 } + .terminal-579508414-r6 { fill: #90908a } + .terminal-579508414-r7 { fill: #f92672 } + .terminal-579508414-r8 { fill: #e6db74 } + .terminal-579508414-r9 { fill: #ae81ff } + .terminal-579508414-r10 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  # This is a comment in TOML -  2   -  3  string = "Hello, world!" -  4  integer = 42 -  5  float = 3.14 -  6  boolean = true -  7  datetime = 1979-05-27T07:32:00Z -  8   -  9  fruits = ["apple""banana""cherry" - 10   - 11  [address]                               - 12  street = "123 Main St" - 13  city = "Anytown" - 14  state = "CA" - 15  zip = "12345" - 16   - 17  [person.john]                           - 18  name = "John Doe" - 19  age = 28 - 20  is_student = false - 21   - 22   - 23  [[animals]]                             - 24  name = "Fido" - 25  type = "dog" - 26   + + + +  1  # This is a comment in TOML +  2   +  3  string = "Hello, world!" +  4  integer = 42 +  5  float = 3.14 +  6  boolean = true +  7  datetime = 1979-05-27T07:32:00Z +  8   +  9  fruits = ["apple""banana""cherry" + 10   + 11  [address]                               + 12  street = "123 Main St" + 13  city = "Anytown" + 14  state = "CA" + 15  zip = "12345" + 16   + 17  [person.john]                           + 18  name = "John Doe" + 19  age = 28 + 20  is_student = false + 21   + 22   + 23  [[animals]]                             + 24  name = "Fido" + 25  type = "dog" + 26   @@ -30138,199 +30141,199 @@ font-weight: 700; } - .terminal-1644508950-matrix { + .terminal-575800710-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1644508950-title { + .terminal-575800710-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1644508950-r1 { fill: #e4e5e6 } - .terminal-1644508950-r2 { fill: #151515 } - .terminal-1644508950-r3 { fill: #75715e } - .terminal-1644508950-r4 { fill: #c5c8c6 } - .terminal-1644508950-r5 { fill: #86898c } - .terminal-1644508950-r6 { fill: #e2e3e3 } - .terminal-1644508950-r7 { fill: #f92672;font-weight: bold } - .terminal-1644508950-r8 { fill: #e6db74 } - .terminal-1644508950-r9 { fill: #ae81ff } - .terminal-1644508950-r10 { fill: #66d9ef;font-style: italic; } + .terminal-575800710-r1 { fill: #c2c2bf } + .terminal-575800710-r2 { fill: #272822 } + .terminal-575800710-r3 { fill: #75715e } + .terminal-575800710-r4 { fill: #f8f8f2 } + .terminal-575800710-r5 { fill: #c5c8c6 } + .terminal-575800710-r6 { fill: #90908a } + .terminal-575800710-r7 { fill: #f92672;font-weight: bold } + .terminal-575800710-r8 { fill: #e6db74 } + .terminal-575800710-r9 { fill: #ae81ff } + .terminal-575800710-r10 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  # This is a comment in YAML -  2   -  3  # Scalars -  4  string"Hello, world!" -  5  integer42 -  6  float3.14 -  7  booleantrue -  8   -  9  # Sequences (Arrays) - 10  fruits:                                               - 11    - Apple - 12    - Banana - 13    - Cherry - 14   - 15  # Nested sequences - 16  persons:                                              - 17    - nameJohn - 18  age28 - 19  is_studentfalse - 20    - nameJane - 21  age22 - 22  is_studenttrue - 23   - 24  # Mappings (Dictionaries) - 25  address:                                              - 26  street123 Main St - 27  cityAnytown - 28  stateCA - 29  zip'12345' - 30   - 31  # Multiline string - 32  description| - 33    This is a multiline - 34    string in YAML. - 35   - 36  # Inline and nested collections - 37  colors: { redFF0000green00FF00blue0000FF }  - 38   + + + +  1  # This is a comment in YAML +  2   +  3  # Scalars +  4  string"Hello, world!" +  5  integer42 +  6  float3.14 +  7  booleantrue +  8   +  9  # Sequences (Arrays) + 10  fruits:                                               + 11    - Apple + 12    - Banana + 13    - Cherry + 14   + 15  # Nested sequences + 16  persons:                                              + 17    - nameJohn + 18  age28 + 19  is_studentfalse + 20    - nameJane + 21  age22 + 22  is_studenttrue + 23   + 24  # Mappings (Dictionaries) + 25  address:                                              + 26  street123 Main St + 27  cityAnytown + 28  stateCA + 29  zip'12345' + 30   + 31  # Multiline string + 32  description|                                        + 33    This is a multiline                                 + 34    string in YAML. + 35   + 36  # Inline and nested collections + 37  colors: { redFF0000green00FF00blue0000FF }  + 38   @@ -30361,60 +30364,58 @@ font-weight: 700; } - .terminal-488877296-matrix { + .terminal-1544890574-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-488877296-title { + .terminal-1544890574-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-488877296-r1 { fill: #e2e3e3 } - .terminal-488877296-r2 { fill: #dde6ed } - .terminal-488877296-r3 { fill: #c5c8c6 } - .terminal-488877296-r4 { fill: #004578 } - .terminal-488877296-r5 { fill: #151515 } - .terminal-488877296-r6 { fill: #e4e5e6 } + .terminal-1544890574-r1 { fill: #f8f8f2 } + .terminal-1544890574-r2 { fill: #c5c8c6 } + .terminal-1544890574-r3 { fill: #65686a } + .terminal-1544890574-r4 { fill: #272822 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - I am a line. - - I am another line.             - - I am the final line.  + + + + I am a line. + ▌                     + I am another line.             + + I am the final line.  @@ -30444,60 +30445,58 @@ font-weight: 700; } - .terminal-291251717-matrix { + .terminal-1447338365-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-291251717-title { + .terminal-1447338365-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-291251717-r1 { fill: #e2e3e3 } - .terminal-291251717-r2 { fill: #151515 } - .terminal-291251717-r3 { fill: #dde6ed } - .terminal-291251717-r4 { fill: #e4e5e6 } - .terminal-291251717-r5 { fill: #c5c8c6 } - .terminal-291251717-r6 { fill: #004578 } + .terminal-1447338365-r1 { fill: #f8f8f2 } + .terminal-1447338365-r2 { fill: #272822 } + .terminal-1447338365-r3 { fill: #c5c8c6 } + .terminal-1447338365-r4 { fill: #65686a } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - I am a line. - - I am another line.    - - I am the final line.  + + + + I am a line. + ▌                     + I am another line.    + + I am the final line.  @@ -30527,60 +30526,58 @@ font-weight: 700; } - .terminal-1080619275-matrix { + .terminal-4280380793-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1080619275-title { + .terminal-4280380793-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1080619275-r1 { fill: #e2e3e3 } - .terminal-1080619275-r2 { fill: #151515 } - .terminal-1080619275-r3 { fill: #dde6ed } - .terminal-1080619275-r4 { fill: #e4e5e6 } - .terminal-1080619275-r5 { fill: #c5c8c6 } - .terminal-1080619275-r6 { fill: #004578 } + .terminal-4280380793-r1 { fill: #f8f8f2 } + .terminal-4280380793-r2 { fill: #272822 } + .terminal-4280380793-r3 { fill: #c5c8c6 } + .terminal-4280380793-r4 { fill: #65686a } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - I am a line. - - I am another line. - - I am the final line.  + + + + I am a line. + ▌                     + I am another line. + ▌                     + I am the final line.  @@ -30610,60 +30607,58 @@ font-weight: 700; } - .terminal-3301472261-matrix { + .terminal-154203323-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3301472261-title { + .terminal-154203323-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3301472261-r1 { fill: #e2e3e3 } - .terminal-3301472261-r2 { fill: #dde6ed } - .terminal-3301472261-r3 { fill: #c5c8c6 } - .terminal-3301472261-r4 { fill: #004578 } - .terminal-3301472261-r5 { fill: #151515 } - .terminal-3301472261-r6 { fill: #e4e5e6 } + .terminal-154203323-r1 { fill: #f8f8f2 } + .terminal-154203323-r2 { fill: #c5c8c6 } + .terminal-154203323-r3 { fill: #65686a } + .terminal-154203323-r4 { fill: #272822 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - I am a line. - - I am another line. - - I am the final line. + + + + I am a line. + ▌                     + I am another line. + ▌                     + I am the final line. @@ -30693,58 +30688,57 @@ font-weight: 700; } - .terminal-2211880532-matrix { + .terminal-2079201489-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2211880532-title { + .terminal-2079201489-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2211880532-r1 { fill: #e2e3e3 } - .terminal-2211880532-r2 { fill: #c5c8c6 } - .terminal-2211880532-r3 { fill: #151515 } - .terminal-2211880532-r4 { fill: #e4e5e6 } + .terminal-2079201489-r1 { fill: #f8f8f2 } + .terminal-2079201489-r2 { fill: #c5c8c6 } + .terminal-2079201489-r3 { fill: #272822 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - I am a line.          - - I am another line.    - - I am the final line.  + + + + I am a line.          + + I am another line.    + + I am the final line.  @@ -30774,58 +30768,57 @@ font-weight: 700; } - .terminal-2103108078-matrix { + .terminal-2869124204-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2103108078-title { + .terminal-2869124204-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2103108078-r1 { fill: #e2e3e3 } - .terminal-2103108078-r2 { fill: #c5c8c6 } - .terminal-2103108078-r3 { fill: #e4e5e6 } - .terminal-2103108078-r4 { fill: #151515 } + .terminal-2869124204-r1 { fill: #f8f8f2 } + .terminal-2869124204-r2 { fill: #c5c8c6 } + .terminal-2869124204-r3 { fill: #272822 } - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - I am a line.          - - I am another line.             - - I am the final line.  + + + + I am a line.          + + I am another line.             + + I am the final line.  @@ -30855,62 +30848,60 @@ font-weight: 700; } - .terminal-3933483729-matrix { + .terminal-3657281873-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3933483729-title { + .terminal-3657281873-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3933483729-r1 { fill: #6f7173 } - .terminal-3933483729-r2 { fill: #dfe5e9 } - .terminal-3933483729-r3 { fill: #e2e2e3 } - .terminal-3933483729-r4 { fill: #c5c8c6 } - .terminal-3933483729-r5 { fill: #0b395c } - .terminal-3933483729-r6 { fill: #b7b8ba } - .terminal-3933483729-r7 { fill: #080909 } - .terminal-3933483729-r8 { fill: #e3e3e4 } + .terminal-3657281873-r1 { fill: #90908a } + .terminal-3657281873-r2 { fill: #f8f8f2 } + .terminal-3657281873-r3 { fill: #c5c8c6 } + .terminal-3657281873-r4 { fill: #65686a } + .terminal-3657281873-r5 { fill: #c2c2bf } + .terminal-3657281873-r6 { fill: #272822 } - + - + - + - + - + - TextAreaUnfocusSnapshot + TextAreaUnfocusSnapshot - - - - 1  I am a line. - 2   - 3      I amanother line.      - 4   - 5      I am the final line.  + + + + 1  I am a line. + 2  ▌                         + 3      I amanother line.      + 4   + 5      I am the final line.  diff --git a/tree-sitter/highlights/python.scm b/tree-sitter/highlights/python.scm index 8778026979..37aceef1fd 100644 --- a/tree-sitter/highlights/python.scm +++ b/tree-sitter/highlights/python.scm @@ -292,7 +292,7 @@ ;; Class definitions -(class_definition name: (identifier) @type) +(class_definition name: (identifier) @type.class) (class_definition body: (block From 910dec71d72c3ab07365c15264dfc9e0ebd9a174 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 6 Sep 2023 11:48:39 +0100 Subject: [PATCH 308/366] Remove old comment, fix docstring --- src/textual/document/_text_area_theme.py | 53 +++--------------------- 1 file changed, 5 insertions(+), 48 deletions(-) diff --git a/src/textual/document/_text_area_theme.py b/src/textual/document/_text_area_theme.py index 6055a85d17..d7455c09a2 100644 --- a/src/textual/document/_text_area_theme.py +++ b/src/textual/document/_text_area_theme.py @@ -11,7 +11,10 @@ @dataclass class TextAreaTheme: - """Maps tree-sitter names to Rich styles for syntax-highlighting in `TextArea`. + """A theme for the `TextArea` widget. + + Allows theming the general widget (gutter, selections, cursor, and so on) and + mapping of tree-sitter tokens to Rich styles. For example, consider the following snippet from the `markdown.scm` highlight query file. We've assigned the `heading_content` token type to the name `heading`. @@ -25,7 +28,7 @@ class TextAreaTheme: node is used (as will be the case when language="markdown"): ``` - SyntaxTheme('my_theme', {'heading': Style(color='cyan', bold=True)}) + TextAreaThe('my_theme', {'heading': Style(color='cyan', bold=True)}) ``` """ @@ -238,52 +241,6 @@ def default(cls) -> TextAreaTheme: }, ) -# _DRACULA = { -# "string": Style(color="#f1fa8c"), -# "string.documentation": Style(color="#f1fa8c"), -# "comment": Style(color="#6272a4"), -# "keyword": Style(color="#ff79c6"), -# "operator": Style(color="#ff79c6"), -# "repeat": Style(color="#ff79c6"), -# "exception": Style(color="#ff79c6"), -# "include": Style(color="#ff79c6"), -# "keyword.function": Style(color="#ff79c6"), -# "keyword.return": Style(color="#ff79c6"), -# "keyword.operator": Style(color="#ff79c6"), -# "conditional": Style(color="#ff79c6"), -# "number": Style(color="#bd93f9"), -# "float": Style(color="#bd93f9"), -# "class": Style(color="#50fa7b"), -# "function": Style(color="#50fa7b"), -# "function.call": Style(color="#50fa7b"), -# "method": Style(color="#50fa7b"), -# "method.call": Style(color="#50fa7b"), -# "boolean": Style(color="#bd93f9"), -# "json.null": Style(color="#bd93f9"), -# # "constant": Style(color="#bd93f9"), -# # "variable": Style(color="white"), -# # "parameter": Style(color="cyan"), -# # "type": Style(color="cyan"), -# # "escape": Style(bgcolor="magenta"), -# "regex.punctuation.bracket": Style(color="#ff79c6"), -# "regex.operator": Style(color="#ff79c6"), -# # "error": Style(color="black", bgcolor="red"), -# "json.error": _NULL_STYLE, -# "html.end_tag_error": Style(color="#F83333", underline=True), -# "tag": Style(color="#ff79c6"), -# "yaml.field": Style(color="#ff79c6", bold=True), -# "json.label": Style(color="#ff79c6", bold=True), -# "toml.type": Style(color="#ff79c6"), -# "toml.datetime": Style(color="#bd93f9"), -# "toml.error": _NULL_STYLE, -# "heading": Style(color="#ff79c6", bold=True), -# "bold": Style(bold=True), -# "italic": Style(italic=True), -# "strikethrough": Style(strike=True), -# "link": Style(color="#bd93f9", underline=True), -# "inline_code": Style(color="#ff79c6"), -# } - _BUILTIN_THEMES = { "monokai": _MONOKAI, "dracula": _DRACULA, From 33d8d26a26cee0b2e6943d3442e9883294ea615b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 6 Sep 2023 12:42:58 +0100 Subject: [PATCH 309/366] Fixing docstrings --- src/textual/document/_text_area_theme.py | 21 ++++++++++++--------- src/textual/widgets/_text_area.py | 24 ++++++++++++++++-------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/textual/document/_text_area_theme.py b/src/textual/document/_text_area_theme.py index d7455c09a2..94ed4c5614 100644 --- a/src/textual/document/_text_area_theme.py +++ b/src/textual/document/_text_area_theme.py @@ -25,11 +25,14 @@ class TextAreaTheme: Now, we can map this `heading` name to a Rich style, and it will be styled as such in the `TextArea`, assuming a parser which returns a `heading_content` - node is used (as will be the case when language="markdown"): + node is used (as will be the case when language="markdown"). ``` - TextAreaThe('my_theme', {'heading': Style(color='cyan', bold=True)}) + TextAreaTheme('my_theme', token_styles={'heading': Style(color='cyan', bold=True)}) ``` + + We can supply this theme to our `TextArea`, and headings in our markdown files will + be styled bold cyan. """ name: str | None = None @@ -98,17 +101,17 @@ def __post_init__(self) -> None: @classmethod def get_by_name(cls, theme_name: str) -> "TextAreaTheme": - """Get a `SyntaxTheme` by name. + """Get a `TextAreaTheme` by name. - Given a `theme_name` return the corresponding `SyntaxTheme` object. + Given a `theme_name` return the corresponding `TextAreaTheme` object. - Check the available `SyntaxTheme`s by calling `SyntaxTheme.available_themes()`. + Check the available `TextAreaTheme`s by calling `TextAreaTheme.available_themes()`. Args: theme_name: The name of the theme. Returns: - The `SyntaxTheme` corresponding to the name. + The `TextAreaTheme` corresponding to the name. """ return _BUILTIN_THEMES.get(theme_name, TextAreaTheme()) @@ -126,10 +129,10 @@ def get_highlight(self, name: str) -> Style: @classmethod def available_themes(cls) -> list[TextAreaTheme]: - """Get a list of all available SyntaxThemes. + """Get a list of all available TextAreaThemes. Returns: - A list of all available SyntaxThemes. + A list of all available TextAreaThemes. """ return list(_BUILTIN_THEMES.values()) @@ -138,7 +141,7 @@ def default(cls) -> TextAreaTheme: """Get the default syntax theme. Returns: - The default SyntaxTheme (probably Monokai). + The default TextAreaTheme (probably "monokai"). """ return DEFAULT_SYNTAX_THEME diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 01f5efdf40..15300db218 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -42,7 +42,6 @@ _TREE_SITTER_PATH = Path(__file__) / "../../../../tree-sitter/" _HIGHLIGHTS_PATH = _TREE_SITTER_PATH / "highlights/" - StartColumn = int EndColumn = Optional[int] HighlightName = str @@ -167,7 +166,7 @@ class TextArea(ScrollView, can_focus=True): it first using `register_language`. """ - theme: Reactive[str | TextAreaTheme] = reactive( + theme: Reactive[str | TextAreaTheme | None] = reactive( TextAreaTheme.default(), always_update=True, init=False ) """The theme to syntax highlight with. @@ -226,6 +225,9 @@ def __init__( """Construct a new `TextArea`. Args: + text: The initial text to load into the TextArea. + language: The language to use. + theme: The theme to use. name: The name of the `TextArea` widget. id: The ID of the widget, used to refer to it from Textual CSS. classes: One or more Textual CSS compatible class names separated by spaces. @@ -265,7 +267,7 @@ def __init__( self._highlight_query: "Query" | None = None """The query that's currently being used for highlighting.""" - self.document: DocumentBase | None = None + self.document: DocumentBase = Document(text) """The document this widget is currently editing.""" self.language = language @@ -273,10 +275,19 @@ def __init__( if isinstance(theme, str): theme = TextAreaTheme.get_by_name(theme) - self.theme: TextAreaTheme = theme + self.theme: TextAreaTheme | None = theme """The theme of the `TextArea`.""" - def _get_builtin_highlight_query(self, language_name: str) -> str: + @staticmethod + def _get_builtin_highlight_query(language_name: str) -> str: + """Get the highlight query for a builtin language. + + Args: + language_name: The name of the builtin language. + + Returns: + The highlight query. + """ try: highlight_query_path = ( Path(_HIGHLIGHTS_PATH.resolve()) / f"{language_name}.scm" @@ -289,12 +300,9 @@ def _get_builtin_highlight_query(self, language_name: str) -> str: def _build_highlight_map(self) -> None: """Query the tree for ranges to highlights, and update the internal highlights mapping.""" - - print("building highlight map") highlights = self._highlights highlights.clear() if not self._highlight_query: - print("no highlight query") return captures = self.document.query_syntax_tree(self._highlight_query) From 5f0aedf1bcb4baa1623bfdbe0523486d9882b50d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 6 Sep 2023 15:48:46 +0100 Subject: [PATCH 310/366] Fixing mypy --- src/textual/widgets/_text_area.py | 137 ++++++++++++++++-------------- 1 file changed, 74 insertions(+), 63 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 15300db218..d239fd39d9 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -53,7 +53,7 @@ class TextAreaLanguage: name: str language: "Language" - highlight_query: str | "Query" + highlight_query: str class TextArea(ScrollView, can_focus=True): @@ -271,12 +271,12 @@ def __init__( """The document this widget is currently editing.""" self.language = language + """The language of the `TextArea`.""" - if isinstance(theme, str): - theme = TextAreaTheme.get_by_name(theme) - - self.theme: TextAreaTheme | None = theme - """The theme of the `TextArea`.""" + self.theme = theme + """The theme of the `TextArea` as set by the user.""" + self._theme: TextAreaTheme | None = None + """The theme that is actually being used.""" @staticmethod def _get_builtin_highlight_query(language_name: str) -> str: @@ -327,10 +327,6 @@ def _build_highlight_map(self) -> None: # Add the last line of the node range highlights[node_end_row].append((0, node_end_column, highlight_name)) - def _watch_theme(self) -> None: - """When the theme changes, update the highlight map""" - self._build_highlight_map() - def _watch_selection(self, selection: Selection) -> None: """When the cursor moves, scroll it into view.""" self.scroll_cursor_visible() @@ -405,6 +401,10 @@ def _watch_indent_width(self) -> None: """Changing width of tabs will change document display width.""" self._refresh_size() + def _watch_theme(self) -> None: + """When the theme changes, update the highlight map""" + self._build_highlight_map() + def _validate_theme(self, theme: str | TextAreaTheme) -> TextAreaTheme: if isinstance(theme, str): theme = TextAreaTheme.get_by_name(theme) @@ -458,12 +458,14 @@ def _set_document(self, text: str, language: str | None) -> None: if TREE_SITTER and language: # Attempt to get the override language. text_area_language = self._languages.get(language, None) + document_language: str | "Language" if text_area_language: document_language = text_area_language.language highlight_query = text_area_language.highlight_query else: document_language = language highlight_query = self._get_builtin_highlight_query(language) + document: DocumentBase try: document = SyntaxAwareDocument(text, document_language) except RuntimeError: @@ -580,7 +582,7 @@ def render_line(self, widget_y: int) -> Strip: if out_of_bounds: return Strip.blank(self.size.width) - theme = self.theme + theme = self._theme # Get the line from the Document. line_string = document.get_line(line_index) @@ -592,87 +594,92 @@ def render_line(self, widget_y: int) -> Strip: expanded_length = max(virtual_width, self.size.width) line.set_length(expanded_length) + selection = self.selection + start, end = selection + selection_top, selection_bottom = selection.range + selection_top_row, selection_top_column = selection_top + selection_bottom_row, selection_bottom_column = selection_bottom + highlights = self._highlights - if highlights: + if highlights and theme: line_bytes = _utf8_encode(line_string) byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) - get_highlight_from_theme = self.theme.token_styles.get + get_highlight_from_theme = theme.token_styles.get line_highlights = highlights[line_index] - for start, end, highlight_name in line_highlights: + for highlight_start, highlight_end, highlight_name in line_highlights: node_style = get_highlight_from_theme(highlight_name) if node_style is not None: line.stylize( node_style, - byte_to_codepoint.get(start, 0), - byte_to_codepoint.get(end) if end else None, + byte_to_codepoint.get(highlight_start, 0), + byte_to_codepoint.get(highlight_end) if highlight_end else None, ) - selection = self.selection - start, end = selection - selection_top, selection_bottom = selection.range - selection_top_row, selection_top_column = selection_top - selection_bottom_row, selection_bottom_column = selection_bottom - - matching_bracket = self._matching_bracket_location - match_cursor_bracket = self.match_cursor_bracket - draw_matched_brackets = match_cursor_bracket and matching_bracket is not None - cursor_row, cursor_column = end - cursor_line_style = theme.cursor_line_style - if cursor_row == line_index: + cursor_line_style = theme.cursor_line_style if theme else None + if cursor_line_style and cursor_row == line_index: line.stylize(cursor_line_style) # Selection styling if start != end and selection_top_row <= line_index <= selection_bottom_row: # If this row intersects with the selection range - selection_style = theme.selection_style + selection_style = theme.selection_style if theme else None cursor_row, _ = end - if line_character_count == 0 and line_index != cursor_row: - # A simple highlight to show empty lines are included in the selection - line = Text("▌", end="", style=Style(color=selection_style.bgcolor)) - line.set_length(self.virtual_size.width) - else: - if line_index == selection_top_row == selection_bottom_row: - # Selection within a single line - line.stylize( - selection_style, - start=selection_top_column, - end=selection_bottom_column, - ) + if selection_style: + if line_character_count == 0 and line_index != cursor_row: + # A simple highlight to show empty lines are included in the selection + line = Text("▌", end="", style=Style(color=selection_style.bgcolor)) + line.set_length(self.virtual_size.width) else: - # Selection spanning multiple lines - if line_index == selection_top_row: + if line_index == selection_top_row == selection_bottom_row: + # Selection within a single line line.stylize( selection_style, start=selection_top_column, - end=line_character_count, + end=selection_bottom_column, ) - elif line_index == selection_bottom_row: - line.stylize(selection_style, end=selection_bottom_column) else: - line.stylize(selection_style, end=line_character_count) + # Selection spanning multiple lines + if line_index == selection_top_row: + line.stylize( + selection_style, + start=selection_top_column, + end=line_character_count, + ) + elif line_index == selection_bottom_row: + line.stylize(selection_style, end=selection_bottom_column) + else: + line.stylize(selection_style, end=line_character_count) # Highlight the cursor + matching_bracket = self._matching_bracket_location + match_cursor_bracket = self.match_cursor_bracket + draw_matched_brackets = match_cursor_bracket and matching_bracket is not None + if cursor_row == line_index: draw_cursor = not self.cursor_blink or ( self.cursor_blink and self._cursor_blink_visible ) if draw_matched_brackets: - matching_bracket_style = theme.bracket_matching_style - line.stylize( - matching_bracket_style, - cursor_column, - cursor_column + 1, - ) + matching_bracket_style = theme.bracket_matching_style if theme else None + if matching_bracket_style: + line.stylize( + matching_bracket_style, + cursor_column, + cursor_column + 1, + ) if draw_cursor: - cursor_style = theme.cursor_style - line.stylize(cursor_style, cursor_column, cursor_column + 1) + cursor_style = theme.cursor_style if theme else None + if cursor_style: + line.stylize(cursor_style, cursor_column, cursor_column + 1) # Highlight the partner opening/closing bracket. if draw_matched_brackets: + # mypy doesn't know matching bracket is guaranteed to be non-None + assert matching_bracket is not None bracket_match_row, bracket_match_column = matching_bracket - if bracket_match_row == line_index: + if theme and bracket_match_row == line_index: matching_bracket_style = theme.bracket_matching_style if matching_bracket_style: line.stylize( @@ -685,14 +692,14 @@ def render_line(self, widget_y: int) -> Strip: gutter_width = self.gutter_width if self.show_line_numbers: if cursor_row == line_index: - gutter_style = theme.cursor_line_gutter_style + gutter_style = theme.cursor_line_gutter_style if theme else None else: - gutter_style = theme.gutter_style + gutter_style = theme.gutter_style if theme else None gutter_width_no_margin = gutter_width - 2 gutter = Text( f"{line_index + 1:>{gutter_width_no_margin}} ", - style=gutter_style, + style=gutter_style or "", end="", ) else: @@ -713,19 +720,23 @@ def render_line(self, widget_y: int) -> Strip: ) # Stylize the line the cursor is currently on. - # TODO - is this required or have we already expanded it? if cursor_row == line_index: text_strip = text_strip.extend_cell_length( - expanded_length, theme.cursor_line_style + expanded_length, cursor_line_style ) else: text_strip = text_strip.extend_cell_length( - expanded_length, theme.base_style + expanded_length, theme.base_style if theme else None ) # Join and return the gutter and the visible portion of this line strip = Strip.join([gutter_strip, text_strip]).simplify() - return strip.apply_style(theme.base_style) + + return strip.apply_style( + theme.base_style + if theme and theme.base_style is not None + else self.rich_style + ) @property def text(self) -> str: From 0d2cd85ff451567d175513fb65fedc87e8b44bd7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 6 Sep 2023 16:37:46 +0100 Subject: [PATCH 311/366] Fixing mypy issues in document --- src/textual/document/_document.py | 7 --- .../document/_syntax_aware_document.py | 45 ++++++++++++------- src/textual/widgets/_text_area.py | 2 +- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 52b699aa39..3eb0ad11cf 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -370,10 +370,3 @@ def is_empty(self) -> bool: """Return True if the selection has 0 width, i.e. it's just a cursor.""" start, end = self return start == end - - @property - def range(self) -> tuple[Location, Location]: - """Return the Selection as a "standard" range, from top to bottom i.e. (minimum point, maximum point) - where the minimum point is inclusive and the maximum point is exclusive.""" - start, end = self - return tuple(sorted((start, end))) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 1cad36b973..832accdb26 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -1,9 +1,7 @@ from __future__ import annotations -from typing import Any - try: - from tree_sitter import Language, Parser, Tree + from tree_sitter import Language, Node, Parser, Tree from tree_sitter.binding import Query from tree_sitter_languages import get_language, get_parser @@ -16,6 +14,12 @@ from textual.document._text_area_theme import TextAreaTheme +class SyntaxAwareDocumentError(Exception): + """General error raised when SyntaxAwareDocument is used incorrectly.""" + + pass + + class SyntaxAwareDocument(Document): """A wrapper around a Document which also maintains a tree-sitter syntax tree when the document is edited. @@ -53,16 +57,13 @@ def __init__( self._parser: Parser | None = None """The tree-sitter Parser or None if tree-sitter is unavailable.""" - self._syntax_tree: Tree | None = None - """The tree-sitter Tree (syntax tree) built from the document.""" - self._syntax_theme: TextAreaTheme | None = None """The syntax highlighting theme to use.""" # If the language is `None`, then avoid doing any parsing related stuff. if isinstance(language, str): if language not in VALID_LANGUAGES: - raise RuntimeError(f"Invalid language {language!r}") + raise SyntaxAwareDocumentError(f"Invalid language {language!r}") self.language = get_language(language) self._parser = get_parser(language) else: @@ -70,31 +71,45 @@ def __init__( self._parser = Parser() self._parser.set_language(language) - self._syntax_tree = self._parser.parse(self._read_callable) # type: ignore + self._syntax_tree: Tree = self._parser.parse(self._read_callable) # type: ignore + """The tree-sitter Tree (syntax tree) built from the document.""" @property def language_name(self) -> str | None: return self.language.name if self.language else None def prepare_query(self, query: str) -> Query | None: - if TREE_SITTER: - prepared_query = self.language.query(query) - else: - prepared_query = None - return prepared_query + if not TREE_SITTER: + raise SyntaxAwareDocumentError( + "Couldn't prepare query - tree-sitter is not available on this architecture." + ) + + if self.language is None: + raise SyntaxAwareDocumentError( + "Couldn't prepare query - no language assigned." + ) + + return self.language.query(query) def query_syntax_tree( self, query: Query, start_point: tuple[int, int] | None = None, end_point: tuple[int, int] | None = None, - ) -> Any: + ) -> list[tuple["Node", str]]: + if not TREE_SITTER: + raise SyntaxAwareDocumentError( + "tree-sitter is not available on this architecture." + ) + captures_kwargs = {} if start_point is not None: captures_kwargs["start_point"] = start_point if end_point is not None: captures_kwargs["end_point"] = end_point - return query.captures(self._syntax_tree.root_node, **captures_kwargs) + + captures = query.captures(self._syntax_tree.root_node, **captures_kwargs) + return captures def replace_range(self, start: Location, end: Location, text: str) -> EditResult: """Replace text at the given range. diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index d239fd39d9..fd2f9e7fc7 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -596,7 +596,7 @@ def render_line(self, widget_y: int) -> Strip: selection = self.selection start, end = selection - selection_top, selection_bottom = selection.range + selection_top, selection_bottom = sorted(selection) selection_top_row, selection_top_column = selection_top selection_bottom_row, selection_bottom_column = selection_bottom From a2ea1f64761471105625140b315c3d4ae06847f1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 6 Sep 2023 17:04:48 +0100 Subject: [PATCH 312/366] Tidying things --- src/textual/widgets/_text_area.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index fd2f9e7fc7..c1e3345dcc 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -267,17 +267,15 @@ def __init__( self._highlight_query: "Query" | None = None """The query that's currently being used for highlighting.""" - self.document: DocumentBase = Document(text) + self.document: DocumentBase | None = None """The document this widget is currently editing.""" + self.theme: TextAreaTheme | None = theme + """The theme of the `TextArea` as set by the user.""" + self.language = language """The language of the `TextArea`.""" - self.theme = theme - """The theme of the `TextArea` as set by the user.""" - self._theme: TextAreaTheme | None = None - """The theme that is actually being used.""" - @staticmethod def _get_builtin_highlight_query(language_name: str) -> str: """Get the highlight query for a builtin language. @@ -401,10 +399,6 @@ def _watch_indent_width(self) -> None: """Changing width of tabs will change document display width.""" self._refresh_size() - def _watch_theme(self) -> None: - """When the theme changes, update the highlight map""" - self._build_highlight_map() - def _validate_theme(self, theme: str | TextAreaTheme) -> TextAreaTheme: if isinstance(theme, str): theme = TextAreaTheme.get_by_name(theme) @@ -479,6 +473,7 @@ def _set_document(self, text: str, language: str | None) -> None: document = Document(text) self.document = document + log.debug(f"setting document: {document!r}") self._build_highlight_map() @property @@ -582,7 +577,7 @@ def render_line(self, widget_y: int) -> Strip: if out_of_bounds: return Strip.blank(self.size.width) - theme = self._theme + theme = self.theme # Get the line from the Document. line_string = document.get_line(line_index) From 46ab8ca10cc71ce7d140ad5f378ecbfe225c0a38 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 6 Sep 2023 17:10:29 +0100 Subject: [PATCH 313/366] Updating version --- poetry.lock | 350 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 219 insertions(+), 131 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9edbad9711..02dbfb24e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -151,14 +151,14 @@ trio = ["trio (<0.22)"] [[package]] name = "async-timeout" -version = "4.0.2" +version = "4.0.3" description = "Timeout context manager for asyncio programs" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] [package.dependencies] @@ -198,6 +198,21 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope-interface"] tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +[[package]] +name = "babel" +version = "2.12.1" +description = "Internationalization utilities" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, + {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, +] + +[package.dependencies] +pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} + [[package]] name = "black" version = "23.3.0" @@ -372,14 +387,14 @@ files = [ [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -496,14 +511,14 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.1.2" +version = "1.1.3" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, - {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, ] [package.extras] @@ -644,14 +659,14 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.32" +version = "3.1.34" description = "GitPython is a Python library used to interact with Git repositories" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.32-py3-none-any.whl", hash = "sha256:e3d59b1c2c6ebb9dfa7a184daf3b6dd4914237e7488a1730a6d8f6f5d0b4187f"}, - {file = "GitPython-3.1.32.tar.gz", hash = "sha256:8d9b8cb1e80b9735e8717c9362079d3ce4c6e5ddeebedd0361b228c3a67a62f6"}, + {file = "GitPython-3.1.34-py3-none-any.whl", hash = "sha256:5d3802b98a3bae1c2b8ae0e1ff2e4aa16bcdf02c145da34d092324f599f01395"}, + {file = "GitPython-3.1.34.tar.gz", hash = "sha256:85f7d365d1f6bf677ae51039c1ef67ca59091c7ebd5a3509aa399d4eda02d6dd"}, ] [package.dependencies] @@ -1050,26 +1065,28 @@ mkdocs = "*" [[package]] name = "mkdocs-material" -version = "9.1.21" +version = "9.2.7" description = "Documentation that simply works" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mkdocs_material-9.1.21-py3-none-any.whl", hash = "sha256:58bb2f11ef240632e176d6f0f7d1cff06be1d11c696a5a1b553b808b4280ed47"}, - {file = "mkdocs_material-9.1.21.tar.gz", hash = "sha256:71940cdfca84ab296b6362889c25395b1621273fb16c93deda257adb7ff44ec8"}, + {file = "mkdocs_material-9.2.7-py3-none-any.whl", hash = "sha256:92e4160d191cc76121fed14ab9f14638e43a6da0f2e9d7a9194d377f0a4e7f18"}, + {file = "mkdocs_material-9.2.7.tar.gz", hash = "sha256:b44da35b0d98cd762d09ef74f1ddce5b6d6e35c13f13beb0c9d82a629e5f229e"}, ] [package.dependencies] -colorama = ">=0.4" -jinja2 = ">=3.0" -markdown = ">=3.2" -mkdocs = ">=1.5.0" -mkdocs-material-extensions = ">=1.1" -pygments = ">=2.14" -pymdown-extensions = ">=9.9.1" -regex = ">=2022.4.24" -requests = ">=2.26" +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.5,<2.0" +mkdocs-material-extensions = ">=1.1,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4,<2023.0" +requests = ">=2.26,<3.0" [[package]] name = "mkdocs-material-extensions" @@ -1391,6 +1408,17 @@ files = [ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + [[package]] name = "pathspec" version = "0.11.2" @@ -1478,30 +1506,33 @@ plugins = ["importlib-metadata"] [[package]] name = "pymdown-extensions" -version = "10.1" +version = "10.2.1" description = "Extension pack for Python Markdown." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pymdown_extensions-10.1-py3-none-any.whl", hash = "sha256:ef25dbbae530e8f67575d222b75ff0649b1e841e22c2ae9a20bad9472c2207dc"}, - {file = "pymdown_extensions-10.1.tar.gz", hash = "sha256:508009b211373058debb8247e168de4cbcb91b1bff7b5e961b2c3e864e00b195"}, + {file = "pymdown_extensions-10.2.1-py3-none-any.whl", hash = "sha256:bded105eb8d93f88f2f821f00108cb70cef1269db6a40128c09c5f48bfc60ea4"}, + {file = "pymdown_extensions-10.2.1.tar.gz", hash = "sha256:d0c534b4a5725a4be7ccef25d65a4c97dba58b54ad7c813babf0eb5ba9c81591"}, ] [package.dependencies] markdown = ">=3.2" pyyaml = "*" +[package.extras] +extra = ["pygments (>=2.12)"] + [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.1-py3-none-any.whl", hash = "sha256:460c9a59b14e27c602eb5ece2e47bec99dc5fc5f6513cf924a7d03a578991b1f"}, + {file = "pytest-7.4.1.tar.gz", hash = "sha256:2f2301e797521b23e4d2585a0a3d7b5e50fdddaaf7e7d6773ea26ddb17c213ab"}, ] [package.dependencies] @@ -1518,14 +1549,14 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-aiohttp" -version = "1.0.4" +version = "1.0.5" description = "Pytest plugin for aiohttp support" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"}, - {file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"}, + {file = "pytest-aiohttp-1.0.5.tar.gz", hash = "sha256:880262bc5951e934463b15e3af8bb298f11f7d4d3ebac970aab425aff10a780a"}, + {file = "pytest_aiohttp-1.0.5-py3-none-any.whl", hash = "sha256:63a5360fd2f34dda4ab8e6baee4c5f5be4cd186a403cabd498fced82ac9c561e"}, ] [package.dependencies] @@ -1689,100 +1720,100 @@ pyyaml = "*" [[package]] name = "regex" -version = "2023.6.3" +version = "2022.10.31" description = "Alternative regular expression module, to replace re." category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "regex-2023.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:824bf3ac11001849aec3fa1d69abcb67aac3e150a933963fb12bda5151fe1bfd"}, - {file = "regex-2023.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05ed27acdf4465c95826962528f9e8d41dbf9b1aa8531a387dee6ed215a3e9ef"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b49c764f88a79160fa64f9a7b425620e87c9f46095ef9c9920542ab2495c8bc"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8e3f1316c2293e5469f8f09dc2d76efb6c3982d3da91ba95061a7e69489a14ef"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43e1dd9d12df9004246bacb79a0e5886b3b6071b32e41f83b0acbf293f820ee8"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4959e8bcbfda5146477d21c3a8ad81b185cd252f3d0d6e4724a5ef11c012fb06"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af4dd387354dc83a3bff67127a124c21116feb0d2ef536805c454721c5d7993d"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2239d95d8e243658b8dbb36b12bd10c33ad6e6933a54d36ff053713f129aa536"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:890e5a11c97cf0d0c550eb661b937a1e45431ffa79803b942a057c4fb12a2da2"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a8105e9af3b029f243ab11ad47c19b566482c150c754e4c717900a798806b222"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:25be746a8ec7bc7b082783216de8e9473803706723b3f6bef34b3d0ed03d57e2"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3676f1dd082be28b1266c93f618ee07741b704ab7b68501a173ce7d8d0d0ca18"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:10cb847aeb1728412c666ab2e2000ba6f174f25b2bdc7292e7dd71b16db07568"}, - {file = "regex-2023.6.3-cp310-cp310-win32.whl", hash = "sha256:dbbbfce33cd98f97f6bffb17801b0576e653f4fdb1d399b2ea89638bc8d08ae1"}, - {file = "regex-2023.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:c5f8037000eb21e4823aa485149f2299eb589f8d1fe4b448036d230c3f4e68e0"}, - {file = "regex-2023.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c123f662be8ec5ab4ea72ea300359023a5d1df095b7ead76fedcd8babbedf969"}, - {file = "regex-2023.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9edcbad1f8a407e450fbac88d89e04e0b99a08473f666a3f3de0fd292badb6aa"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcba6dae7de533c876255317c11f3abe4907ba7d9aa15d13e3d9710d4315ec0e"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29cdd471ebf9e0f2fb3cac165efedc3c58db841d83a518b082077e612d3ee5df"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12b74fbbf6cbbf9dbce20eb9b5879469e97aeeaa874145517563cca4029db65c"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c29ca1bd61b16b67be247be87390ef1d1ef702800f91fbd1991f5c4421ebae8"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77f09bc4b55d4bf7cc5eba785d87001d6757b7c9eec237fe2af57aba1a071d9"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ea353ecb6ab5f7e7d2f4372b1e779796ebd7b37352d290096978fea83c4dba0c"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:10590510780b7541969287512d1b43f19f965c2ece6c9b1c00fc367b29d8dce7"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e2fbd6236aae3b7f9d514312cdb58e6494ee1c76a9948adde6eba33eb1c4264f"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:6b2675068c8b56f6bfd5a2bda55b8accbb96c02fd563704732fd1c95e2083461"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74419d2b50ecb98360cfaa2974da8689cb3b45b9deff0dcf489c0d333bcc1477"}, - {file = "regex-2023.6.3-cp311-cp311-win32.whl", hash = "sha256:fb5ec16523dc573a4b277663a2b5a364e2099902d3944c9419a40ebd56a118f9"}, - {file = "regex-2023.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:09e4a1a6acc39294a36b7338819b10baceb227f7f7dbbea0506d419b5a1dd8af"}, - {file = "regex-2023.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0654bca0cdf28a5956c83839162692725159f4cda8d63e0911a2c0dc76166525"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:463b6a3ceb5ca952e66550a4532cef94c9a0c80dc156c4cc343041951aec1697"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87b2a5bb5e78ee0ad1de71c664d6eb536dc3947a46a69182a90f4410f5e3f7dd"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6343c6928282c1f6a9db41f5fd551662310e8774c0e5ebccb767002fcf663ca9"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6192d5af2ccd2a38877bfef086d35e6659566a335b1492786ff254c168b1693"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74390d18c75054947e4194019077e243c06fbb62e541d8817a0fa822ea310c14"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:742e19a90d9bb2f4a6cf2862b8b06dea5e09b96c9f2df1779e53432d7275331f"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8abbc5d54ea0ee80e37fef009e3cec5dafd722ed3c829126253d3e22f3846f1e"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c2b867c17a7a7ae44c43ebbeb1b5ff406b3e8d5b3e14662683e5e66e6cc868d3"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d831c2f8ff278179705ca59f7e8524069c1a989e716a1874d6d1aab6119d91d1"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ee2d1a9a253b1729bb2de27d41f696ae893507c7db224436abe83ee25356f5c1"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:61474f0b41fe1a80e8dfa70f70ea1e047387b7cd01c85ec88fa44f5d7561d787"}, - {file = "regex-2023.6.3-cp36-cp36m-win32.whl", hash = "sha256:0b71e63226e393b534105fcbdd8740410dc6b0854c2bfa39bbda6b0d40e59a54"}, - {file = "regex-2023.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bbb02fd4462f37060122e5acacec78e49c0fbb303c30dd49c7f493cf21fc5b27"}, - {file = "regex-2023.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b862c2b9d5ae38a68b92e215b93f98d4c5e9454fa36aae4450f61dd33ff48487"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976d7a304b59ede34ca2921305b57356694f9e6879db323fd90a80f865d355a3"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:83320a09188e0e6c39088355d423aa9d056ad57a0b6c6381b300ec1a04ec3d16"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9427a399501818a7564f8c90eced1e9e20709ece36be701f394ada99890ea4b3"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178bbc1b2ec40eaca599d13c092079bf529679bf0371c602edaa555e10b41c3"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:837328d14cde912af625d5f303ec29f7e28cdab588674897baafaf505341f2fc"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d44dc13229905ae96dd2ae2dd7cebf824ee92bc52e8cf03dcead37d926da019"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d54af539295392611e7efbe94e827311eb8b29668e2b3f4cadcfe6f46df9c777"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7117d10690c38a622e54c432dfbbd3cbd92f09401d622902c32f6d377e2300ee"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bb60b503ec8a6e4e3e03a681072fa3a5adcbfa5479fa2d898ae2b4a8e24c4591"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:65ba8603753cec91c71de423a943ba506363b0e5c3fdb913ef8f9caa14b2c7e0"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:271f0bdba3c70b58e6f500b205d10a36fb4b58bd06ac61381b68de66442efddb"}, - {file = "regex-2023.6.3-cp37-cp37m-win32.whl", hash = "sha256:9beb322958aaca059f34975b0df135181f2e5d7a13b84d3e0e45434749cb20f7"}, - {file = "regex-2023.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fea75c3710d4f31389eed3c02f62d0b66a9da282521075061ce875eb5300cf23"}, - {file = "regex-2023.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8f56fcb7ff7bf7404becdfc60b1e81a6d0561807051fd2f1860b0d0348156a07"}, - {file = "regex-2023.6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d2da3abc88711bce7557412310dfa50327d5769a31d1c894b58eb256459dc289"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a99b50300df5add73d307cf66abea093304a07eb017bce94f01e795090dea87c"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5708089ed5b40a7b2dc561e0c8baa9535b77771b64a8330b684823cfd5116036"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:687ea9d78a4b1cf82f8479cab23678aff723108df3edeac098e5b2498879f4a7"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d3850beab9f527f06ccc94b446c864059c57651b3f911fddb8d9d3ec1d1b25d"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8915cc96abeb8983cea1df3c939e3c6e1ac778340c17732eb63bb96247b91d2"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:841d6e0e5663d4c7b4c8099c9997be748677d46cbf43f9f471150e560791f7ff"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9edce5281f965cf135e19840f4d93d55b3835122aa76ccacfd389e880ba4cf82"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b956231ebdc45f5b7a2e1f90f66a12be9610ce775fe1b1d50414aac1e9206c06"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:36efeba71c6539d23c4643be88295ce8c82c88bbd7c65e8a24081d2ca123da3f"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:cf67ca618b4fd34aee78740bea954d7c69fdda419eb208c2c0c7060bb822d747"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b4598b1897837067a57b08147a68ac026c1e73b31ef6e36deeeb1fa60b2933c9"}, - {file = "regex-2023.6.3-cp38-cp38-win32.whl", hash = "sha256:f415f802fbcafed5dcc694c13b1292f07fe0befdb94aa8a52905bd115ff41e88"}, - {file = "regex-2023.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:d4f03bb71d482f979bda92e1427f3ec9b220e62a7dd337af0aa6b47bf4498f72"}, - {file = "regex-2023.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ccf91346b7bd20c790310c4147eee6ed495a54ddb6737162a36ce9dbef3e4751"}, - {file = "regex-2023.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b28f5024a3a041009eb4c333863d7894d191215b39576535c6734cd88b0fcb68"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0bb18053dfcfed432cc3ac632b5e5e5c5b7e55fb3f8090e867bfd9b054dbcbf"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5bfb3004f2144a084a16ce19ca56b8ac46e6fd0651f54269fc9e230edb5e4a"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c6b48d0fa50d8f4df3daf451be7f9689c2bde1a52b1225c5926e3f54b6a9ed1"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:051da80e6eeb6e239e394ae60704d2b566aa6a7aed6f2890a7967307267a5dc6"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4c3b7fa4cdaa69268748665a1a6ff70c014d39bb69c50fda64b396c9116cf77"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:457b6cce21bee41ac292d6753d5e94dcbc5c9e3e3a834da285b0bde7aa4a11e9"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aad51907d74fc183033ad796dd4c2e080d1adcc4fd3c0fd4fd499f30c03011cd"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0385e73da22363778ef2324950e08b689abdf0b108a7d8decb403ad7f5191938"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c6a57b742133830eec44d9b2290daf5cbe0a2f1d6acee1b3c7b1c7b2f3606df7"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3e5219bf9e75993d73ab3d25985c857c77e614525fac9ae02b1bebd92f7cecac"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e5087a3c59eef624a4591ef9eaa6e9a8d8a94c779dade95d27c0bc24650261cd"}, - {file = "regex-2023.6.3-cp39-cp39-win32.whl", hash = "sha256:20326216cc2afe69b6e98528160b225d72f85ab080cbdf0b11528cbbaba2248f"}, - {file = "regex-2023.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:bdff5eab10e59cf26bc479f565e25ed71a7d041d1ded04ccf9aee1d9f208487a"}, - {file = "regex-2023.6.3.tar.gz", hash = "sha256:72d1a25bf36d2050ceb35b517afe13864865268dfb45910e2e17a84be6cbfeb0"}, + {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, + {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, + {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, + {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, + {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, + {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, + {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, + {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, + {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, + {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, + {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, + {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, + {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, + {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, + {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, + {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, + {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, ] [[package]] @@ -2026,14 +2057,71 @@ files = [ [[package]] name = "tree-sitter" -version = "0.20.1" -description = "Python bindings to the Tree-sitter parsing library" +version = "0.20.2" +description = "Python bindings for the Tree-Sitter parsing library" category = "main" optional = false python-versions = ">=3.3" files = [ - {file = "tree_sitter-0.20.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:6f11a1fd909dcf569e7b1d98861a837436799e757bbbc5cd5280989050929e12"}, - {file = "tree_sitter-0.20.1.tar.gz", hash = "sha256:e93f082c545d6649bcfb5d681ed255eb004a6ce22988971a128f40692feec60d"}, + {file = "tree_sitter-0.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1a151ccf9233b0b84850422654247f68a4d78f548425c76520402ea6fb6cdb24"}, + {file = "tree_sitter-0.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52ca2738c3c4c660c83054ac3e44a49cbecb9f89dc26bb8e154d6ca288aa06b0"}, + {file = "tree_sitter-0.20.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8d51478ea078da7cc6f626e9e36f131bbc5fac036cf38ea4b5b81632cbac37d"}, + {file = "tree_sitter-0.20.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0b2b59e1633efbf19cd2ed1ceb8d51b2c44a278153b1113998c70bc1570b750"}, + {file = "tree_sitter-0.20.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7f691c57d2a65d6e53e2f3574153c9cd0c157ff938b8d6f252edd5e619811403"}, + {file = "tree_sitter-0.20.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba72a363387eebaff9a0b788f864fe47da425136cbd4cac6cd125051f043c296"}, + {file = "tree_sitter-0.20.2-cp310-cp310-win32.whl", hash = "sha256:55e33eb206446d5046d3b5fe36ab300840f5a8a844246adb0ccc68c55c30b722"}, + {file = "tree_sitter-0.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ce9d14daba0a71a778417d9d61dd4038ca96981ddec19e1e8990881469321c"}, + {file = "tree_sitter-0.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:942dbfb8bc380f09b0e323d3884de07d19022930516f33b7503a6eb5f6e18979"}, + {file = "tree_sitter-0.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ee5651c11924d426f8d6858a40fd5090ae31574f81ef180bef2055282f43bf62"}, + {file = "tree_sitter-0.20.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fb6982b480031628dad7f229c4c8d90b17d4c281ba97848d3b100666d7fa45f"}, + {file = "tree_sitter-0.20.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:067609c6c7cb6e5a6c4be50076a380fe52b6e8f0641ee9d0da33b24a5b972e82"}, + {file = "tree_sitter-0.20.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:849d7e6b66fe7ded08a633943b30e0ed807eee76104288e6c6841433f4a9651b"}, + {file = "tree_sitter-0.20.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e85689573797e49f86e2d7cf48b9dd23bc044c477df074a78546e666d6990a29"}, + {file = "tree_sitter-0.20.2-cp311-cp311-win32.whl", hash = "sha256:098906148e44ea391a91b019d584dd8d0ea1437af62a9744e280e93163fd35ca"}, + {file = "tree_sitter-0.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:2753a87094b72fe7f02276b3948155618f53aa14e1ca20588f0eeed510f68512"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5de192cb9e7b1c882d45418decb7899f1547f7056df756bcae186bbf4966d96e"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3a77e663293a73a97edbf2a2e05001de08933eb5d311a16bdc25b9b2fac54f3"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:415da4a70c56a003758537517fe9e60b8b0c5f70becde54cc8b8f3ba810adc70"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:707fb4d7a6123b8f9f2b005d61194077c3168c0372556e7418802280eddd4892"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:75fcbfb0a61ad64e7f787eb3f8fbf29b8e2b858dc011897ad039d838a06cee02"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-win32.whl", hash = "sha256:622926530895d939fa6e1e2487e71a311c71d3b09f4c4f19301695ea866304a4"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-win_amd64.whl", hash = "sha256:5c0712f031271d9bc462f1db7623d23703ed9fbcbaa6dc19ba535f58d6110774"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2dfdf680ecf5619447243c4c20e4040a7b5e7afca4e1569f03c814e86bfda248"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79650ee23a15559b69542c71ed9eb3297dce21932a7c5c148be384dd0f2cd49d"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d63059746b4b2f2f87dd19c208141c69452694aae32459b7a4ebca8539d13bf4"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9398d1e214d4915032cf68a678de7eb803f64d25ef04724d70b88db7bb7746e9"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b506fb2e2bd7a5a1603c644bbb90401fe488f86bbca39706addaa8d2bfc80815"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-win32.whl", hash = "sha256:405e83804ba60ca1c3dbd258adbe0d7b0f1bdce948e5eec5587a2ebedcf930ba"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a1e66d211c04144484e223922ac094a2367476e6f57000f986c5560dc5a83c6e"}, + {file = "tree_sitter-0.20.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f8adc325c74c042204ed47d095e0ec86f83de3c7ec4979645f86b58514f60297"}, + {file = "tree_sitter-0.20.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb49c861e1d111e0df119ecbfaa409e6413b8d91e8f56bcdb15f07fbc35594e"}, + {file = "tree_sitter-0.20.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e17ee83409b01fdd09021997b0c747be2f773bb2bb140ba6fb48b7e12fdd039a"}, + {file = "tree_sitter-0.20.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:475ab841647a0d1bc1266c8978279f8e4f7b9520b9a7336d532e5dfc8910214d"}, + {file = "tree_sitter-0.20.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:222350189675d9814966a5c88c6c1378a2ee2f3041c439a6f1d1ff2006f403aa"}, + {file = "tree_sitter-0.20.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:31ea52f0deee70f2cb00aff01e40aae325a34ebe1661de274c9107322fb95f54"}, + {file = "tree_sitter-0.20.2-cp38-cp38-win32.whl", hash = "sha256:cceaf7287137cbca707006624a4a8d4b5ccbfec025793fde84d90524c2bb0946"}, + {file = "tree_sitter-0.20.2-cp38-cp38-win_amd64.whl", hash = "sha256:25b9669911f21ec2b3727bb2f4dfeff6ddb6f81898c3e968d378a660e0d7f90e"}, + {file = "tree_sitter-0.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce30a17f46a6b39a04a599dea88c127a19e3e1f43a2ad0ced71b5c032d585077"}, + {file = "tree_sitter-0.20.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9576e8b2e663639527e01ab251b87f0bd370bfdd40515588689ebc424aec786"}, + {file = "tree_sitter-0.20.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d03731a498f624ce3536c821ef23b03d1ad569b3845b326a5b7149ef189d732c"}, + {file = "tree_sitter-0.20.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0116ecb163573ebaa0fc04cc99c90bd94c0be5cc4d0a1ebeb102de9cc9a054"}, + {file = "tree_sitter-0.20.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0943b00d3700f253c3ee6a53a71b9a6ca46defd9c0a33edb07a9388e70dc3a9e"}, + {file = "tree_sitter-0.20.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cb566b6f0b5457148cb8310a1ca3d764edf28e47fcccfe0b167861ecaa50c12"}, + {file = "tree_sitter-0.20.2-cp39-cp39-win32.whl", hash = "sha256:4544204a24c2b4d25d1731b0df83f7c819ce87c4f2538a19724b8753815ef388"}, + {file = "tree_sitter-0.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:9517b204e471d6aa59ee2232f6220f315ed5336079034d5c861a24660d6511d6"}, + {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:84343678f58cb354d22ed14b627056ffb33c540cf16c35a83db4eeee8827b935"}, + {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:611a80171d8fa6833dd0c8b022714d2ea789de15a955ec42ec4fd5fcc1032edb"}, + {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bacecfb61694c95ccee462742b3fcea50ba1baf115c42e60adf52b549ef642ce"}, + {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f344ae94a268479456f19712736cc7398de5822dc74cca7d39538c28085721d0"}, + {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:221784d7f326fe81ce7174ac5972800f58b9a7c5c48a03719cad9830c22e5a76"}, + {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64210ed8d2a1b7e2951f6576aa0cb7be31ad06d87da26c52961318fc54c7fe77"}, + {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2634ac73b39ceacfa431d6d95692eae7465977fa0b9e9f7ae6cb445991e829a5"}, + {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:71663a0e8230dae99d9c55e6895bd2c9e42534ec861b255775f704ae2db70c1d"}, + {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:32c3e0f30b45a58d36bf6a0ec982ca3eaa23c7f924628da499b7ad22a8abad71"}, + {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b02e4ab2158c25f6f520c93318d562da58fa4ba53e1dbd434be008f48104980"}, + {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10e567eb6961a1e86aebbe26a9ca07d324f8529bca90937a924f8aa0ea4dc127"}, + {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63f8e8e69f5f25c2b565449e1b8a2aa7b6338b4f37c8658c5fbdec04858c30be"}, + {file = "tree_sitter-0.20.2.tar.gz", hash = "sha256:0a6c06abaa55de174241a476b536173bba28241d2ea85d198d33aa8bf009f028"}, ] [[package]] @@ -2244,14 +2332,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.24.2" +version = "20.24.4" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, - {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, + {file = "virtualenv-20.24.4-py3-none-any.whl", hash = "sha256:29c70bb9b88510f6414ac3e55c8b413a1f96239b6b789ca123437d5e892190cb"}, + {file = "virtualenv-20.24.4.tar.gz", hash = "sha256:772b05bfda7ed3b8ecd16021ca9716273ad9f4467c801f27e83ac73430246dca"}, ] [package.dependencies] @@ -2261,7 +2349,7 @@ importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} platformdirs = ">=3.9.1,<4" [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] From d7b91f389e2423ad0b8926e8a0edab6d531ec68c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 6 Sep 2023 18:24:34 +0100 Subject: [PATCH 314/366] Add theme --- src/textual/document/_text_area_theme.py | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/textual/document/_text_area_theme.py b/src/textual/document/_text_area_theme.py index 94ed4c5614..72029eff2d 100644 --- a/src/textual/document/_text_area_theme.py +++ b/src/textual/document/_text_area_theme.py @@ -244,9 +244,51 @@ def default(cls) -> TextAreaTheme: }, ) +_DARK_VS = TextAreaTheme( + name="dark_vs", + base_style=Style(color="#CCCCCC", bgcolor="#1F1F1F"), + gutter_style=Style(color="#6E7681", bgcolor="#1F1F1F"), + cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"), + cursor_line_style=Style(bgcolor="#232323"), + cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#232323"), + selection_style=Style(bgcolor="#264F78"), + token_styles={ + "string": Style(color="#ce9178"), + "string.documentation": Style(color="#ce9178"), + "comment": Style(color="#6A9955"), + "keyword": Style(color="#569cd6"), + "operator": Style(color="#569cd6"), + "conditional": Style(color="#569cd6"), + "keyword.function": Style(color="#569cd6"), + "keyword.return": Style(color="#569cd6"), + "keyword.operator": Style(color="#d4d4d4"), + "repeat": Style(color="#569cd6"), + "exception": Style(color="#569cd6"), + "include": Style(color="#569cd6"), + "number": Style(color="#b5cea8"), + "float": Style(color="#b5cea8"), + "class": Style(color="#4EC9B0"), + "type.class": Style(color="#4EC9B0"), + "function": Style(color="#4EC9B0"), + "method": Style(color="#4EC9B0"), + "boolean": Style(color="#7DAF9C"), + "tag": Style(color="#EFCB43"), + "yaml.field": Style(color="#EFCB43", bold=True), + "json.label": Style(color="#EFCB43", bold=True), + "toml.type": Style(color="#EFCB43"), + "heading": Style(color="#569cd6", bold=True), + "bold": Style(bold=True), + "italic": Style(italic=True), + "strikethrough": Style(strike=True), + "link": Style(color="#40A6FF", underline=True), + "inline_code": Style(color="#ce9178"), + }, +) + _BUILTIN_THEMES = { "monokai": _MONOKAI, "dracula": _DRACULA, + "dark_vs": _DARK_VS, } DEFAULT_SYNTAX_THEME = TextAreaTheme.get_by_name("monokai") From cc19218dc468e44833a7d010ab5eab8b339cc835 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 6 Sep 2023 20:00:14 +0100 Subject: [PATCH 315/366] Fix VSCode theme bracket matching --- src/textual/document/_text_area_theme.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/document/_text_area_theme.py b/src/textual/document/_text_area_theme.py index 72029eff2d..c2137dca60 100644 --- a/src/textual/document/_text_area_theme.py +++ b/src/textual/document/_text_area_theme.py @@ -249,7 +249,8 @@ def default(cls) -> TextAreaTheme: base_style=Style(color="#CCCCCC", bgcolor="#1F1F1F"), gutter_style=Style(color="#6E7681", bgcolor="#1F1F1F"), cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"), - cursor_line_style=Style(bgcolor="#232323"), + cursor_line_style=Style(bgcolor="#252525"), + bracket_matching_style=Style(bgcolor="#3a3a3a"), cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#232323"), selection_style=Style(bgcolor="#264F78"), token_styles={ From 533e51d3c2de17869b822708374675b6765a0f0e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 6 Sep 2023 20:10:52 +0100 Subject: [PATCH 316/366] Only match brackets when theres no selection --- src/textual/document/_text_area_theme.py | 2 +- src/textual/widgets/_text_area.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/textual/document/_text_area_theme.py b/src/textual/document/_text_area_theme.py index c2137dca60..9af5903bcd 100644 --- a/src/textual/document/_text_area_theme.py +++ b/src/textual/document/_text_area_theme.py @@ -250,7 +250,7 @@ def default(cls) -> TextAreaTheme: gutter_style=Style(color="#6E7681", bgcolor="#1F1F1F"), cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"), cursor_line_style=Style(bgcolor="#252525"), - bracket_matching_style=Style(bgcolor="#3a3a3a"), + bracket_matching_style=Style(bgcolor="#3a3a3a", bold=True), cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#232323"), selection_style=Style(bgcolor="#264F78"), token_styles={ diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index c1e3345dcc..fb87c76498 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -649,7 +649,9 @@ def render_line(self, widget_y: int) -> Strip: # Highlight the cursor matching_bracket = self._matching_bracket_location match_cursor_bracket = self.match_cursor_bracket - draw_matched_brackets = match_cursor_bracket and matching_bracket is not None + draw_matched_brackets = ( + match_cursor_bracket and matching_bracket is not None and start == end + ) if cursor_row == line_index: draw_cursor = not self.cursor_blink or ( From 84e1b4856b9b50f5c65bb5040c358731276cca0b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 7 Sep 2023 11:46:38 +0100 Subject: [PATCH 317/366] Highlighting tidying --- src/textual/document/_text_area_theme.py | 65 ++++++++++++++++++++---- tree-sitter/highlights/markdown.scm | 4 +- tree-sitter/highlights/yaml.scm | 2 +- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/textual/document/_text_area_theme.py b/src/textual/document/_text_area_theme.py index c2137dca60..07270f84c8 100644 --- a/src/textual/document/_text_area_theme.py +++ b/src/textual/document/_text_area_theme.py @@ -128,11 +128,11 @@ def get_highlight(self, name: str) -> Style: return self.token_styles.get(name) @classmethod - def available_themes(cls) -> list[TextAreaTheme]: - """Get a list of all available TextAreaThemes. + def builtin_themes(cls) -> list[TextAreaTheme]: + """Get a list of all builtin TextAreaThemes. Returns: - A list of all available TextAreaThemes. + A list of all builtin TextAreaThemes. """ return list(_BUILTIN_THEMES.values()) @@ -191,7 +191,7 @@ def default(cls) -> TextAreaTheme: "italic": Style(italic=True), "strikethrough": Style(strike=True), "link": Style(color="#66D9EF", underline=True), - "inline_code": Style(color="#F92672"), + "inline_code": Style(color="#E6DB74"), }, ) @@ -240,12 +240,12 @@ def default(cls) -> TextAreaTheme: "italic": Style(italic=True), "strikethrough": Style(strike=True), "link": Style(color="#bd93f9", underline=True), - "inline_code": Style(color="#ff79c6"), + "inline_code": Style(color="#f1fa8c"), }, ) _DARK_VS = TextAreaTheme( - name="dark_vs", + name="vscode_dark", base_style=Style(color="#CCCCCC", bgcolor="#1F1F1F"), gutter_style=Style(color="#6E7681", bgcolor="#1F1F1F"), cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"), @@ -274,22 +274,67 @@ def default(cls) -> TextAreaTheme: "method": Style(color="#4EC9B0"), "boolean": Style(color="#7DAF9C"), "tag": Style(color="#EFCB43"), - "yaml.field": Style(color="#EFCB43", bold=True), - "json.label": Style(color="#EFCB43", bold=True), - "toml.type": Style(color="#EFCB43"), + "yaml.field": Style(color="#569cd6", bold=True), + "json.label": Style(color="#569cd6", bold=True), + "toml.type": Style(color="#569cd6"), "heading": Style(color="#569cd6", bold=True), "bold": Style(bold=True), "italic": Style(italic=True), "strikethrough": Style(strike=True), "link": Style(color="#40A6FF", underline=True), "inline_code": Style(color="#ce9178"), + "info_string": Style(color="#ce9178", bold=True, italic=True), + }, +) + +_GITHUB_LIGHT = TextAreaTheme( + name="github_light", + base_style=Style(color="#24292e", bgcolor="#f0f0f0"), + gutter_style=Style(color="#BBBBBB", bgcolor="#f0f0f0"), + cursor_style=Style(color="#fafbfc", bgcolor="#24292e"), + cursor_line_style=Style(bgcolor="#ebebeb"), + bracket_matching_style=Style(color="#24292e", underline=True), + cursor_line_gutter_style=Style(color="#A4A4A4", bgcolor="#ebebeb"), + selection_style=Style(bgcolor="#c8c8fa"), + token_styles={ + "string": Style(color="#093069"), + "string.documentation": Style(color="#093069"), + "comment": Style(color="#6a737d"), + "keyword": Style(color="#d73a49"), + "operator": Style(color="#0450AE"), + "conditional": Style(color="#CF222E"), + "keyword.function": Style(color="#CF222E"), + "keyword.return": Style(color="#CF222E"), + "keyword.operator": Style(color="#CF222E"), + "repeat": Style(color="#CF222E"), + "exception": Style(color="#CF222E"), + "include": Style(color="#CF222E"), + "number": Style(color="#d73a49"), + "float": Style(color="#d73a49"), + "parameter": Style(color="#24292e"), + "class": Style(color="#963800"), + "variable": Style(color="#e36209"), + "function": Style(color="#6639BB"), + "method": Style(color="#6639BB"), + "boolean": Style(color="#7DAF9C"), + "tag": Style(color="#6639BB"), + "yaml.field": Style(color="#6639BB"), + "json.label": Style(color="#6639BB"), + "toml.type": Style(color="#6639BB"), + "heading": Style(color="#24292e", bold=True), + "bold": Style(bold=True), + "italic": Style(italic=True), + "strikethrough": Style(strike=True), + "link": Style(color="#40A6FF", underline=True), + "inline_code": Style(color="#093069"), }, ) _BUILTIN_THEMES = { "monokai": _MONOKAI, "dracula": _DRACULA, - "dark_vs": _DARK_VS, + "vscode_dark": _DARK_VS, + "github_light": _GITHUB_LIGHT, } DEFAULT_SYNTAX_THEME = TextAreaTheme.get_by_name("monokai") diff --git a/tree-sitter/highlights/markdown.scm b/tree-sitter/highlights/markdown.scm index 0912f52892..cb0aa1fb75 100644 --- a/tree-sitter/highlights/markdown.scm +++ b/tree-sitter/highlights/markdown.scm @@ -1,7 +1,9 @@ -(heading_content) @heading +(atx_heading) @heading (list_marker) @comment (strong_emphasis) @bold (emphasis) @italic (strikethrough) @strikethrough (link) @link (code_span) @inline_code +(info_string) @info_string +(fenced_code_block) @fenced_code_block diff --git a/tree-sitter/highlights/yaml.scm b/tree-sitter/highlights/yaml.scm index d2ab774bf4..a57f464dfc 100644 --- a/tree-sitter/highlights/yaml.scm +++ b/tree-sitter/highlights/yaml.scm @@ -7,7 +7,7 @@ (escape_sequence) @string.escape (integer_scalar) @number (float_scalar) @number -(comment) @comment @spell +(comment) @comment (anchor_name) @type (alias_name) @type (tag) @type From 29ffcfc05f94032c5a54d05228ff721fe946b892 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 7 Sep 2023 11:49:01 +0100 Subject: [PATCH 318/366] Fix markdown header highlighting --- .../__snapshots__/test_snapshots.ambr | 312 +++++++++--------- tree-sitter/highlights/markdown.scm | 2 +- 2 files changed, 157 insertions(+), 157 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 0ec72692d1..401a32e874 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -28817,323 +28817,323 @@ font-weight: 700; } - .terminal-3459552068-matrix { + .terminal-332895177-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3459552068-title { + .terminal-332895177-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3459552068-r1 { fill: #c2c2bf } - .terminal-3459552068-r2 { fill: #272822;font-weight: bold } - .terminal-3459552068-r3 { fill: #f92672;font-weight: bold } - .terminal-3459552068-r4 { fill: #f8f8f2 } - .terminal-3459552068-r5 { fill: #c5c8c6 } - .terminal-3459552068-r6 { fill: #90908a } - .terminal-3459552068-r7 { fill: #f8f8f2;font-style: italic; } - .terminal-3459552068-r8 { fill: #f8f8f2;font-weight: bold } - .terminal-3459552068-r9 { fill: #f92672 } - .terminal-3459552068-r10 { fill: #75715e } - .terminal-3459552068-r11 { fill: #66d9ef;text-decoration: underline; } - .terminal-3459552068-r12 { fill: #e1e1e1 } - .terminal-3459552068-r13 { fill: #23568b } + .terminal-332895177-r1 { fill: #c2c2bf } + .terminal-332895177-r2 { fill: #272822;font-weight: bold } + .terminal-332895177-r3 { fill: #f92672;font-weight: bold } + .terminal-332895177-r4 { fill: #f8f8f2 } + .terminal-332895177-r5 { fill: #c5c8c6 } + .terminal-332895177-r6 { fill: #90908a } + .terminal-332895177-r7 { fill: #f8f8f2;font-style: italic; } + .terminal-332895177-r8 { fill: #f8f8f2;font-weight: bold } + .terminal-332895177-r9 { fill: #e6db74 } + .terminal-332895177-r10 { fill: #75715e } + .terminal-332895177-r11 { fill: #66d9ef;text-decoration: underline; } + .terminal-332895177-r12 { fill: #e1e1e1 } + .terminal-332895177-r13 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  Heading -  2  =======                                                                      -  3   -  4  Sub-heading -  5  -----------                                                                  -  6   -  7  ### Heading -  8   -  9  #### H4 Heading - 10   - 11  ##### H5 Heading - 12   - 13  ###### H6 Heading - 14   - 15   - 16  Paragraphs are separated                                                     - 17  by a blank line.                                                             - 18   - 19  Two spaces at the end of a line                                              - 20  produces a line break.                                                       - 21   - 22  Text attributes _italic_,                                                    - 23  **bold**`monospace`.                                                       - 24   - 25  Horizontal rule:                                                             - 26   - 27  ---                                                                          - 28   - 29  Bullet list:                                                                 - 30   - 31  * apples                                                                   - 32  * oranges                                                                  - 33  * pears                                                                    - 34   - 35  Numbered list:                                                               - 36   - 37  1. lather                                                                  - 38  2. rinse                                                                   - 39  3. repeat                                                                  - 40   - 41  An [example](http://example.com).                                            - 42   - 43  > Markdown uses email-style > characters for blockquoting.                   - 44  >                                                                            - 45  > Lorem ipsum                                                                - 46   - 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) - 48   - 49   - 50  ```                                                                          - 51  a=1                                                                          - 52  ```                                                                          - 53   - 54  ```python                                                                    - 55  import this                                                                  - 56  ```                                                                          - 57   - 58  ```somelang                                                                  - 59  foobar                                                                       - 60  ```                                                                          - 61   - 62      import this                                                              - 63   - 64   - 65  1. List item                                                                 - 66   - 67         Code block                                                            - 68   - + +  1  Heading +  2  =======                                                                      +  3   +  4  Sub-heading +  5  -----------                                                                  +  6   +  7  ### Heading +  8   +  9  #### H4 Heading + 10   + 11  ##### H5 Heading + 12   + 13  ###### H6 Heading + 14   + 15   + 16  Paragraphs are separated                                                     + 17  by a blank line.                                                             + 18   + 19  Two spaces at the end of a line                                              + 20  produces a line break.                                                       + 21   + 22  Text attributes _italic_,                                                    + 23  **bold**`monospace`.                                                       + 24   + 25  Horizontal rule:                                                             + 26   + 27  ---                                                                          + 28   + 29  Bullet list:                                                                 + 30   + 31  * apples                                                                   + 32  * oranges                                                                  + 33  * pears                                                                    + 34   + 35  Numbered list:                                                               + 36   + 37  1. lather                                                                  + 38  2. rinse                                                                   + 39  3. repeat                                                                  + 40   + 41  An [example](http://example.com).                                            + 42   + 43  > Markdown uses email-style > characters for blockquoting.                   + 44  >                                                                            + 45  > Lorem ipsum                                                                + 46   + 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) + 48   + 49   + 50  ```                                                                          + 51  a=1                                                                          + 52  ```                                                                          + 53   + 54  ```python                                                                    + 55  import this                                                                  + 56  ```                                                                          + 57   + 58  ```somelang                                                                  + 59  foobar                                                                       + 60  ```                                                                          + 61   + 62      import this                                                              + 63   + 64   + 65  1. List item                                                                 + 66   + 67         Code block                                                            + 68   + diff --git a/tree-sitter/highlights/markdown.scm b/tree-sitter/highlights/markdown.scm index cb0aa1fb75..1d6691bcfc 100644 --- a/tree-sitter/highlights/markdown.scm +++ b/tree-sitter/highlights/markdown.scm @@ -1,4 +1,4 @@ -(atx_heading) @heading +(heading_content) @heading (list_marker) @comment (strong_emphasis) @bold (emphasis) @italic From 7b75970ce3147749ad1e952b76a9805247ef0e79 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 7 Sep 2023 15:56:32 +0100 Subject: [PATCH 319/366] Setting theme correctly in background --- src/textual/widgets/_text_area.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index c1e3345dcc..7ed7744f01 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -11,6 +11,7 @@ from rich.text import Text from textual._tree_sitter import TREE_SITTER +from textual.color import Color from textual.document._document import _utf8_encode from textual.expand_tabs import expand_tabs_inline @@ -399,7 +400,17 @@ def _watch_indent_width(self) -> None: """Changing width of tabs will change document display width.""" self._refresh_size() - def _validate_theme(self, theme: str | TextAreaTheme) -> TextAreaTheme: + def _watch_theme(self, theme: TextAreaTheme | None) -> None: + if theme is None: + self.styles.color = None + self.styles.background = None + else: + self.styles.color = Color.from_rich_color(theme.base_style.color) + self.styles.background = Color.from_rich_color(theme.base_style.bgcolor) + + def _validate_theme( + self, theme: str | TextAreaTheme | None + ) -> TextAreaTheme | None: if isinstance(theme, str): theme = TextAreaTheme.get_by_name(theme) return theme From 0dd1fffa3cabc283ef69ac33570309fded3f3158 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 8 Sep 2023 09:57:45 +0100 Subject: [PATCH 320/366] Tidying module interface --- .../{document => }/_text_area_theme.py | 0 src/textual/document/__init__.py | 15 - src/textual/document/_languages.py | 24 +- .../document/_syntax_aware_document.py | 6 +- src/textual/widgets/_text_area.py | 98 +- src/textual/widgets/text_area.py | 18 + tests/document/test_document.py | 2 +- tests/document/test_document_delete.py | 2 +- tests/document/test_document_insert.py | 2 +- .../__snapshots__/test_snapshots.ambr | 2043 ++++++++--------- tests/snapshot_tests/test_snapshots.py | 4 +- tests/text_area/test_edit_via_api.py | 2 +- tests/text_area/test_edit_via_bindings.py | 2 +- tests/text_area/test_selection.py | 2 +- tests/text_area/test_selection_bindings.py | 2 +- 15 files changed, 1136 insertions(+), 1086 deletions(-) rename src/textual/{document => }/_text_area_theme.py (100%) diff --git a/src/textual/document/_text_area_theme.py b/src/textual/_text_area_theme.py similarity index 100% rename from src/textual/document/_text_area_theme.py rename to src/textual/_text_area_theme.py diff --git a/src/textual/document/__init__.py b/src/textual/document/__init__.py index 14dc915756..e69de29bb2 100644 --- a/src/textual/document/__init__.py +++ b/src/textual/document/__init__.py @@ -1,15 +0,0 @@ -from ._document import Document, DocumentBase, EditResult, Location, Selection -from ._languages import VALID_LANGUAGES -from ._syntax_aware_document import SyntaxAwareDocument -from ._text_area_theme import DEFAULT_SYNTAX_THEME, TextAreaTheme - -__all__ = [ - "Document", - "DocumentBase", - "Location", - "EditResult", - "Selection", - "SyntaxAwareDocument", - "TextAreaTheme", - "VALID_LANGUAGES", -] diff --git a/src/textual/document/_languages.py b/src/textual/document/_languages.py index d7076bb193..a33f7544e8 100644 --- a/src/textual/document/_languages.py +++ b/src/textual/document/_languages.py @@ -1,11 +1,13 @@ -VALID_LANGUAGES = [ - "markdown", - "yaml", - "sql", - "css", - "html", - "json", - "python", - "regex", - "toml", -] +BUILTIN_LANGUAGES = sorted( + [ + "markdown", + "yaml", + "sql", + "css", + "html", + "json", + "python", + "regex", + "toml", + ] +) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 832accdb26..5ccd806f00 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -9,9 +9,9 @@ except ImportError: TREE_SITTER = False +from textual._text_area_theme import TextAreaTheme from textual.document._document import Document, EditResult, Location, _utf8_encode -from textual.document._languages import VALID_LANGUAGES -from textual.document._text_area_theme import TextAreaTheme +from textual.document._languages import BUILTIN_LANGUAGES class SyntaxAwareDocumentError(Exception): @@ -62,7 +62,7 @@ def __init__( # If the language is `None`, then avoid doing any parsing related stuff. if isinstance(language, str): - if language not in VALID_LANGUAGES: + if language not in BUILTIN_LANGUAGES: raise SyntaxAwareDocumentError(f"Invalid language {language!r}") self.language = get_language(language) self._parser = get_parser(language) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 26480446d3..f349f7ef12 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -10,9 +10,19 @@ from rich.style import Style from rich.text import Text +from textual._text_area_theme import TextAreaTheme from textual._tree_sitter import TREE_SITTER from textual.color import Color -from textual.document._document import _utf8_encode +from textual.document._document import ( + Document, + DocumentBase, + EditResult, + Location, + Selection, + _utf8_encode, +) +from textual.document._languages import BUILTIN_LANGUAGES +from textual.document._syntax_aware_document import SyntaxAwareDocument from textual.expand_tabs import expand_tabs_inline if TYPE_CHECKING: @@ -23,15 +33,6 @@ from textual._cells import cell_len from textual._types import Literal, Protocol, runtime_checkable from textual.binding import Binding -from textual.document import ( - Document, - DocumentBase, - EditResult, - Location, - Selection, - SyntaxAwareDocument, - TextAreaTheme, -) from textual.events import MouseEvent from textual.geometry import Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive @@ -338,11 +339,30 @@ def _watch_selection(self, selection: Selection) -> None: character = None # Record the location of a matching closing/opening bracket. + match_location = self.find_matching_bracket(character, cursor_location) + self._matching_bracket_location = match_location + if match_location is not None: + match_row, match_column = match_location + if match_row in range(*self._visible_line_indices): + self.refresh_lines(match_row) + + def find_matching_bracket( + self, bracket: str, search_from: Location + ) -> Location | None: + """If the character is a bracket, find the matching bracket. + + Args: + bracket: The character we're searching for the matching bracket of. + search_from: The location to start the search. + + Returns: + The `Location` of the matching bracket, or None if it's not found. + """ match_location = None bracket_stack = [] - if character in _OPENING_BRACKETS: + if bracket in _OPENING_BRACKETS: for candidate, candidate_location in self._yield_character_locations( - cursor_location + search_from ): if candidate in _OPENING_BRACKETS: bracket_stack.append(candidate) @@ -355,11 +375,11 @@ def _watch_selection(self, selection: Selection) -> None: if not bracket_stack: match_location = candidate_location break - elif character in _CLOSING_BRACKETS: + elif bracket in _CLOSING_BRACKETS: for ( candidate, candidate_location, - ) in self._yield_character_locations_reverse(cursor_location): + ) in self._yield_character_locations_reverse(search_from): if candidate in _CLOSING_BRACKETS: bracket_stack.append(candidate) elif candidate in _OPENING_BRACKETS: @@ -372,11 +392,7 @@ def _watch_selection(self, selection: Selection) -> None: match_location = candidate_location break - self._matching_bracket_location = match_location - if match_location is not None: - match_row, match_column = match_location - if match_row in range(*self._visible_line_indices): - self.refresh_lines(match_row) + return match_location def _validate_selection(self, selection: Selection) -> Selection: """Clamp the selection to valid locations.""" @@ -400,7 +416,17 @@ def _watch_indent_width(self) -> None: """Changing width of tabs will change document display width.""" self._refresh_size() + def _validate_theme( + self, theme: str | TextAreaTheme | None + ) -> TextAreaTheme | None: + """When the user sets the theme to a string, convert it to a `TextAreaTheme`.""" + if isinstance(theme, str): + theme = TextAreaTheme.get_by_name(theme) + return theme + def _watch_theme(self, theme: TextAreaTheme | None) -> None: + """We set the styles on this widget when the theme changes, to ensure that + if padding is applied, the colours match.""" if theme is None: self.styles.color = None self.styles.background = None @@ -408,12 +434,34 @@ def _watch_theme(self, theme: TextAreaTheme | None) -> None: self.styles.color = Color.from_rich_color(theme.base_style.color) self.styles.background = Color.from_rich_color(theme.base_style.bgcolor) - def _validate_theme( - self, theme: str | TextAreaTheme | None - ) -> TextAreaTheme | None: - if isinstance(theme, str): - theme = TextAreaTheme.get_by_name(theme) - return theme + @property + def available_themes(self) -> list[str]: + """A list of the names of the themes available to the `TextArea`. + + The values in this list can be assigned `theme` reactive attribute of + `TextArea`. + + You can retrieve the full specification for a theme by passing one of + the strings from this list into `TextAreaTheme.get_by_name(theme_name: str)`. + + Alternatively, you can directly retrieve a list of `TextAreaTheme` objects + (which contain the full theme specification) by calling + `TextAreaTheme.builtin_themes()`. + """ + return [theme.name for theme in TextAreaTheme.builtin_themes()] + + @property + def available_languages(self) -> list[str]: + """A list of the names of languages available to the `TextArea`. + + The values in this list can be assigned to the `language` reactive attribute + of `TextArea`. + + The returned list contains the builtin languages plus those registered via the + `register_language` method. Builtin languages will be listed before + user-registered languages, but there are no other ordering guarantees. + """ + return BUILTIN_LANGUAGES + list(self._languages.keys()) def register_language( self, diff --git a/src/textual/widgets/text_area.py b/src/textual/widgets/text_area.py index a157ff7ec3..6f3a214e9e 100644 --- a/src/textual/widgets/text_area.py +++ b/src/textual/widgets/text_area.py @@ -1,3 +1,13 @@ +from textual._text_area_theme import TextAreaTheme +from textual.document._document import ( + Document, + DocumentBase, + EditResult, + Location, + Selection, +) +from textual.document._languages import BUILTIN_LANGUAGES +from textual.document._syntax_aware_document import SyntaxAwareDocument from textual.widgets._text_area import ( Edit, EndColumn, @@ -12,4 +22,12 @@ "Highlight", "HighlightName", "StartColumn", + "TextAreaTheme", + "Document", + "DocumentBase", + "Location", + "EditResult", + "Selection", + "SyntaxAwareDocument", + "BUILTIN_LANGUAGES", ] diff --git a/tests/document/test_document.py b/tests/document/test_document.py index af232fafb2..b6e9952782 100644 --- a/tests/document/test_document.py +++ b/tests/document/test_document.py @@ -1,6 +1,6 @@ import pytest -from textual.document import Document +from textual.widgets.text_area import Document TEXT = """I must not fear. Fear is the mind-killer.""" diff --git a/tests/document/test_document_delete.py b/tests/document/test_document_delete.py index 3007ac5df3..d00fa686c9 100644 --- a/tests/document/test_document_delete.py +++ b/tests/document/test_document_delete.py @@ -1,6 +1,6 @@ import pytest -from textual.document import Document, EditResult +from textual.widgets.text_area import Document, EditResult TEXT = """I must not fear. Fear is the mind-killer. diff --git a/tests/document/test_document_insert.py b/tests/document/test_document_insert.py index cba3aed275..ea706c9abf 100644 --- a/tests/document/test_document_insert.py +++ b/tests/document/test_document_insert.py @@ -1,4 +1,4 @@ -from textual.document import Document +from textual.widgets.text_area import Document TEXT = """I must not fear. Fear is the mind-killer.""" diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 401a32e874..099ea8096f 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -27982,319 +27982,319 @@ font-weight: 700; } - .terminal-2963979561-matrix { + .terminal-2148777132-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2963979561-title { + .terminal-2148777132-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2963979561-r1 { fill: #c2c2bf } - .terminal-2963979561-r2 { fill: #272822 } - .terminal-2963979561-r3 { fill: #75715e } - .terminal-2963979561-r4 { fill: #f8f8f2 } - .terminal-2963979561-r5 { fill: #c5c8c6 } - .terminal-2963979561-r6 { fill: #90908a } - .terminal-2963979561-r7 { fill: #e6db74 } - .terminal-2963979561-r8 { fill: #ae81ff } - .terminal-2963979561-r9 { fill: #f92672 } - .terminal-2963979561-r10 { fill: #a6e22e } + .terminal-2148777132-r1 { fill: #c2c2bf } + .terminal-2148777132-r2 { fill: #272822 } + .terminal-2148777132-r3 { fill: #75715e } + .terminal-2148777132-r4 { fill: #f8f8f2 } + .terminal-2148777132-r5 { fill: #c5c8c6 } + .terminal-2148777132-r6 { fill: #90908a } + .terminal-2148777132-r7 { fill: #e6db74 } + .terminal-2148777132-r8 { fill: #ae81ff } + .terminal-2148777132-r9 { fill: #f92672 } + .terminal-2148777132-r10 { fill: #a6e22e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  /* This is a comment in CSS */ -  2   -  3  /* Basic selectors and properties */ -  4  body {                                 -  5      font-family: Arial, sans-serif;    -  6      background-color: #f4f4f4;         -  7      margin: 0;                         -  8      padding: 0;                        -  9  }                                      - 10   - 11  /* Class and ID selectors */ - 12  .header {                              - 13      background-color: #333;            - 14      color: #fff;                       - 15      padding: 10px0;                   - 16      text-align: center;                - 17  }                                      - 18   - 19  #logo {                                - 20      font-size: 24px;                   - 21      font-weight: bold;                 - 22  }                                      - 23   - 24  /* Descendant and child selectors */ - 25  .nav ul {                              - 26      list-style-type: none;             - 27      padding: 0;                        - 28  }                                      - 29   - 30  .nav > li {                            - 31      display: inline-block;             - 32      margin-right: 10px;                - 33  }                                      - 34   - 35  /* Pseudo-classes */ - 36  a:hover {                              - 37      text-decoration: underline;        - 38  }                                      - 39   - 40  input:focus {                          - 41      border-color: #007BFF;             - 42  }                                      - 43   - 44  /* Media query */ - 45  @media (max-width: 768px) {            - 46      body {                             - 47          font-size: 16px;               - 48      }                                  - 49   - 50      .header {                          - 51          padding: 5px0;                - 52      }                                  - 53  }                                      - 54   - 55  /* Keyframes animation */ - 56  @keyframes slideIn {                   - 57  from {                             - 58          transform: translateX(-100%);  - 59      }                                  - 60  to {                               - 61          transform: translateX(0);      - 62      }                                  - 63  }                                      - 64   - 65  .slide-in-element {                    - 66      animation: slideIn 0.5s forwards;  - 67  }                                      - 68   + + + +  1  /* This is a comment in CSS */ +  2   +  3  /* Basic selectors and properties */ +  4  body {                                 +  5      font-family: Arial, sans-serif;    +  6      background-color: #f4f4f4;         +  7      margin: 0;                         +  8      padding: 0;                        +  9  }                                      + 10   + 11  /* Class and ID selectors */ + 12  .header {                              + 13      background-color: #333;            + 14      color: #fff;                       + 15      padding: 10px0;                   + 16      text-align: center;                + 17  }                                      + 18   + 19  #logo {                                + 20      font-size: 24px;                   + 21      font-weight: bold;                 + 22  }                                      + 23   + 24  /* Descendant and child selectors */ + 25  .nav ul {                              + 26      list-style-type: none;             + 27      padding: 0;                        + 28  }                                      + 29   + 30  .nav > li {                            + 31      display: inline-block;             + 32      margin-right: 10px;                + 33  }                                      + 34   + 35  /* Pseudo-classes */ + 36  a:hover {                              + 37      text-decoration: underline;        + 38  }                                      + 39   + 40  input:focus {                          + 41      border-color: #007BFF;             + 42  }                                      + 43   + 44  /* Media query */ + 45  @media (max-width: 768px) {            + 46      body {                             + 47          font-size: 16px;               + 48      }                                  + 49   + 50      .header {                          + 51          padding: 5px0;                + 52      }                                  + 53  }                                      + 54   + 55  /* Keyframes animation */ + 56  @keyframes slideIn {                   + 57  from {                             + 58          transform: translateX(-100%);  + 59      }                                  + 60  to {                               + 61          transform: translateX(0);      + 62      }                                  + 63  }                                      + 64   + 65  .slide-in-element {                    + 66      animation: slideIn 0.5s forwards;  + 67  }                                      + 68   @@ -28325,273 +28325,273 @@ font-weight: 700; } - .terminal-2763447717-matrix { + .terminal-1948245288-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2763447717-title { + .terminal-1948245288-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2763447717-r1 { fill: #c2c2bf } - .terminal-2763447717-r2 { fill: #272822 } - .terminal-2763447717-r3 { fill: #f8f8f2 } - .terminal-2763447717-r4 { fill: #c5c8c6 } - .terminal-2763447717-r5 { fill: #90908a } - .terminal-2763447717-r6 { fill: #f92672 } - .terminal-2763447717-r7 { fill: #e6db74 } - .terminal-2763447717-r8 { fill: #75715e } + .terminal-1948245288-r1 { fill: #c2c2bf } + .terminal-1948245288-r2 { fill: #272822 } + .terminal-1948245288-r3 { fill: #f8f8f2 } + .terminal-1948245288-r4 { fill: #c5c8c6 } + .terminal-1948245288-r5 { fill: #90908a } + .terminal-1948245288-r6 { fill: #f92672 } + .terminal-1948245288-r7 { fill: #e6db74 } + .terminal-1948245288-r8 { fill: #75715e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  <!DOCTYPE html>                                                              -  2  <html lang="en">                                                            -  3   -  4  <head>                                                                      -  5  <!-- Meta tags --> -  6      <meta charset="UTF-8">                                                  -  7      <meta name="viewport" content="width=device-width, initial-scale=1.0" -  8  <!-- Title --> -  9      <title>HTML Test Page</title>                                           - 10  <!-- Link to CSS --> - 11      <link rel="stylesheet" href="styles.css">                               - 12  </head>                                                                     - 13   - 14  <body>                                                                      - 15  <!-- Header section --> - 16      <header class="header">                                                 - 17          <h1 id="logo">HTML Test Page</h1>                                   - 18      </header>                                                               - 19   - 20  <!-- Navigation --> - 21      <nav class="nav">                                                       - 22          <ul>                                                                - 23              <li><a href="#">Home</a></li>                                   - 24              <li><a href="#">About</a></li>                                  - 25              <li><a href="#">Contact</a></li>                                - 26          </ul>                                                               - 27      </nav>                                                                  - 28   - 29  <!-- Main content area --> - 30      <main>                                                                  - 31          <article>                                                           - 32              <h2>Welcome to the Test Page</h2>                               - 33              <p>This is a paragraph to test the HTML structure.</p>          - 34              <img src="test-image.jpg" alt="Test Image" width="300">         - 35          </article>                                                          - 36      </main>                                                                 - 37   - 38  <!-- Form --> - 39      <section>                                                               - 40          <form action="/submit" method="post">                               - 41              <label for="name">Name:</label>                                 - 42              <input type="text" id="name" name="name">                       - 43              <input type="submit" value="Submit">                            - 44          </form>                                                             - 45      </section>                                                              - 46   - 47  <!-- Footer --> - 48      <footer>                                                                - 49          <p>&copy; 2023 HTML Test Page</p>                                   - 50      </footer>                                                               - 51   - 52  <!-- Script tag --> - 53      <script src="scripts.js"></script>                                      - 54  </body>                                                                     - 55   - 56  </html>                                                                     - 57   + + + +  1  <!DOCTYPE html>                                                              +  2  <html lang="en">                                                            +  3   +  4  <head>                                                                      +  5  <!-- Meta tags --> +  6      <meta charset="UTF-8">                                                  +  7      <meta name="viewport" content="width=device-width, initial-scale=1.0" +  8  <!-- Title --> +  9      <title>HTML Test Page</title>                                           + 10  <!-- Link to CSS --> + 11      <link rel="stylesheet" href="styles.css">                               + 12  </head>                                                                     + 13   + 14  <body>                                                                      + 15  <!-- Header section --> + 16      <header class="header">                                                 + 17          <h1 id="logo">HTML Test Page</h1>                                   + 18      </header>                                                               + 19   + 20  <!-- Navigation --> + 21      <nav class="nav">                                                       + 22          <ul>                                                                + 23              <li><a href="#">Home</a></li>                                   + 24              <li><a href="#">About</a></li>                                  + 25              <li><a href="#">Contact</a></li>                                + 26          </ul>                                                               + 27      </nav>                                                                  + 28   + 29  <!-- Main content area --> + 30      <main>                                                                  + 31          <article>                                                           + 32              <h2>Welcome to the Test Page</h2>                               + 33              <p>This is a paragraph to test the HTML structure.</p>          + 34              <img src="test-image.jpg" alt="Test Image" width="300">         + 35          </article>                                                          + 36      </main>                                                                 + 37   + 38  <!-- Form --> + 39      <section>                                                               + 40          <form action="/submit" method="post">                               + 41              <label for="name">Name:</label>                                 + 42              <input type="text" id="name" name="name">                       + 43              <input type="submit" value="Submit">                            + 44          </form>                                                             + 45      </section>                                                              + 46   + 47  <!-- Footer --> + 48      <footer>                                                                + 49          <p>&copy; 2023 HTML Test Page</p>                                   + 50      </footer>                                                               + 51   + 52  <!-- Script tag --> + 53      <script src="scripts.js"></script>                                      + 54  </body>                                                                     + 55   + 56  </html>                                                                     + 57   @@ -28622,171 +28622,171 @@ font-weight: 700; } - .terminal-2458942-matrix { + .terminal-3481240769-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2458942-title { + .terminal-3481240769-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2458942-r1 { fill: #c2c2bf } - .terminal-2458942-r2 { fill: #272822;font-weight: bold } - .terminal-2458942-r3 { fill: #f8f8f2 } - .terminal-2458942-r4 { fill: #c5c8c6 } - .terminal-2458942-r5 { fill: #90908a } - .terminal-2458942-r6 { fill: #f92672;font-weight: bold } - .terminal-2458942-r7 { fill: #e6db74 } - .terminal-2458942-r8 { fill: #ae81ff } - .terminal-2458942-r9 { fill: #66d9ef;font-style: italic; } - .terminal-2458942-r10 { fill: #f8f8f2;font-weight: bold } + .terminal-3481240769-r1 { fill: #c2c2bf } + .terminal-3481240769-r2 { fill: #272822;font-weight: bold } + .terminal-3481240769-r3 { fill: #f8f8f2 } + .terminal-3481240769-r4 { fill: #c5c8c6 } + .terminal-3481240769-r5 { fill: #90908a } + .terminal-3481240769-r6 { fill: #f92672;font-weight: bold } + .terminal-3481240769-r7 { fill: #e6db74 } + .terminal-3481240769-r8 { fill: #ae81ff } + .terminal-3481240769-r9 { fill: #66d9ef;font-style: italic; } + .terminal-3481240769-r10 { fill: #f8f8f2;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  { -  2  "name""John Doe",                            -  3  "age"30,                                     -  4  "isStudent"false,                            -  5  "address": {                                   -  6  "street""123 Main St",                   -  7  "city""Anytown",                         -  8  "state""CA",                             -  9  "zip""12345" - 10      },                                             - 11  "phoneNumbers": [                              - 12          {                                          - 13  "type""home",                        - 14  "number""555-555-1234" - 15          },                                         - 16          {                                          - 17  "type""work",                        - 18  "number""555-555-5678" - 19          }                                          - 20      ],                                             - 21  "hobbies": ["reading""hiking""swimming"],  - 22  "pets": [                                      - 23          {                                          - 24  "type""dog",                         - 25  "name""Fido" - 26          },                                         - 27      ],                                             - 28  "graduationYear"null - 29  } - 30   - 31   + + + +  1  { +  2  "name""John Doe",                            +  3  "age"30,                                     +  4  "isStudent"false,                            +  5  "address": {                                   +  6  "street""123 Main St",                   +  7  "city""Anytown",                         +  8  "state""CA",                             +  9  "zip""12345" + 10      },                                             + 11  "phoneNumbers": [                              + 12          {                                          + 13  "type""home",                        + 14  "number""555-555-1234" + 15          },                                         + 16          {                                          + 17  "type""work",                        + 18  "number""555-555-5678" + 19          }                                          + 20      ],                                             + 21  "hobbies": ["reading""hiking""swimming"],  + 22  "pets": [                                      + 23          {                                          + 24  "type""dog",                         + 25  "name""Fido" + 26          },                                         + 27      ],                                             + 28  "graduationYear"null + 29  } + 30   + 31   @@ -28817,323 +28817,322 @@ font-weight: 700; } - .terminal-332895177-matrix { + .terminal-567647262-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-332895177-title { + .terminal-567647262-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-332895177-r1 { fill: #c2c2bf } - .terminal-332895177-r2 { fill: #272822;font-weight: bold } - .terminal-332895177-r3 { fill: #f92672;font-weight: bold } - .terminal-332895177-r4 { fill: #f8f8f2 } - .terminal-332895177-r5 { fill: #c5c8c6 } - .terminal-332895177-r6 { fill: #90908a } - .terminal-332895177-r7 { fill: #f8f8f2;font-style: italic; } - .terminal-332895177-r8 { fill: #f8f8f2;font-weight: bold } - .terminal-332895177-r9 { fill: #e6db74 } - .terminal-332895177-r10 { fill: #75715e } - .terminal-332895177-r11 { fill: #66d9ef;text-decoration: underline; } - .terminal-332895177-r12 { fill: #e1e1e1 } - .terminal-332895177-r13 { fill: #23568b } + .terminal-567647262-r1 { fill: #c2c2bf } + .terminal-567647262-r2 { fill: #272822;font-weight: bold } + .terminal-567647262-r3 { fill: #f92672;font-weight: bold } + .terminal-567647262-r4 { fill: #f8f8f2 } + .terminal-567647262-r5 { fill: #c5c8c6 } + .terminal-567647262-r6 { fill: #90908a } + .terminal-567647262-r7 { fill: #f8f8f2;font-style: italic; } + .terminal-567647262-r8 { fill: #f8f8f2;font-weight: bold } + .terminal-567647262-r9 { fill: #e6db74 } + .terminal-567647262-r10 { fill: #75715e } + .terminal-567647262-r11 { fill: #66d9ef;text-decoration: underline; } + .terminal-567647262-r12 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  Heading -  2  =======                                                                      -  3   -  4  Sub-heading -  5  -----------                                                                  -  6   -  7  ### Heading -  8   -  9  #### H4 Heading - 10   - 11  ##### H5 Heading - 12   - 13  ###### H6 Heading - 14   - 15   - 16  Paragraphs are separated                                                     - 17  by a blank line.                                                             - 18   - 19  Two spaces at the end of a line                                              - 20  produces a line break.                                                       - 21   - 22  Text attributes _italic_,                                                    - 23  **bold**`monospace`.                                                       - 24   - 25  Horizontal rule:                                                             - 26   - 27  ---                                                                          - 28   - 29  Bullet list:                                                                 - 30   - 31  * apples                                                                   - 32  * oranges                                                                  - 33  * pears                                                                    - 34   - 35  Numbered list:                                                               - 36   - 37  1. lather                                                                  - 38  2. rinse                                                                   - 39  3. repeat                                                                  - 40   - 41  An [example](http://example.com).                                            - 42   - 43  > Markdown uses email-style > characters for blockquoting.                   - 44  >                                                                            - 45  > Lorem ipsum                                                                - 46   - 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) - 48   - 49   - 50  ```                                                                          - 51  a=1                                                                          - 52  ```                                                                          - 53   - 54  ```python                                                                    - 55  import this                                                                  - 56  ```                                                                          - 57   - 58  ```somelang                                                                  - 59  foobar                                                                       - 60  ```                                                                          - 61   - 62      import this                                                              - 63   - 64   - 65  1. List item                                                                 - 66   - 67         Code block                                                            - 68   - + +  1  Heading +  2  =======                                                                      +  3   +  4  Sub-heading +  5  -----------                                                                  +  6   +  7  ### Heading +  8   +  9  #### H4 Heading + 10   + 11  ##### H5 Heading + 12   + 13  ###### H6 Heading + 14   + 15   + 16  Paragraphs are separated                                                     + 17  by a blank line.                                                             + 18   + 19  Two spaces at the end of a line                                              + 20  produces a line break.                                                       + 21   + 22  Text attributes _italic_,                                                    + 23  **bold**`monospace`.                                                       + 24   + 25  Horizontal rule:                                                             + 26   + 27  ---                                                                          + 28   + 29  Bullet list:                                                                 + 30   + 31  * apples                                                                   + 32  * oranges                                                                  + 33  * pears                                                                    + 34   + 35  Numbered list:                                                               + 36   + 37  1. lather                                                                  + 38  2. rinse                                                                   + 39  3. repeat                                                                  + 40   + 41  An [example](http://example.com).                                            + 42   + 43  > Markdown uses email-style > characters for blockquoting.                   + 44  >                                                                            + 45  > Lorem ipsum                                                                + 46   + 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) + 48   + 49   + 50  ```                                                                          + 51  a=1                                                                          + 52  ```                                                                          + 53   + 54  ```python                                                                    + 55  import this                                                                  + 56  ```                                                                          + 57   + 58  ```somelang                                                                  + 59  foobar                                                                       + 60  ```                                                                          + 61   + 62      import this                                                              + 63   + 64   + 65  1. List item                                                                 + 66   + 67         Code block                                                            + 68   + @@ -29163,363 +29162,363 @@ font-weight: 700; } - .terminal-1854819181-matrix { + .terminal-1039616752-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1854819181-title { + .terminal-1039616752-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1854819181-r1 { fill: #c2c2bf } - .terminal-1854819181-r2 { fill: #272822 } - .terminal-1854819181-r3 { fill: #f92672 } - .terminal-1854819181-r4 { fill: #f8f8f2 } - .terminal-1854819181-r5 { fill: #c5c8c6 } - .terminal-1854819181-r6 { fill: #90908a } - .terminal-1854819181-r7 { fill: #75715e } - .terminal-1854819181-r8 { fill: #e6db74 } - .terminal-1854819181-r9 { fill: #ae81ff } - .terminal-1854819181-r10 { fill: #a6e22e } + .terminal-1039616752-r1 { fill: #c2c2bf } + .terminal-1039616752-r2 { fill: #272822 } + .terminal-1039616752-r3 { fill: #f92672 } + .terminal-1039616752-r4 { fill: #f8f8f2 } + .terminal-1039616752-r5 { fill: #c5c8c6 } + .terminal-1039616752-r6 { fill: #90908a } + .terminal-1039616752-r7 { fill: #75715e } + .terminal-1039616752-r8 { fill: #e6db74 } + .terminal-1039616752-r9 { fill: #ae81ff } + .terminal-1039616752-r10 { fill: #a6e22e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  import math                                                                  -  2  from os import path                                                          -  3   -  4  # I'm a comment :) -  5   -  6  string_var ="Hello, world!" -  7  int_var =42 -  8  float_var =3.14 -  9  complex_var =1+2j - 10   - 11  list_var = [12345]                                                   - 12  tuple_var = (12345)                                                  - 13  set_var = {12345}                                                    - 14  dict_var = {"a"1"b"2"c"3}                                          - 15   - 16  deffunction_no_args():                                                      - 17  return"No arguments" - 18   - 19  deffunction_with_args(a, b):                                                - 20  return a + b                                                             - 21   - 22  deffunction_with_default_args(a=0, b=0):                                    - 23  return a * b                                                             - 24   - 25  lambda_func =lambda x: x**2 - 26   - 27  if int_var ==42:                                                            - 28  print("It's the answer!")                                                - 29  elif int_var <42:                                                           - 30  print("Less than the answer.")                                           - 31  else:                                                                        - 32  print("Greater than the answer.")                                        - 33   - 34  for index, value inenumerate(list_var):                                     - 35  print(f"Index: {index}, Value: {value}")                                 - 36   - 37  counter =0 - 38  while counter <5:                                                           - 39  print(f"Counter value: {counter}")                                       - 40      counter +=1 - 41   - 42  squared_numbers = [x**2for x inrange(10if x %2==0]                    - 43   - 44  try:                                                                         - 45      result =10/0 - 46  except ZeroDivisionError:                                                    - 47  print("Cannot divide by zero!")                                          - 48  finally:                                                                     - 49  print("End of try-except block.")                                        - 50   - 51  classAnimal:                                                                - 52  def__init__(self, name):                                                - 53          self.name = name                                                     - 54   - 55  defspeak(self):                                                         - 56  raiseNotImplementedError("Subclasses must implement this method." - 57   - 58  classDog(Animal):                                                           - 59  defspeak(self):                                                         - 60  returnf"{self.name} says Woof!" - 61   - 62  deffibonacci(n):                                                            - 63      a, b =01 - 64  for _ inrange(n):                                                       - 65  yield a                                                              - 66          a, b = b, a + b                                                      - 67   - 68  for num infibonacci(5):                                                     - 69  print(num)                                                               - 70   - 71  withopen('test.txt''w'as f:                                             - 72      f.write("Testing with statement.")                                       - 73   - 74  @my_decorator                                                                - 75  defsay_hello():                                                             - 76  print("Hello!")                                                          - 77   - 78  say_hello()                                                                  - 79   + + + +  1  import math                                                                  +  2  from os import path                                                          +  3   +  4  # I'm a comment :) +  5   +  6  string_var ="Hello, world!" +  7  int_var =42 +  8  float_var =3.14 +  9  complex_var =1+2j + 10   + 11  list_var = [12345]                                                   + 12  tuple_var = (12345)                                                  + 13  set_var = {12345}                                                    + 14  dict_var = {"a"1"b"2"c"3}                                          + 15   + 16  deffunction_no_args():                                                      + 17  return"No arguments" + 18   + 19  deffunction_with_args(a, b):                                                + 20  return a + b                                                             + 21   + 22  deffunction_with_default_args(a=0, b=0):                                    + 23  return a * b                                                             + 24   + 25  lambda_func =lambda x: x**2 + 26   + 27  if int_var ==42:                                                            + 28  print("It's the answer!")                                                + 29  elif int_var <42:                                                           + 30  print("Less than the answer.")                                           + 31  else:                                                                        + 32  print("Greater than the answer.")                                        + 33   + 34  for index, value inenumerate(list_var):                                     + 35  print(f"Index: {index}, Value: {value}")                                 + 36   + 37  counter =0 + 38  while counter <5:                                                           + 39  print(f"Counter value: {counter}")                                       + 40      counter +=1 + 41   + 42  squared_numbers = [x**2for x inrange(10if x %2==0]                    + 43   + 44  try:                                                                         + 45      result =10/0 + 46  except ZeroDivisionError:                                                    + 47  print("Cannot divide by zero!")                                          + 48  finally:                                                                     + 49  print("End of try-except block.")                                        + 50   + 51  classAnimal:                                                                + 52  def__init__(self, name):                                                + 53          self.name = name                                                     + 54   + 55  defspeak(self):                                                         + 56  raiseNotImplementedError("Subclasses must implement this method." + 57   + 58  classDog(Animal):                                                           + 59  defspeak(self):                                                         + 60  returnf"{self.name} says Woof!" + 61   + 62  deffibonacci(n):                                                            + 63      a, b =01 + 64  for _ inrange(n):                                                       + 65  yield a                                                              + 66          a, b = b, a + b                                                      + 67   + 68  for num infibonacci(5):                                                     + 69  print(num)                                                               + 70   + 71  withopen('test.txt''w'as f:                                             + 72      f.write("Testing with statement.")                                       + 73   + 74  @my_decorator                                                                + 75  defsay_hello():                                                             + 76  print("Hello!")                                                          + 77   + 78  say_hello()                                                                  + 79   @@ -29550,146 +29549,145 @@ font-weight: 700; } - .terminal-4250850736-matrix { + .terminal-446422533-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4250850736-title { + .terminal-446422533-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4250850736-r1 { fill: #c2c2bf } - .terminal-4250850736-r2 { fill: #272822 } - .terminal-4250850736-r3 { fill: #f8f8f2 } - .terminal-4250850736-r4 { fill: #c5c8c6 } - .terminal-4250850736-r5 { fill: #90908a } - .terminal-4250850736-r6 { fill: #f92672 } - .terminal-4250850736-r7 { fill: #e1e1e1 } - .terminal-4250850736-r8 { fill: #23568b } + .terminal-446422533-r1 { fill: #c2c2bf } + .terminal-446422533-r2 { fill: #272822 } + .terminal-446422533-r3 { fill: #f8f8f2 } + .terminal-446422533-r4 { fill: #c5c8c6 } + .terminal-446422533-r5 { fill: #90908a } + .terminal-446422533-r6 { fill: #f92672 } + .terminal-446422533-r7 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  ^abc            # Matches any string that starts with "abc"                  -  2  abc$            # Matches any string that ends with "abc"                    -  3  ^abc$           # Matches the string "abc" and nothing else                  -  4  a.b             # Matches any string containing "a", any character, then "b" -  5  a[.]b           # Matches the string "a.b"                                   -  6  a|b             # Matches either "a" or "b"                                  -  7  a{2}            # Matches "aa"                                               -  8  a{2,}           # Matches two or more consecutive "a" characters             -  9  a{2,5}          # Matches between 2 and 5 consecutive "a" characters         - 10  a?              # Matches "a" or nothing (0 or 1 occurrence of "a") - 11  a*              # Matches zero or more consecutive "a" characters            - 12  a+              # Matches one or more consecutive "a" characters             - 13  \d              # Matches any digit (equivalent to [0-9]) - 14  \D              # Matches any non-digit                                      - 15  \w              # Matches any word character (equivalent to [a-zA-Z0-9_]) - 16  \W              # Matches any non-word character                             - 17  \s              # Matches any whitespace character (spaces, tabs, line break - 18  \S              # Matches any non-whitespace character                       - 19  (?i)abc         # Case-insensitive match for "abc"                           - 20  (?:a|b)         # Non-capturing group for either "a" or "b"                  - 21  (?<=a)b         # Positive lookbehind: matches "b" that is preceded by "a"   - 22  (?<!a)b         # Negative lookbehind: matches "b" that is not preceded by " - 23  a(?=b)          # Positive lookahead: matches "a" that is followed by "b"    - 24  a(?!b)          # Negative lookahead: matches "a" that is not followed by "b - 25   - + +  1  ^abc            # Matches any string that starts with "abc"                  +  2  abc$            # Matches any string that ends with "abc"                    +  3  ^abc$           # Matches the string "abc" and nothing else                  +  4  a.b             # Matches any string containing "a", any character, then "b" +  5  a[.]b           # Matches the string "a.b"                                   +  6  a|b             # Matches either "a" or "b"                                  +  7  a{2}            # Matches "aa"                                               +  8  a{2,}           # Matches two or more consecutive "a" characters             +  9  a{2,5}          # Matches between 2 and 5 consecutive "a" characters         + 10  a?              # Matches "a" or nothing (0 or 1 occurrence of "a") + 11  a*              # Matches zero or more consecutive "a" characters            + 12  a+              # Matches one or more consecutive "a" characters             + 13  \d              # Matches any digit (equivalent to [0-9]) + 14  \D              # Matches any non-digit                                      + 15  \w              # Matches any word character (equivalent to [a-zA-Z0-9_]) + 16  \W              # Matches any non-word character                             + 17  \s              # Matches any whitespace character (spaces, tabs, line break + 18  \S              # Matches any non-whitespace character                       + 19  (?i)abc         # Case-insensitive match for "abc"                           + 20  (?:a|b)         # Non-capturing group for either "a" or "b"                  + 21  (?<=a)b         # Positive lookbehind: matches "b" that is preceded by "a"   + 22  (?<!a)b         # Negative lookbehind: matches "b" that is not preceded by " + 23  a(?=b)          # Positive lookahead: matches "a" that is followed by "b"    + 24  a(?!b)          # Negative lookahead: matches "a" that is not followed by "b + 25   + @@ -29719,223 +29717,222 @@ font-weight: 700; } - .terminal-1502683429-matrix { + .terminal-1861495189-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1502683429-title { + .terminal-1861495189-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1502683429-r1 { fill: #c2c2bf } - .terminal-1502683429-r2 { fill: #272822 } - .terminal-1502683429-r3 { fill: #75715e } - .terminal-1502683429-r4 { fill: #f8f8f2 } - .terminal-1502683429-r5 { fill: #c5c8c6 } - .terminal-1502683429-r6 { fill: #90908a } - .terminal-1502683429-r7 { fill: #f92672 } - .terminal-1502683429-r8 { fill: #ae81ff } - .terminal-1502683429-r9 { fill: #e6db74 } - .terminal-1502683429-r10 { fill: #e1e1e1 } + .terminal-1861495189-r1 { fill: #c2c2bf } + .terminal-1861495189-r2 { fill: #272822 } + .terminal-1861495189-r3 { fill: #75715e } + .terminal-1861495189-r4 { fill: #f8f8f2 } + .terminal-1861495189-r5 { fill: #c5c8c6 } + .terminal-1861495189-r6 { fill: #90908a } + .terminal-1861495189-r7 { fill: #f92672 } + .terminal-1861495189-r8 { fill: #ae81ff } + .terminal-1861495189-r9 { fill: #e6db74 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - + - -  1  -- This is a comment in SQL -  2   -  3  -- Create tables -  4  CREATETABLE Authors (                                                       -  5      AuthorID INT PRIMARY KEY,                                                -  6      Name VARCHAR(255NOT NULL,                                              -  7      Country VARCHAR(50)                                                      -  8  );                                                                           -  9   - 10  CREATETABLE Books (                                                         - 11      BookID INT PRIMARY KEY,                                                  - 12      Title VARCHAR(255NOT NULL,                                             - 13      AuthorID INT,                                                            - 14      PublishedDate DATE,                                                      - 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)                      - 16  );                                                                           - 17   - 18  -- Insert data - 19  INSERTINTO Authors (AuthorID, Name, Country) VALUES (1'George Orwell''U - 20   - 21  INSERTINTO Books (BookID, Title, AuthorID, PublishedDate) VALUES (1'1984' - 22   - 23  -- Update data - 24  UPDATE Authors SET Country ='United Kingdom'WHERE Country ='UK';          - 25   - 26  -- Select data with JOIN - 27  SELECT Books.Title, Authors.Name                                             - 28  FROM Books                                                                   - 29  JOIN Authors ON Books.AuthorID = Authors.AuthorID;                           - 30   - 31  -- Delete data (commented to preserve data for other examples) - 32  -- DELETE FROM Books WHERE BookID = 1; - 33   - 34  -- Alter table structure - 35  ALTER TABLE Authors ADD COLUMN BirthDate DATE;                               - 36   - 37  -- Create index - 38  CREATEINDEX idx_author_name ON Authors(Name);                               - 39   - 40  -- Drop index (commented to avoid actually dropping it) - 41  -- DROP INDEX idx_author_name ON Authors; - 42   - 43  -- End of script - 44   + +  1  -- This is a comment in SQL +  2   +  3  -- Create tables +  4  CREATETABLE Authors (                                                       +  5      AuthorID INT PRIMARY KEY,                                                +  6      Name VARCHAR(255NOT NULL,                                              +  7      Country VARCHAR(50)                                                      +  8  );                                                                           +  9   + 10  CREATETABLE Books (                                                         + 11      BookID INT PRIMARY KEY,                                                  + 12      Title VARCHAR(255NOT NULL,                                             + 13      AuthorID INT,                                                            + 14      PublishedDate DATE,                                                      + 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)                      + 16  );                                                                           + 17   + 18  -- Insert data + 19  INSERTINTO Authors (AuthorID, Name, Country) VALUES (1'George Orwell''U + 20   + 21  INSERTINTO Books (BookID, Title, AuthorID, PublishedDate) VALUES (1'1984' + 22   + 23  -- Update data + 24  UPDATE Authors SET Country ='United Kingdom'WHERE Country ='UK';          + 25   + 26  -- Select data with JOIN + 27  SELECT Books.Title, Authors.Name                                             + 28  FROM Books                                                                   + 29  JOIN Authors ON Books.AuthorID = Authors.AuthorID;                           + 30   + 31  -- Delete data (commented to preserve data for other examples) + 32  -- DELETE FROM Books WHERE BookID = 1; + 33   + 34  -- Alter table structure + 35  ALTER TABLE Authors ADD COLUMN BirthDate DATE;                               + 36   + 37  -- Create index + 38  CREATEINDEX idx_author_name ON Authors(Name);                               + 39   + 40  -- Drop index (commented to avoid actually dropping it) + 41  -- DROP INDEX idx_author_name ON Authors; + 42   + 43  -- End of script + 44   @@ -29966,151 +29963,151 @@ font-weight: 700; } - .terminal-579508414-matrix { + .terminal-4058290241-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-579508414-title { + .terminal-4058290241-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-579508414-r1 { fill: #c2c2bf } - .terminal-579508414-r2 { fill: #272822 } - .terminal-579508414-r3 { fill: #75715e } - .terminal-579508414-r4 { fill: #f8f8f2 } - .terminal-579508414-r5 { fill: #c5c8c6 } - .terminal-579508414-r6 { fill: #90908a } - .terminal-579508414-r7 { fill: #f92672 } - .terminal-579508414-r8 { fill: #e6db74 } - .terminal-579508414-r9 { fill: #ae81ff } - .terminal-579508414-r10 { fill: #66d9ef;font-style: italic; } + .terminal-4058290241-r1 { fill: #c2c2bf } + .terminal-4058290241-r2 { fill: #272822 } + .terminal-4058290241-r3 { fill: #75715e } + .terminal-4058290241-r4 { fill: #f8f8f2 } + .terminal-4058290241-r5 { fill: #c5c8c6 } + .terminal-4058290241-r6 { fill: #90908a } + .terminal-4058290241-r7 { fill: #f92672 } + .terminal-4058290241-r8 { fill: #e6db74 } + .terminal-4058290241-r9 { fill: #ae81ff } + .terminal-4058290241-r10 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  # This is a comment in TOML -  2   -  3  string = "Hello, world!" -  4  integer = 42 -  5  float = 3.14 -  6  boolean = true -  7  datetime = 1979-05-27T07:32:00Z -  8   -  9  fruits = ["apple""banana""cherry" - 10   - 11  [address]                               - 12  street = "123 Main St" - 13  city = "Anytown" - 14  state = "CA" - 15  zip = "12345" - 16   - 17  [person.john]                           - 18  name = "John Doe" - 19  age = 28 - 20  is_student = false - 21   - 22   - 23  [[animals]]                             - 24  name = "Fido" - 25  type = "dog" - 26   + + + +  1  # This is a comment in TOML +  2   +  3  string = "Hello, world!" +  4  integer = 42 +  5  float = 3.14 +  6  boolean = true +  7  datetime = 1979-05-27T07:32:00Z +  8   +  9  fruits = ["apple""banana""cherry" + 10   + 11  [address]                               + 12  street = "123 Main St" + 13  city = "Anytown" + 14  state = "CA" + 15  zip = "12345" + 16   + 17  [person.john]                           + 18  name = "John Doe" + 19  age = 28 + 20  is_student = false + 21   + 22   + 23  [[animals]]                             + 24  name = "Fido" + 25  type = "dog" + 26   @@ -30141,199 +30138,199 @@ font-weight: 700; } - .terminal-575800710-matrix { + .terminal-4054582537-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-575800710-title { + .terminal-4054582537-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-575800710-r1 { fill: #c2c2bf } - .terminal-575800710-r2 { fill: #272822 } - .terminal-575800710-r3 { fill: #75715e } - .terminal-575800710-r4 { fill: #f8f8f2 } - .terminal-575800710-r5 { fill: #c5c8c6 } - .terminal-575800710-r6 { fill: #90908a } - .terminal-575800710-r7 { fill: #f92672;font-weight: bold } - .terminal-575800710-r8 { fill: #e6db74 } - .terminal-575800710-r9 { fill: #ae81ff } - .terminal-575800710-r10 { fill: #66d9ef;font-style: italic; } + .terminal-4054582537-r1 { fill: #c2c2bf } + .terminal-4054582537-r2 { fill: #272822 } + .terminal-4054582537-r3 { fill: #75715e } + .terminal-4054582537-r4 { fill: #f8f8f2 } + .terminal-4054582537-r5 { fill: #c5c8c6 } + .terminal-4054582537-r6 { fill: #90908a } + .terminal-4054582537-r7 { fill: #f92672;font-weight: bold } + .terminal-4054582537-r8 { fill: #e6db74 } + .terminal-4054582537-r9 { fill: #ae81ff } + .terminal-4054582537-r10 { fill: #66d9ef;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - -  1  # This is a comment in YAML -  2   -  3  # Scalars -  4  string"Hello, world!" -  5  integer42 -  6  float3.14 -  7  booleantrue -  8   -  9  # Sequences (Arrays) - 10  fruits:                                               - 11    - Apple - 12    - Banana - 13    - Cherry - 14   - 15  # Nested sequences - 16  persons:                                              - 17    - nameJohn - 18  age28 - 19  is_studentfalse - 20    - nameJane - 21  age22 - 22  is_studenttrue - 23   - 24  # Mappings (Dictionaries) - 25  address:                                              - 26  street123 Main St - 27  cityAnytown - 28  stateCA - 29  zip'12345' - 30   - 31  # Multiline string - 32  description|                                        - 33    This is a multiline                                 - 34    string in YAML. - 35   - 36  # Inline and nested collections - 37  colors: { redFF0000green00FF00blue0000FF }  - 38   + + + +  1  # This is a comment in YAML +  2   +  3  # Scalars +  4  string"Hello, world!" +  5  integer42 +  6  float3.14 +  7  booleantrue +  8   +  9  # Sequences (Arrays) + 10  fruits:                                               + 11    - Apple + 12    - Banana + 13    - Cherry + 14   + 15  # Nested sequences + 16  persons:                                              + 17    - nameJohn + 18  age28 + 19  is_studentfalse + 20    - nameJane + 21  age22 + 22  is_studenttrue + 23   + 24  # Mappings (Dictionaries) + 25  address:                                              + 26  street123 Main St + 27  cityAnytown + 28  stateCA + 29  zip'12345' + 30   + 31  # Multiline string + 32  description|                                        + 33    This is a multiline                                 + 34    string in YAML. + 35   + 36  # Inline and nested collections + 37  colors: { redFF0000green00FF00blue0000FF }  + 38   diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index c1aa06e7a2..449e17db33 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -3,7 +3,7 @@ import pytest from tests.snapshot_tests.language_snippets import SNIPPETS -from textual.document import Selection, VALID_LANGUAGES +from textual.widgets.text_area import Selection, BUILTIN_LANGUAGES from textual.widgets import TextArea # These paths should be relative to THIS directory. @@ -639,7 +639,7 @@ def test_nested_fr(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "nested_fr.py") -@pytest.mark.parametrize("language", VALID_LANGUAGES) +@pytest.mark.parametrize("language", BUILTIN_LANGUAGES) def test_text_area_language_rendering(language, snap_compare): # This test will fail if we're missing a snapshot test for a valid # language. We should have a snapshot test for each language we support diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index 66f086cf88..4cf8602e0a 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -8,8 +8,8 @@ import pytest from textual.app import App, ComposeResult -from textual.document import EditResult, Selection from textual.widgets import TextArea +from textual.widgets.text_area import EditResult, Selection TEXT = """\ I must not fear. diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index 4c6d1e1872..a8685b4456 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -9,8 +9,8 @@ import pytest from textual.app import App, ComposeResult -from textual.document import Selection from textual.widgets import TextArea +from textual.widgets.text_area import Selection TEXT = """I must not fear. Fear is the mind-killer. diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py index 968aba1aed..d089aecc0f 100644 --- a/tests/text_area/test_selection.py +++ b/tests/text_area/test_selection.py @@ -1,8 +1,8 @@ import pytest from textual.app import App, ComposeResult -from textual.document import Selection from textual.widgets import TextArea +from textual.widgets.text_area import Selection TEXT = """I must not fear. Fear is the mind-killer. diff --git a/tests/text_area/test_selection_bindings.py b/tests/text_area/test_selection_bindings.py index d8def8857a..76d4586df4 100644 --- a/tests/text_area/test_selection_bindings.py +++ b/tests/text_area/test_selection_bindings.py @@ -1,9 +1,9 @@ import pytest from textual.app import App, ComposeResult -from textual.document import Document, Selection from textual.geometry import Offset from textual.widgets import TextArea +from textual.widgets.text_area import Document, Selection TEXT = """I must not fear. Fear is the mind-killer. From 42fdf7e8b40f8602af65c5bfb4a68713ba39d93b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 8 Sep 2023 09:59:01 +0100 Subject: [PATCH 321/366] Merging main --- .faq/FAQ.md | 10 +- CHANGELOG.md | 47 +- CONTRIBUTING.md | 120 +++++ docs/FAQ.md | 46 +- docs/blog/posts/textual-web.md | 45 ++ docs/events/blur.md | 6 + docs/events/descendant_blur.md | 6 + docs/events/descendant_focus.md | 6 + docs/events/focus.md | 6 + docs/examples/guide/screens/modes01.py | 42 ++ docs/examples/widgets/horizontal_rules.py | 27 ++ docs/examples/widgets/horizontal_rules.tcss | 13 + docs/examples/widgets/vertical_rules.py | 27 ++ docs/examples/widgets/vertical_rules.tcss | 14 + docs/guide/CSS.md | 26 +- docs/guide/app.md | 39 ++ docs/guide/screens.md | 60 +++ docs/images/screens/modes1.excalidraw.svg | 16 + docs/images/screens/modes2.excalidraw.svg | 16 + docs/widget_gallery.md | 10 + docs/widgets/rule.md | 75 +++ mkdocs-nav.yml | 1 + poetry.lock | 18 +- pyproject.toml | 8 +- questions/align-center-middle.question.md | 5 + src/textual/__init__.py | 12 +- src/textual/_slug.py | 116 +++++ src/textual/app.py | 67 ++- src/textual/constants.py | 6 + src/textual/css/_style_properties.py | 6 +- src/textual/css/_styles_builder.py | 4 +- src/textual/dom.py | 16 +- src/textual/drivers/_byte_stream.py | 2 +- src/textual/drivers/_input_reader.py | 10 + src/textual/drivers/_input_reader_linux.py | 49 ++ src/textual/drivers/_input_reader_windows.py | 37 ++ src/textual/drivers/linux_driver.py | 5 +- src/textual/drivers/web_driver.py | 104 +++-- src/textual/drivers/win32.py | 20 +- src/textual/events.py | 4 + src/textual/message.py | 1 - src/textual/message_pump.py | 8 +- src/textual/reactive.py | 16 +- src/textual/screen.py | 39 +- src/textual/widget.py | 12 +- src/textual/widgets/__init__.py | 2 + src/textual/widgets/__init__.pyi | 1 + src/textual/widgets/_button.py | 2 +- src/textual/widgets/_data_table.py | 19 +- src/textual/widgets/_directory_tree.py | 2 +- src/textual/widgets/_input.py | 5 +- src/textual/widgets/_list_view.py | 4 +- src/textual/widgets/_markdown.py | 6 +- src/textual/widgets/_radio_set.py | 4 +- src/textual/widgets/_rule.py | 217 +++++++++ src/textual/widgets/_select.py | 2 +- src/textual/widgets/_switch.py | 2 +- src/textual/widgets/_toggle_button.py | 2 +- src/textual/widgets/_tree.py | 24 +- src/textual/widgets/rule.py | 8 + tests/input/test_input_mouse.py | 57 ++- .../__snapshots__/test_snapshots.ambr | 433 +++++++++++++++--- .../snapshot_tests/snapshot_apps/auto_grid.py | 2 + tests/snapshot_tests/test_snapshots.py | 8 + tests/test_app.py | 39 ++ ...all_later.py => test_call_x_schedulers.py} | 0 tests/test_focus.py | 108 +++++ tests/test_message_pump.py | 36 ++ tests/test_reactive.py | 78 ++++ tests/test_rule.py | 26 ++ tests/test_slug.py | 62 +++ tests/test_visibility_change.py | 43 -- tests/test_visible.py | 78 ++++ 73 files changed, 2204 insertions(+), 289 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/blog/posts/textual-web.md create mode 100644 docs/examples/guide/screens/modes01.py create mode 100644 docs/examples/widgets/horizontal_rules.py create mode 100644 docs/examples/widgets/horizontal_rules.tcss create mode 100644 docs/examples/widgets/vertical_rules.py create mode 100644 docs/examples/widgets/vertical_rules.tcss create mode 100644 docs/images/screens/modes1.excalidraw.svg create mode 100644 docs/images/screens/modes2.excalidraw.svg create mode 100644 docs/widgets/rule.md create mode 100644 src/textual/_slug.py create mode 100644 src/textual/drivers/_input_reader.py create mode 100644 src/textual/drivers/_input_reader_linux.py create mode 100644 src/textual/drivers/_input_reader_windows.py create mode 100644 src/textual/widgets/_rule.py create mode 100644 src/textual/widgets/rule.py rename tests/{test_call_later.py => test_call_x_schedulers.py} (100%) create mode 100644 tests/test_rule.py create mode 100644 tests/test_slug.py delete mode 100644 tests/test_visibility_change.py create mode 100644 tests/test_visible.py diff --git a/.faq/FAQ.md b/.faq/FAQ.md index b9ece60228..0dd6a22d3b 100644 --- a/.faq/FAQ.md +++ b/.faq/FAQ.md @@ -8,14 +8,20 @@ hide: # Frequently Asked Questions + +Welcome to the Textual FAQ. +Here we try and answer any question that comes up frequently. +If you can't find what you are looking for here, see our other [help](./help.md) channels. + {%- for question in questions %} + ## {{ question.title }} {{ question.body }} -{%- endfor %} +--- -
+{%- endfor %} Generated by [FAQtory](https://github.com/willmcgugan/faqtory) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6626a0b4cb..aeaecf2074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,42 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## Unreleased +## [0.36.0] - 2023-09-05 + +### Added + +- TCSS styles `layer` and `layers` can be strings https://github.com/Textualize/textual/pull/3169 +- `App.return_code` for the app return code https://github.com/Textualize/textual/pull/3202 +- Added `animate` switch to `Tree.scroll_to_line` and `Tree.scroll_to_node` https://github.com/Textualize/textual/pull/3210 +- Added `Rule` widget https://github.com/Textualize/textual/pull/3209 +- Added App.current_mode to get the current mode https://github.com/Textualize/textual/pull/3233 + +### Changed + +- Reactive callbacks are now scheduled on the message pump of the reactable that is watching instead of the owner of reactive attribute https://github.com/Textualize/textual/pull/3065 +- Callbacks scheduled with `call_next` will now have the same prevented messages as when the callback was scheduled https://github.com/Textualize/textual/pull/3065 +- Added `cursor_type` to the `DataTable` constructor. +- Fixed `push_screen` not updating Screen.CSS styles https://github.com/Textualize/textual/issues/3217 + +### Fixed + +- Fixed flicker when calling pop_screen multiple times https://github.com/Textualize/textual/issues/3126 +- Fixed setting styles.layout not updating https://github.com/Textualize/textual/issues/3047 +- Fixed flicker when scrolling tree up or down a line https://github.com/Textualize/textual/issues/3206 + +## [0.35.1] + +### Fixed + +- Fixed flash of 80x24 interface in textual-web + +## [0.35.0] ### Added - Ability to enable/disable tabs via the reactive `disabled` in tab panes https://github.com/Textualize/textual/pull/3152 +- Textual-web driver support for Windows + ### Fixed - Could not hide/show/disable/enable tabs in nested `TabbedContent` https://github.com/Textualize/textual/pull/3150 @@ -35,6 +66,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed auto height container with default grid-rows https://github.com/Textualize/textual/issues/1597 - Fixed `page_up` and `page_down` bug in `DataTable` when `show_header = False` https://github.com/Textualize/textual/pull/3093 +- Fixed issue with visible children inside invisible container when moving focus https://github.com/Textualize/textual/issues/3053 ## [0.33.0] - 2023-08-15 @@ -46,6 +78,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed background refresh https://github.com/Textualize/textual/issues/3055 - Fixed `SelectionList.clear_options` https://github.com/Textualize/textual/pull/3075 - `MouseMove` events bubble up from widgets. `App` and `Screen` receive `MouseMove` events even if there's no Widget under the cursor. https://github.com/Textualize/textual/issues/2905 +- Fixed click on double-width char https://github.com/Textualize/textual/issues/2968 + +### Changed + +- Breaking change: `DOMNode.visible` now takes into account full DOM to report whether a node is visible or not. + +### Removed + +- Property `Widget.focusable_children` https://github.com/Textualize/textual/pull/3070 ### Added @@ -58,6 +99,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - DescendantBlur and DescendantFocus can now be used with @on decorator + ## [0.32.0] - 2023-08-03 ### Added @@ -1205,6 +1247,9 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[0.36.0]: https://github.com/Textualize/textual/compare/v0.35.1...v0.36.0 +[0.35.1]: https://github.com/Textualize/textual/compare/v0.35.0...v0.35.1 +[0.35.0]: https://github.com/Textualize/textual/compare/v0.34.0...v0.35.0 [0.34.0]: https://github.com/Textualize/textual/compare/v0.33.0...v0.34.0 [0.33.0]: https://github.com/Textualize/textual/compare/v0.32.0...v0.33.0 [0.32.0]: https://github.com/Textualize/textual/compare/v0.31.0...v0.32.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..ea95ebd28e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,120 @@ +# Contributing Guidelines + +🎉 **First of all, thanks for taking the time to contribute!** 🎉 + +## 🤔 How can I contribute? + +**1.** Fix issue + +**2.** Report bug + +**3.** Improve Documentation + + +## Setup 🚀 +You need to set up Textualize to make your contribution. Textual requires Python 3.7 or later (if you have a choice, pick the most recent Python). Textual runs on Linux, macOS, Windows, and probably any OS where Python also runs. + +### Installation + +**Install Texualize via pip:** +```bash +pip install textual +``` +**Install [Poetry](https://python-poetry.org/)** +```bash +curl -sSL https://install.python-poetry.org | python3 - +``` +**To install all dependencies, run:** +```bash +poetry install --all +``` +**Make sure everything works fine:** +```bash +textual --version +``` +### Demo + +Once you have Textual installed, run the following to get an impression of what it can do: + +```bash +python -m textual +``` +If Texualize is installed, you should see this: +demo + +## Make contribution +**1.** Fork [this](repo) repository. + +**2.** Clone the forked repository. + +```bash +git clone https://github.com//textual.git +``` + +**3.** Navigate to the project directory. + +```bash +cd textual +``` + +**4.** Create a new [pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) + + +### 📣 Pull Requests(PRs) + +The process described here should check off these goals: + +- [x] Maintain the project's quality. +- [x] Fix problems that are important to users. +- [x] The CHANGELOG.md was updated; +- [x] Your code was formatted with black (make format); +- [x] All of your code has docstrings in the style of the rest of the codebase; +- [x] your code passes all tests (make test); and +- [x] You added documentation when needed. + +### After the PR 🥳 +When you open a PR, your code will be reviewed by one of the Textual maintainers. +In that review process, + +- We will take a look at all of the changes you are making; +- We might ask for clarifications (why did you do X or Y?); +- We might ask for more tests/more documentation; and +- We might ask for some code changes. + +The sole purpose of those interactions is to make sure that, in the long run, everyone has the best experience possible with Textual and with the feature you are implementing/fixing. + +Don't be discouraged if a reviewer asks for code changes. +If you go through our history of pull requests, you will see that every single one of the maintainers has had to make changes following a review. + + + +## 🛑 Important + +- Make sure to read the issue instructions carefully. If you are a newbie you should look out for some good first issues because they should be clear enough and sometimes even provide some hints. If something isn't clear, ask for clarification! + +- Add docstrings to all of your code (functions, methods, classes, ...). The codebase should have enough examples for you to copy from. + +- Write tests for your code. + +- If you are fixing a bug, make sure to add regression tests that link to the original issue. + +- If you are implementing a visual element, make sure to add snapshot tests. See below for more details. + + +### Snapshot Testing +Snapshot tests ensure that things like widgets look like they are supposed to. +PR [#1969](https://github.com/Textualize/textual/pull/1969) is a good example of what adding snapshot tests means: it amounts to a change in the file ```tests/snapshot_tests/test_snapshots.py```, that should run an app that you write and compare it against a historic snapshot of what that app should look like. + +When you create a new snapshot test, run it with ```pytest -vv tests/snapshot_tests/test_snapshots.py.``` +Because you just created this snapshot test, there is no history to compare against and the test will fail automatically. +After running the snapshot tests, you should see a link that opens an interface in your browser. +This interface should show all failing snapshot tests and a side-by-side diff between what the app looked like when it ran VS the historic snapshot. + +Make sure your snapshot app looks like it is supposed to and that you didn't break any other snapshot tests. +If that's the case, you can run ```make test-snapshot-update``` to update the snapshot history with your new snapshot. +This will write to the file ```tests/snapshot_tests/__snapshots__/test_snapshots.ambr```, that you should NOT modify by hand + + +### 📈Join the community + +- 😕 Seems a little overwhelming? Join our community on [Discord](https://discord.gg/uNRPEGCV) to get help. diff --git a/docs/FAQ.md b/docs/FAQ.md index 45c94099f4..4b280e9dda 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -8,12 +8,21 @@ hide: # Frequently Asked Questions + +Welcome to the Textual FAQ. +Here we try and answer any question that comes up frequently. +If you can't find what you are looking for here, see our other [help](./help.md) channels. + + ## Does Textual support images? Textual doesn't have built-in support for images yet, but it is on the [Roadmap](https://textual.textualize.io/roadmap/). See also the [rich-pixels](https://github.com/darrenburns/rich-pixels) project for a Rich renderable for images that works with Textual. +--- + + ## How can I fix ImportError cannot import name ComposeResult from textual.app ? You likely have an older version of Textual. You can install the latest version by adding the `-U` switch which will force pip to upgrade. @@ -24,6 +33,9 @@ The following should do it: pip install textual-dev -U ``` +--- + + ## How can I select and copy text in a Textual app? Running a Textual app puts your terminal in to *application mode* which disables clicking and dragging to select text. @@ -36,6 +48,9 @@ may expect from the command line. The exact modifier key depends on the terminal Refer to the documentation for your terminal emulator, if it is not listed above. +--- + + ## How can I set a translucent app background? Some terminal emulators have a translucent background feature which allows the desktop underneath to be partially visible. @@ -45,8 +60,16 @@ Textual uses 16.7 million colors where available which enables consistent colors For more information on ANSI colors in Textual, see [Why no Ansi Themes?](#why-doesnt-textual-support-ansi-themes). +--- + + ## How do I center a widget in a screen? +!!! tip + + See [*How To Center Things*](https://textual.textualize.io/how-to/center-things/) in the + Textual documentation for a more comprensive answer to this question. + To center a widget within a container use [`align`](https://textual.textualize.io/styles/align/). But remember that `align` works on the *children* of a container, it isn't something you use @@ -134,6 +157,9 @@ if __name__ == "__main__": ButtonApp().run() ``` +--- + + ## How do I fix WorkerDeclarationError? Textual version 0.31.0 requires that you set `thread=True` on the `@work` decorator if you want to run a threaded worker. @@ -156,6 +182,9 @@ async def run_in_background(): This change was made because it was too easy to accidentally create a threaded worker, which may produce unexpected results. +--- + + ## How do I pass arguments to an app? When creating your `App` class, override `__init__` as you would when @@ -189,6 +218,9 @@ Greetings(to_greet="davep").run() Greetings("Well hello", "there").run() ``` +--- + + ## No widget called TextLog The `TextLog` widget was renamed to `RichLog` in Textual 0.32.0. @@ -201,6 +233,9 @@ Here's how you should import RichLog: from textual.widgets import RichLog ``` +--- + + ## Why do some key combinations never make it to my app? Textual can only ever support key combinations that are passed on by your @@ -230,6 +265,9 @@ If you need to test what [key combinations](https://textual.textualize.io/guide/input/#keyboard-input) work in different environments you can try them out with `textual keys`. +--- + + ## Why doesn't Textual look good on macOS? You may find that the default macOS Terminal.app doesn't render Textual apps (and likely other TUIs) very well, particuarily when it comes to box characters. @@ -265,6 +303,9 @@ We recommend any of the following terminals: Screenshot 2023-06-19 at 11 00 25 +--- + + ## Why doesn't Textual support ANSI themes? Textual will not generate escape sequences for the 16 themeable *ANSI* colors. @@ -278,6 +319,9 @@ Textual has a design system which guarantees apps will be readable on all platfo There is currently a light and dark version of the design system, but more are planned. It will also be possible for users to customize the source colors on a per-app or per-system basis. This means that in the future you will be able to modify the core colors to blend in with your chosen terminal theme. +--- + + ## Why doesn't the `DataTable` scroll programmatically? If scrolling in your `DataTable` is _apparently_ broken, it may be because your `DataTable` is using the default value of `height: auto`. @@ -286,6 +330,6 @@ If you would like the table itself to scroll, set the height to something other **NOTE:** As of Textual v0.31.0 the `max-height` of a `DataTable` is set to `100%`, this will mean that the above is no longer the default experience. -
+--- Generated by [FAQtory](https://github.com/willmcgugan/faqtory) diff --git a/docs/blog/posts/textual-web.md b/docs/blog/posts/textual-web.md new file mode 100644 index 0000000000..e819bb3309 --- /dev/null +++ b/docs/blog/posts/textual-web.md @@ -0,0 +1,45 @@ +--- +draft: false +date: 2023-09-06 +categories: + - News +title: "What is Textual Web?" +authors: + - willmcgugan +--- + +# What is Textual Web? + +If you know us, you will know that we are the team behind [Rich](https://github.com/Textualize/rich) and [Textual](https://github.com/Textualize/textual) — two popular Python libraries that work magic in the terminal. + +!!! note + + Not to mention [Rich-CLI](https://github.com/Textualize/rich-cli), [Trogon](https://github.com/Textualize/trogon), and [Frogmouth](https://github.com/Textualize/frogmouth) + +Today we are adding one project more to that lineup: [textual-web](https://github.com/Textualize/textual-web). + + + + +Textual Web takes a Textual-powered TUI and turns it in to a web application. +Here's a video of that in action: + +
+ +
+ +With the `textual-web` command you can publish any Textual app on the web, making it available to anyone you send the URL to. +This works without creating a socket server on your machine, so you won't have to configure firewalls and ports to share your applications. + +We're excited about the possibilities here. +Textual web apps are fast to spin up and tear down, and they can run just about anywhere that has an outgoing internet connection. +They can be built by a single developer without any experience with a traditional web stack. +All you need is proficiency in Python and a little time to read our [lovely docs](https://textual.textualize.io/). + +Future releases will expose more of the Web platform APIs to Textual apps, such as notifications and file system access. +We plan to do this in a way that allows the same (Python) code to drive those features. +For instance, a Textual app might save a file to disk in a terminal, but offer to download it in the browser. + +Also in the pipeline is [PWA](https://en.wikipedia.org/wiki/Progressive_web_app) support, so you can build terminal apps, web apps, and desktop apps with a single codebase. + +Textual Web is currently in a public beta. Join our [Discord server](https://discord.gg/Enf6Z3qhVr) if you would like to help us test, or if you have any questions. diff --git a/docs/events/blur.md b/docs/events/blur.md index 067e7bde9d..df317c5f45 100644 --- a/docs/events/blur.md +++ b/docs/events/blur.md @@ -12,3 +12,9 @@ _No other attributes_ ## Code ::: textual.events.Blur + +## See also + +- [DescendantBlur](descendant_blur.md) +- [DescendantFocus](descendant_focus.md) +- [Focus](focus.md) diff --git a/docs/events/descendant_blur.md b/docs/events/descendant_blur.md index bfe0799f68..c2f447b1f4 100644 --- a/docs/events/descendant_blur.md +++ b/docs/events/descendant_blur.md @@ -12,3 +12,9 @@ _No other attributes_ ## Code ::: textual.events.DescendantBlur + +## See also + +- [Blur](blur.md) +- [DescendantFocus](descendant_focus.md) +- [Focus](focus.md) diff --git a/docs/events/descendant_focus.md b/docs/events/descendant_focus.md index 9090cd65d4..9eb3821805 100644 --- a/docs/events/descendant_focus.md +++ b/docs/events/descendant_focus.md @@ -12,3 +12,9 @@ _No other attributes_ ## Code ::: textual.events.DescendantFocus + +## See also + +- [Blur](blur.md) +- [DescendantBlur](descendant_blur.md) +- [Focus](focus.md) diff --git a/docs/events/focus.md b/docs/events/focus.md index 54f4b2a486..e2c710f115 100644 --- a/docs/events/focus.md +++ b/docs/events/focus.md @@ -12,3 +12,9 @@ _No other attributes_ ## Code ::: textual.events.Focus + +## See also + +- [Blur](blur.md) +- [DescendantBlur](descendant_blur.md) +- [DescendantFocus](descendant_focus.md) diff --git a/docs/examples/guide/screens/modes01.py b/docs/examples/guide/screens/modes01.py new file mode 100644 index 0000000000..c56741dddd --- /dev/null +++ b/docs/examples/guide/screens/modes01.py @@ -0,0 +1,42 @@ +from textual.app import App, ComposeResult +from textual.screen import Screen +from textual.widgets import Footer, Placeholder + + +class DashboardScreen(Screen): + def compose(self) -> ComposeResult: + yield Placeholder("Dashboard Screen") + yield Footer() + + +class SettingsScreen(Screen): + def compose(self) -> ComposeResult: + yield Placeholder("Settings Screen") + yield Footer() + + +class HelpScreen(Screen): + def compose(self) -> ComposeResult: + yield Placeholder("Help Screen") + yield Footer() + + +class ModesApp(App): + BINDINGS = [ + ("d", "switch_mode('dashboard')", "Dashboard"), # (1)! + ("s", "switch_mode('settings')", "Settings"), + ("h", "switch_mode('help')", "Help"), + ] + MODES = { + "dashboard": DashboardScreen, # (2)! + "settings": SettingsScreen, + "help": HelpScreen, + } + + def on_mount(self) -> None: + self.switch_mode("dashboard") # (3)! + + +if __name__ == "__main__": + app = ModesApp() + app.run() diff --git a/docs/examples/widgets/horizontal_rules.py b/docs/examples/widgets/horizontal_rules.py new file mode 100644 index 0000000000..643f129bbe --- /dev/null +++ b/docs/examples/widgets/horizontal_rules.py @@ -0,0 +1,27 @@ +from textual.app import App, ComposeResult +from textual.containers import Vertical +from textual.widgets import Label, Rule + + +class HorizontalRulesApp(App): + CSS_PATH = "horizontal_rules.tcss" + + def compose(self) -> ComposeResult: + with Vertical(): + yield Label("solid (default)") + yield Rule() + yield Label("heavy") + yield Rule(line_style="heavy") + yield Label("thick") + yield Rule(line_style="thick") + yield Label("dashed") + yield Rule(line_style="dashed") + yield Label("double") + yield Rule(line_style="double") + yield Label("ascii") + yield Rule(line_style="ascii") + + +if __name__ == "__main__": + app = HorizontalRulesApp() + app.run() diff --git a/docs/examples/widgets/horizontal_rules.tcss b/docs/examples/widgets/horizontal_rules.tcss new file mode 100644 index 0000000000..fad6140e1f --- /dev/null +++ b/docs/examples/widgets/horizontal_rules.tcss @@ -0,0 +1,13 @@ +Screen { + align: center middle; +} + +Vertical { + height: auto; + width: 80%; +} + +Label { + width: 100%; + text-align: center; +} diff --git a/docs/examples/widgets/vertical_rules.py b/docs/examples/widgets/vertical_rules.py new file mode 100644 index 0000000000..5001045305 --- /dev/null +++ b/docs/examples/widgets/vertical_rules.py @@ -0,0 +1,27 @@ +from textual.app import App, ComposeResult +from textual.containers import Horizontal +from textual.widgets import Label, Rule + + +class VerticalRulesApp(App): + CSS_PATH = "vertical_rules.tcss" + + def compose(self) -> ComposeResult: + with Horizontal(): + yield Label("solid") + yield Rule(orientation="vertical") + yield Label("heavy") + yield Rule(orientation="vertical", line_style="heavy") + yield Label("thick") + yield Rule(orientation="vertical", line_style="thick") + yield Label("dashed") + yield Rule(orientation="vertical", line_style="dashed") + yield Label("double") + yield Rule(orientation="vertical", line_style="double") + yield Label("ascii") + yield Rule(orientation="vertical", line_style="ascii") + + +if __name__ == "__main__": + app = VerticalRulesApp() + app.run() diff --git a/docs/examples/widgets/vertical_rules.tcss b/docs/examples/widgets/vertical_rules.tcss new file mode 100644 index 0000000000..f2148af1c0 --- /dev/null +++ b/docs/examples/widgets/vertical_rules.tcss @@ -0,0 +1,14 @@ +Screen { + align: center middle; +} + +Horizontal { + width: auto; + height: 80%; +} + +Label { + width: 6; + height: 100%; + text-align: center; +} diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md index 8bf7f60aa1..0d38a616cb 100644 --- a/docs/guide/CSS.md +++ b/docs/guide/CSS.md @@ -2,13 +2,13 @@ Textual uses CSS to apply style to widgets. If you have any exposure to web development you will have encountered CSS, but don't worry if you haven't: this chapter will get you up to speed. -## Stylesheets +!!! tip "VSCode User?" -CSS stands for _Cascading Stylesheets_. A stylesheet is a list of styles and rules about how those styles should be applied to a web page. In the case of Textual, the stylesheet applies [styles](./styles.md) to widgets, but otherwise it is the same idea. + The official [Textual CSS](https://marketplace.visualstudio.com/items?itemName=Textualize.textual-syntax-highlighter) extension adds syntax highlighting for both external files and inline CSS. -When Textual loads CSS it sets attributes on your widgets' `style` object. The effect is the same as if you had set attributes in Python. +## Stylesheets -CSS is typically stored in an external file with the extension `.css` alongside your Python code. +CSS stands for _Cascading Stylesheet_. A stylesheet is a list of styles and rules about how those styles should be applied to a web page. In the case of Textual, the stylesheet applies [styles](./styles.md) to widgets, but otherwise it is the same idea. Let's look at some Textual CSS. @@ -52,6 +52,7 @@ The lines inside the curly braces contains CSS _rules_, which consist of a rule The first rule in the above example reads `"dock: top;"`. The rule name is `dock` which tells Textual to place the widget on an edge of the screen. The text after the colon is `top` which tells Textual to dock to the _top_ of the screen. Other valid values for `dock` are "right", "bottom", or "left"; but "top" is most appropriate for a header. + ## The DOM The DOM, or _Document Object Model_, is a term borrowed from the web world. Textual doesn't use documents but the term has stuck. In Textual CSS, the DOM is an arrangement of widgets you can visualize as a tree-like structure. @@ -112,11 +113,10 @@ To further explore the DOM, we're going to build a simple dialog with a question - `textual.widgets.Static` For simple content. - `textual.widgets.Button` For a clickable button. -=== "dom3.py" - ```python hl_lines="12 13 14 15 16 17 18 19 20" - --8<-- "docs/examples/guide/dom3.py" - ``` +```python hl_lines="12 13 14 15 16 17 18 19 20" title="dom3.py" +--8<-- "docs/examples/guide/dom3.py" +``` We've added a Container to our DOM which (as the name suggests) is a container for other widgets. The container has a number of other widgets passed as positional arguments which will be added as the children of the container. Not all widgets accept child widgets in this way. A Button widget doesn't require any children, for example. @@ -138,7 +138,13 @@ You may recognize some elements in the above screenshot, but it doesn't quite lo To add a stylesheet set the `CSS_PATH` classvar to a relative path: -```python hl_lines="9" + +!!! note + + Textual CSS files are typically given the extension `.tcss` to differentiate them from browser CSS (`.css`). + + +```python hl_lines="9" title="dom4.py" --8<-- "docs/examples/guide/dom4.py" ``` @@ -147,7 +153,7 @@ These are used by the CSS to identify parts of the DOM. We will cover these in t Here's the CSS file we are applying: -```sass +```sass title="dom4.tcss" --8<-- "docs/examples/guide/dom4.tcss" ``` diff --git a/docs/guide/app.md b/docs/guide/app.md index bcdf1183be..648847ec28 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -204,11 +204,50 @@ The addition of `[str]` tells mypy that `run()` is expected to return a string. Type annotations are entirely optional (but recommended) with Textual. +### Return code + +When you exit a Textual app with [`App.exit()`][textual.app.App.exit], you can optionally specify a *return code* with the `return_code` parameter. + + +!!! info "What are return codes?" + + Returns codes are a standard feature provided by your operating system. + When any application exits it can return an integer to indicate if it was successful or not. + A return code of `0` indicates success, any other value indicates that an error occurred. + The exact meaning of a non-zero return code is application-dependant. + +When a Textual app exits normally, the return code will be `0`. If there is an unhandled exception, Textual will set a return code of `1`. +You may want to set a different value for the return code if there is error condition that you want to differentiate from an unhandled exception. + +Here's an example of setting a return code for an error condition: + +```python +if critical_error: + self.exit(return_code=4, message="Critical error occurred") +``` + +The app's return code can be queried with `app.return_code`, which will be `None` if it hasn't been set, or an integer. + +Textual won't explicitly exit the process. +To exit the app with a return code, you should call `sys.exit`. +Here's how you might do that: + +```python +if __name__ == "__main__" + app = MyApp() + app.run() + import sys + sys.exit(app.return_code or 0) +``` ## CSS Textual apps can reference [CSS](CSS.md) files which define how your app and widgets will look, while keeping your Python code free of display related code (which tends to be messy). +!!! info + + Textual apps typically use the extension `.tcss` for external CSS files to differentiate them from browser (`.css`) files. + The chapter on [Textual CSS](CSS.md) describes how to use CSS in detail. For now let's look at how your app references external CSS files. The following example enables loading of CSS by adding a `CSS_PATH` class variable: diff --git a/docs/guide/screens.md b/docs/guide/screens.md index ca964edb8a..b9aefdc175 100644 --- a/docs/guide/screens.md +++ b/docs/guide/screens.md @@ -256,3 +256,63 @@ Returning data in this way can help keep your code manageable by making it easy You may have noticed in the previous example that we changed the base class to `ModalScreen[bool]`. The addition of `[bool]` adds typing information that tells the type checker to expect a boolean in the call to `dismiss`, and that any callback set in `push_screen` should also expect the same type. As always, typing is optional in Textual, but this may help you catch bugs. + + +## Modes + +Some apps may benefit from having multiple screen stacks, rather than just one. +Consider an app with a dashboard screen, a settings screen, and a help screen. +These are independent in the sense that we don't want to prevent the user from switching between them, even if there are one or more modal screens on the screen stack. +But we may still want each individual screen to have a navigation stack where we can push and pop screens. + +In Textual we can manage this with *modes*. +A mode is simply a named screen stack, which we can switch between as required. +When we switch modes, the topmost screen in the new mode becomes the active visible screen. + +The following diagram illustrates such an app with modes. +On startup the app switches to the "dashboard" mode which makes the top of the stack visible. + +
+--8<-- "docs/images/screens/modes1.excalidraw.svg" +
+ +If we later change the mode to "settings", the top of that mode's screen stack becomes visible. + +
+--8<-- "docs/images/screens/modes2.excalidraw.svg" +
+ +To add modes to your app, define a [`MODES`][textual.app.App.MODES] class variable in your App class which should be a `dict` that maps the name of the mode on to either a screen object, a callable that returns a screen, or the name of an installed screen. +However you specify it, the values in `MODES` set the base screen for each mode's screen stack. + +You can switch between these screens at any time by calling [`App.switch_mode`][textual.app.App.switch_mode]. +When you switch to a new mode, the topmost screen in the new stack becomes visible. +Any calls to [`App.push_screen`][textual.app.App.push_screen] or [`App.pop_screen`][textual.app.App.pop_screen] will affect only the active mode. + +Let's look at an example with modes: + +=== "modes01.py" + + ```python hl_lines="25-29 30-34 37" + --8<-- "docs/examples/guide/screens/modes01.py" + ``` + + 1. `switch_mode` is a builtin action to switch modes. + 2. Associates `DashboardScreen` with the name "dashboard". + 3. Switches to the dashboard mode. + +=== "Output" + + ```{.textual path="docs/examples/guide/screens/modes01.py"} + ``` + +=== "Output (after pressing S)" + + ```{.textual path="docs/examples/guide/screens/modes01.py", press="s"} + ``` + +Here we have defined three screens. +One for a dashboard, one for settings, and one for help. +We've bound keys to each of these screens, so the user can switch between the screens. + +Pressing ++d++, ++s++, or ++h++ switches between these modes. diff --git a/docs/images/screens/modes1.excalidraw.svg b/docs/images/screens/modes1.excalidraw.svg new file mode 100644 index 0000000000..d57f8c6b2f --- /dev/null +++ b/docs/images/screens/modes1.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2aW0/bSFx1MDAxNMff+ylQ9mVXKu7cL5VWK6ClXHUwMDA1UmhJuZRtVTn2JPHGsY3tJEDFd99jw8ZcdTAwMTdcYiSkXHUwMDA0Km1cdTAwMWWCPWfsOTPz+885M+HHi5WVRnpcdTAwMWWZxuuVhjlzbN9zY3vceJmVj0yceGFcdTAwMDAmkt8n4TB28pq9NI2S169eXHLsuG/SyLdcdTAwMWRjjbxkaPtJOnS90HLCwSsvNYPkr+x711x1MDAxZZg/o3DgprFVNLJqXFwvXHLjq7aMb1x1MDAwNiZIXHUwMDEzePvfcL+y8iP/LnnnevYgXGbcvHpuKLmnab10N1xmclcx5YpcdTAwMTOJNZrU8JI30FpqXFwwd8BjU1iyosaRjYZ7zeh78+JTW9uO2t48+/ChaLbj+X4rPfdzp5JcdTAwMTD6UtiSNFx1MDAwZfvmyHPTXtZ2rXzaU3E47PZcdTAwMDKTJJVnwsh2vPRcdTAwMWPKeOG7XHUwMDFkdPNXXHUwMDE0JWdwtyotjVx00pxcdTAwMTFOlGKMTMxXz1OLXHUwMDEwwlx1MDAwNYyT0lxuXHUwMDA2pObYRujDPIBjv6H8U7jWtp1+XHUwMDE3/Fx1MDAwYtyiXHUwMDBl5rbd7lx1MDAxNHXG190lQlpMM1x1MDAwMe1ThbVWk1x1MDAxYT3jdXtp1jvOLCWxkFxiXHUwMDBiTqUo/DD5dGioXHUwMDAwb2BsYshcdTAwMWGPttyci2/lXHUwMDExXHUwMDBi3OtcdTAwMTFcdTAwMGKGvl/4m1x1MDAxOd6WWCqeXHUwMDE5Rq59NelYwDhQxbGkolx1MDAxOCnfXHUwMDBi+vXX+aHTLzjJSy9fzo0n43wqnoogISijbGY8t9+hQ9U8+dTc2Wz7zY3Drffn0f5T4onRvXxyXHUwMDBi5lRrwpiQmFAlK3zChFtcdTAwMDIhSiSlSmjJXHUwMDE2wrNjt1x1MDAxMeKPgyehjGOt0Fx1MDAxMvBkQiMk+Vx1MDAxMvBUpaGo4amFXCLgzVx1MDAxY3SK3ogo01x1MDAxMu6J8665ftzdx1x1MDAwM3fzmdOJLYyBTC44V1xmRlx1MDAxZtEqnlhCXHUwMDA1ilx1MDAxOPArXHUwMDA1XHUwMDExdLHlU1x1MDAxMUdj8yh8YkJcdTAwMTiGJYUsXHUwMDAxUIZcdTAwMTFF0NxcdTAwMTJcdTAwMDClkkxcdTAwMDM0XHUwMDBiaowjiWdcdTAwMDd0dEC3u1EoRu/eXHUwMDFlOKnauaCO/5SA0vv4lDhcdTAwMDOUQ1dcdTAwMDVDWlx1MDAxMF6hkyNkMWCTaSGZYrTu1nxwtlxya7vtXz22g4gxXHUwMDEzVC6DzVIwuFx1MDAxMduZZJrCnM1cZufnL/2NcNPZ+4L39b67ZtZcdTAwMDej95+fNZxUUEtcbqmk1Fx1MDAwMsM3rcGpLSWIJCBhzLhaiM2OK1xyZv+zOTObnN3BJkJKc0g8Z2bT/b56vLcrm+GgXHUwMDFk+73xXHUwMDA2e+tvnjw3Ni3IXCJplkUykucuvFx1MDAwNiu3lIZcdTAwMDSTMYlcdTAwMTFcdTAwMTdcdTAwMTVWmURgXHUwMDE08Fx1MDAxNIxccqRcdTAwMDJ0IViZI0znl89CfzasqTlLbyNcdTAwMTWXwlaNVMVhaYG1Q85cZqp/SFtvdk9OTtaJcS76slx1MDAxZnL1aVxuqD3b6VxyY/P0SShcdTAwMTOWXHUwMDEwkFxcMlx1MDAwNmkoIZKzXG6cXHUwMDEySVx1MDAwYlZRyHUgxSOYL5aCTovymCuLaq6xlkAmYvImnOXk91xuR4Ipw1x1MDAxY5V20o+II+TmisyTc1x1MDAxNtNcdTAwMWVcdTAwMDZpy7vIk0ZVKd20XHUwMDA3nn9embmcU1x1MDAxOKmvXHLXTnrt0I7dr41Gxbzme90gx810qkynnmP7XHUwMDEzc1x1MDAxYUaF1YHmbC8w8ZZbdzuMva5cdTAwMTfY/ue7m4ZcdTAwMWWb95OVwiolg207MZk1z4pcdTAwMWakQqBrarzAXGa2o4ry2eOFQVx0+uLt75zzXvfirFx1MDAxZq7udFx1MDAwZbaeVobsPlx1MDAxNSpMsr0ggTVcdTAwMDfyadiD10SoLYIo5lx1MDAxYWFcYjNqsXOKaVwiXHUwMDE0ylx1MDAxMopqXGJRXGJyJlXKSZ6NXGKh92iek4lFRdgzfrR8/dVbfVTpTVx1MDAwZoDZgVx1MDAxOFwipdbuXHUwMDEz3sH2If9cdTAwMThcdTAwMWSGp7J1vrHzXHUwMDBmYmunb5vPXFx4jHKLYlx1MDAwNblcdTAwMWJHnGOlqvuIXFx5XHUwMDA0Q1qnRCa/R1xuf0RZsIvWRCDCqS6r6dlIXHUwMDBmKTlXOrao9Fx1MDAxMpOmXtBNli+/21p+TFx0lnO0mzt5yLnpPLsl0Vx1MDAxMuHx+MBcdTAwMWZ8XGLGXFydXlx1MDAxY1x1MDAwZtvjvYeJkNTKXHUwMDFmL1x0JcRCXHUwMDAy4p5cdTAwMDagQVx1MDAwNJRU41x1MDAxZtHEXHUwMDAyocLOKtNcdTAwMDfEpukqVLLD21x1MDAwZlShVlkrXHUwMDEwZJDimpaOwO9cdTAwMTChUOAz4z9vS3RtKPgpze1ol1x1MDAxY21/POqf4vFWa20zSnbtuNjqVWCz4zhcdTAwMWM3JpbL66s7XHUwMDE0rjQofJ5fpVx1MDAxNlP4mpN6I7Py+8hLvLZv/liuyqe3/jOUfjX4t0mdsHrpROqwXHUwMDA3k0rL2bebd9PwJEqX91x0PYt0QnPKXHUwMDE51Vx1MDAxNFx1MDAxM1ngklx1MDAxZqxAMOZIXHSuXGLnTEl2x1HIXHUwMDAyQkdcdTAwMTZWXHUwMDFjgStMZb9OXHUwMDEzSW45XGZhzJKIXHUwMDExgYXQWGB9U/lCQFx1MDAwZlx1MDAxOCqWqvulX2j6P1bIdcnlg6LywzWbpHacrnuBXHUwMDBika7q2PU/RGzNXHUwMDEwTXKVO8PMy1VkIcmFJJpjXGJZTFx1MDAxNucm2cjYUbbJgSqUXG5CKMKSiJtdN4FbuFTthZ2kXHUwMDFi4WDgpdD/j6FcdTAwMTek9Vx1MDAxYXmH1jLh9Yx9Q//w5rKtrtAoe2N1+S2uVlxuhvObyfW3l7fWXr2Dr+xzg6zihS/Kf7M1O2+iYUdRK4WZn0xcdTAwMTSg5rnXS27Rz8bIM+P121x1MDAwZbDzT1x1MDAxNlxy8rHOllx1MDAwNpPjePni8l9cdTAwMDSHyVx1MDAxMCJ9 + + + + "dashboard""help""settings"Active (visible) diff --git a/docs/images/screens/modes2.excalidraw.svg b/docs/images/screens/modes2.excalidraw.svg new file mode 100644 index 0000000000..97e38ad8ff --- /dev/null +++ b/docs/images/screens/modes2.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2a2VLbSFx1MDAxNIbv81x1MDAxNJTnNii9L6mammJfMmxcdTAwMDFcdTAwMDJkkkrJUttWLEtCkrFJinef01xuYy1gNrNlfGHsPrL6tPr7z1wi8fPN3FxcKz9PTOv9XFzLjD03XGb81Fx1MDAxZLXe2vEzk2ZBXHUwMDFjgYlcdTAwMTTfs3iYesWRvTxPsvfv3lxy3LRv8iR0PeOcXHUwMDA12dBccrN86Fx1MDAwN7HjxYN3QW5cdTAwMDbZX/Z921x1MDAxZJg/k3jg56lTTjJv/CCP019zmdBcZkyUZ3D2f+D73NzP4r3inVx1MDAxZriDOPKLw1x1MDAwYkPpXHUwMDFlJbw5ulx1MDAxZEeFq5xcdIlcdTAwMTlcIlx1MDAxM3uQLcNcXLnxwdhcdTAwMDF/TWmxQ62j3Z01s7iXu9m8TpLTlW/JsEPLSTtBXHUwMDE47ufnYeFSXHUwMDE2w0pKW5ancd9cdTAwMWNcdTAwMDV+3lx1MDAwMytujE/7VVx1MDAxYVx1MDAwZru9yGRZ7Tdx4npBfm5cdTAwMTeHJoNu1C1OUY6M7Y80dbDWmGtcIpggQtOJ2f6eKe4wXCK1XHUwMDA2XHUwMDEzZVxcNdxaikPYXHUwMDAzcOtcdTAwMGZUvErH2q7X74J3kV9cdTAwMWWDueu2O+Uxo8vFXHUwMDEyIVx1MDAxZKaZUIxRXHUwMDA1zpSz9EzQ7eXWTc5cdTAwMWMlsZBcYlx1MDAwYk6lKP0wxWbAXHUwMDAy7Fx1MDAxOVx1MDAxOJtcdTAwMTjs5MmGXzDxtXq9XCL/8npFwzAs/bWGlSZHVZYq27y/sX7a3/Hzle1w72Rk0sWTxW97k3XVwHPTNFx1MDAxZbUmlovLT6VHw8R3f1x1MDAwMYXh4iuGXHUwMDA1oVKWSIZB1G86XHUwMDFixl6/ZLBcdTAwMTi9eHtv8JlcdTAwMTLTwFx1MDAwNyooJVx1MDAwMle24jb0+3LpQ1x1MDAxYeJvx6j9PTzoxt3R9+HWK0dcdTAwMWbYXHUwMDE2XHUwMDAwNiGMXHUwMDEy1Fx1MDAwMJ9cdEchLlx1MDAwNVx1MDAxMoLCzrCZyO+4bYT405BPQJewUejRyH9ccmxqoqexSTVBmHBy96jMR8nR1tjfpOM12U+Xu0ujT/HglaOpXHUwMDFkiLlSaURcdTAwMTmSVNbYpGClWDFFuCRIK8ZnglNcdTAwMTFPY/MkcGKQXHUwMDE2xoqQ/1x1MDAxN520kiWbJYOWmFx1MDAxM87Znek88Vx1MDAwZdaOt7zOp7PNY7W1tDFYWF7cfkk62W10akxcdTAwMWNCXHUwMDA0x5hSpKSqwVx0VDpcdTAwMWHUXHTVhKRcdTAwMTji60xstlxya/vt36RkuFx1MDAxMU0hlHpcdTAwMTY01TQ0MVKwYCwqaf82NlPlkf7ybrhcdTAwMWLujE829lx1MDAwZvn6SftF61mMboOTXHUwMDBiu+1cdTAwMWFTXHUwMDBiqNSkXHUwMDFlOpkmXHUwMDBlXHUwMDEyXG5ziVx1MDAxONdcdTAwMTQ1/bpnWvelwez3p1x1MDAxM1xuXHUwMDFjhckz0Mn59F6LXG6GIKKgO8OZeWp1NzvrbVxmP6ydjr6vXHUwMDA0KplfeXVwOohcdTAwMTIoppVgRFx1MDAwYlx1MDAwNVA2aJVcdTAwMGXCSENcdTAwMDcmXHUwMDEwpFx1MDAwZVanlUNzhlxixlx1MDAwMlxuXHUwMDFlTORsRSjzhOn8XHUwMDBmitDHpTU34/w6VGGSqYGUI4FcdTAwMTnn8u7dUZet7+xcdTAwMWRcdTAwMWStRHtYXHUwMDFk8uHpeEzaXHUwMDBiU1jtuV5vmJpcdTAwMTdP84QpXHUwMDA3NpxcdMKVRErX2Vx1MDAxNLZcYoXuiCPIKZg+UZ7HXFw5VHONtVx1MDAwNDBcdTAwMTGTV9mkvEkjwZTZPVwiz0GjJlCG03vQWG56XHUwMDFj5fvBXHUwMDBme+GJqo2uuoMgPK/tW4EpXFypLy3fzXrt2E39L61WzbxcdTAwMTBcdTAwMDZdS24rNJ060nngueHEnMdJafVgOjeITLrhN92O06BcdTAwMWJEbnhw89SwYrM+XHRcdTAwMTRO5W5a282MtdpcdTAwMDXyXHUwMDA3ibBcdTAwMWHymlwiXHUwMDE0UHpcIoird+9cdTAwMDPnPZxcdTAwMWZcdTAwMWZ8Xo/ib5tcdTAwMWZHW6uHy8qLXrlcYjFEXFyHKi6FgFpcdTAwMDZcdTAwMTFdr2ckkkWG4IgwTrSa7Vx1MDAwNt00XHUwMDE1XG7lXGJFNWPgXHUwMDAwZqpSlLxcdTAwMWFcdTAwMTVCI4Lv0/rNqsKeXHST51x1MDAxN2Bz1ifVnsDN0Yn2JKOSQ69992Ltx+r4cIVcdTAwMDdq6cTdPmDR2s5cdTAwMGZ/8PFltXd7LyEocygniDJbc6Ayxf2SXHUwMDFlNLpSI6lcdTAwMTDngqjZWompXHSQKEdcdTAwMGLoY1x1MDAwNII0o6tyejXaI5yx+9Rjs2ovM3lcdTAwMWVE3ez59XfdzE+pQYxlc7TMf1RqXHUwMDAxjcHd85//cXfps4/mN8+S4OPSh8V2XHUwMDEwz88/TIOkMf6EXHUwMDFhZNpB0MkjibWwbVJNhERxh2PMidJQqaLqfbcrKlSyw9tcdTAwMGZToYI4QDn0b1xiQ9yrdlx1MDAxZjeIUEDJXGZtwuP1RJeGa1x1MDAxZknhwd/n/vKmiFx1MDAwZldcdTAwMGU6cZxiubpQ3pmowXb/R1Ka2j/PpvBcdTAwMDUvXHUwMDBmzszzars552Oo+teFvk7WeuqTN4K0JETf48HbzTv/XCKqlreK2lxuXG60LyhcdTAwMTSuiuD6LWRcbjlPSlvTaY5cdTAwMTWYb3i+MYOoqVx1MDAwMz08gTDKtOQgVVROM1G11I5iXHUwMDFjXHUwMDBlXHUwMDAxzdubhldFblx1MDAxZlx1MDAwZlx1MDAxMlapXHJuV3kp3/9QIZcjXHUwMDE3XHUwMDBmLH5cdTAwMWYqzyx303wxiHxIanXHLv+lYuNcdTAwMGWJo1x1MDAxMLQ3tF5cIlx1MDAwNyvrk6BcblOoQVx1MDAxMKGVo7puYmOpXHUwMDAztVxmWFx1MDAxMId2XHUwMDFlXHUwMDEyXHUwMDE4u7J2XHUwMDEz+aVP9WW4Wb5cdTAwMTRcdTAwMGZcdTAwMDZBXHUwMDBlXHUwMDE3YDdcdTAwMGWivHlEsaJcdTAwMDUrvJ5xr6hcdTAwMWXOXFy1NVx1MDAxNZrYM9ZDbflprmS4+DL5/PXttUdPx8u+roBVnu5N9a+NzsVcdTAwMDQtN0n2c9j4yT5cdTAwMDFpgX9cdTAwMTlcXMtVts5cdTAwMDIzWrzuZnXxsnG/uNI2MJiCxos3XHUwMDE3/1x1MDAwMiHc1HkifQ== + + + + "dashboard""help""settings"Active diff --git a/docs/widget_gallery.md b/docs/widget_gallery.md index 546e2b33b1..37cbec5160 100644 --- a/docs/widget_gallery.md +++ b/docs/widget_gallery.md @@ -225,6 +225,16 @@ Display and update text in a scrolling panel. ```{.textual path="docs/examples/widgets/rich_log.py" press="H,i"} ``` +## Rule + +A rule widget to separate content, similar to a `
` HTML tag. + +[Rule reference](./widgets/rule.md){ .md-button .md-button--primary } + + +```{.textual path="docs/examples/widgets/horizontal_rules.py"} +``` + ## Select Select from a number of possible options. diff --git a/docs/widgets/rule.md b/docs/widgets/rule.md new file mode 100644 index 0000000000..bc7a2ec1de --- /dev/null +++ b/docs/widgets/rule.md @@ -0,0 +1,75 @@ +# Rule + +A rule widget to separate content, similar to a `
` HTML tag. + +- [ ] Focusable +- [ ] Container + +## Examples + +### Horizontal Rule + +The default orientation of a rule is horizontal. + +The example below shows horizontal rules with all the available line styles. + +=== "Output" + + ```{.textual path="docs/examples/widgets/horizontal_rules.py"} + ``` + +=== "horizontal_rules.py" + + ```python + --8<-- "docs/examples/widgets/horizontal_rules.py" + ``` + +=== "horizontal_rules.tcss" + + ```sass + --8<-- "docs/examples/widgets/horizontal_rules.tcss" + ``` + +### Vertical Rule + +The example below shows vertical rules with all the available line styles. + +=== "Output" + + ```{.textual path="docs/examples/widgets/vertical_rules.py"} + ``` + +=== "vertical_rules.py" + + ```python + --8<-- "docs/examples/widgets/vertical_rules.py" + ``` + +=== "vertical_rules.tcss" + + ```sass + --8<-- "docs/examples/widgets/vertical_rules.tcss" + ``` + +## Reactive Attributes + +| Name | Type | Default | Description | +| ------------- | ----------------- | -------------- | ---------------------------- | +| `orientation` | `RuleOrientation` | `"horizontal"` | The orientation of the rule. | +| `line_style` | `LineStyle` | `"solid"` | The line style of the rule. | + +## Messages + +This widget sends no messages. + +--- + + +::: textual.widgets.Rule + options: + heading_level: 2 + +::: textual.widgets.rule + options: + show_root_heading: true + show_root_toc_entry: true diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index 162fe46da4..26f880ff2c 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -153,6 +153,7 @@ nav: - "widgets/radiobutton.md" - "widgets/radioset.md" - "widgets/rich_log.md" + - "widgets/rule.md" - "widgets/select.md" - "widgets/selection_list.md" - "widgets/sparkline.md" diff --git a/poetry.lock b/poetry.lock index 02dbfb24e9..29b798989d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -659,14 +659,14 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.34" +version = "3.1.35" description = "GitPython is a Python library used to interact with Git repositories" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.34-py3-none-any.whl", hash = "sha256:5d3802b98a3bae1c2b8ae0e1ff2e4aa16bcdf02c145da34d092324f599f01395"}, - {file = "GitPython-3.1.34.tar.gz", hash = "sha256:85f7d365d1f6bf677ae51039c1ef67ca59091c7ebd5a3509aa399d4eda02d6dd"}, + {file = "GitPython-3.1.35-py3-none-any.whl", hash = "sha256:c19b4292d7a1d3c0f653858db273ff8a6614100d1eb1528b014ec97286193c09"}, + {file = "GitPython-3.1.35.tar.gz", hash = "sha256:9cbefbd1789a5fe9bcf621bb34d3f441f3a90c8461d377f84eda73e721d9b06b"}, ] [package.dependencies] @@ -1525,14 +1525,14 @@ extra = ["pygments (>=2.12)"] [[package]] name = "pytest" -version = "7.4.1" +version = "7.4.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.1-py3-none-any.whl", hash = "sha256:460c9a59b14e27c602eb5ece2e47bec99dc5fc5f6513cf924a7d03a578991b1f"}, - {file = "pytest-7.4.1.tar.gz", hash = "sha256:2f2301e797521b23e4d2585a0a3d7b5e50fdddaaf7e7d6773ea26ddb17c213ab"}, + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] @@ -2248,14 +2248,14 @@ files = [ [[package]] name = "types-tree-sitter" -version = "0.20.1.4" +version = "0.20.1.5" description = "Typing stubs for tree-sitter" category = "dev" optional = false python-versions = "*" files = [ - {file = "types-tree-sitter-0.20.1.4.tar.gz", hash = "sha256:673730dcc2efe09be6cdbd9795cdc5243c164262b7a539e6d7e7980fd06c0907"}, - {file = "types_tree_sitter-0.20.1.4-py3-none-any.whl", hash = "sha256:9a38efd62a3cf66f9751c612588b7dbc72340fd6c81fd089c8a0f5877f86b58c"}, + {file = "types-tree-sitter-0.20.1.5.tar.gz", hash = "sha256:94f971599548b90b9bbb6af651d235ad795a094a07651bc565a4b8856caebab1"}, + {file = "types_tree_sitter-0.20.1.5-py3-none-any.whl", hash = "sha256:8d7f9961febbad29789ce5c65f79b95b0702f3d34a7c12fabcd69c36c2bbe184"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 92f98de7f4..33c4df7483 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,10 @@ [tool.poetry] name = "textual" -version = "0.34.0" +version = "0.36.0" homepage = "https://github.com/Textualize/textual" +repository = "https://github.com/Textualize/textual" +documentation = "https://textual.textualize.io/" + description = "Modern Text User Interface framework" authors = ["Will McGugan "] license = "MIT" @@ -33,6 +36,9 @@ include = [ { path = "docs-offline/**/*", format = "sdist" }, ] +[tool.poetry.urls] +"Bug Tracker" = "https://github.com/Textualize/textual/issues" + [tool.poetry.dependencies] python = "^3.7" rich = ">=13.3.3" diff --git a/questions/align-center-middle.question.md b/questions/align-center-middle.question.md index 25e6bd1f84..a33ff239be 100644 --- a/questions/align-center-middle.question.md +++ b/questions/align-center-middle.question.md @@ -9,6 +9,11 @@ alt_titles: - "centre controls" --- +!!! tip + + See [*How To Center Things*](https://textual.textualize.io/how-to/center-things/) in the + Textual documentation for a more comprensive answer to this question. + To center a widget within a container use [`align`](https://textual.textualize.io/styles/align/). But remember that `align` works on the *children* of a container, it isn't something you use diff --git a/src/textual/__init__.py b/src/textual/__init__.py index 46fcf3ec3f..103f9db2fe 100644 --- a/src/textual/__init__.py +++ b/src/textual/__init__.py @@ -64,12 +64,6 @@ def __rich_repr__(self) -> rich.repr.Result: yield self._verbosity, LogVerbosity.NORMAL def __call__(self, *args: object, **kwargs) -> None: - try: - app = active_app.get() - except LookupError: - print_args = (*args, *[f"{key}={value!r}" for key, value in kwargs.items()]) - print(*print_args) - return if constants.LOG_FILE: output = " ".join(str(arg) for arg in args) if kwargs: @@ -80,6 +74,12 @@ def __call__(self, *args: object, **kwargs) -> None: with open(constants.LOG_FILE, "a") as log_file: print(output, file=log_file) + try: + app = active_app.get() + except LookupError: + print_args = (*args, *[f"{key}={value!r}" for key, value in kwargs.items()]) + print(*print_args) + return if app.devtools is None or not app.devtools.is_connected: return diff --git a/src/textual/_slug.py b/src/textual/_slug.py new file mode 100644 index 0000000000..8d23ca4dab --- /dev/null +++ b/src/textual/_slug.py @@ -0,0 +1,116 @@ +"""Provides a utility function and class for creating Markdown-friendly slugs. + +The approach to creating slugs is designed to be as close to +GitHub-flavoured Markdown as possible. However, because there doesn't appear +to be any actual documentation for this 'standard', the code here involves +some guesswork and also some pragmatic shortcuts. + +Expect this to grow over time. + +The main rules used in here at the moment are: + +1. Strip all leading and trailing whitespace. +2. Remove all non-lingual characters (emoji, etc). +3. Remove all punctuation and whitespace apart from dash and underscore. +""" + +from __future__ import annotations + +from collections import defaultdict +from re import compile +from string import punctuation +from typing import Pattern +from urllib.parse import quote + +from typing_extensions import Final + +WHITESPACE_REPLACEMENT: Final[str] = "-" +"""The character to replace undesirable characters with.""" + +REMOVABLE: Final[str] = punctuation.replace(WHITESPACE_REPLACEMENT, "").replace("_", "") +"""The collection of characters that should be removed altogether.""" + +NONLINGUAL: Final[str] = ( + r"\U000024C2-\U0001F251" + r"\U00002702-\U000027B0" + r"\U0001F1E0-\U0001F1FF" + r"\U0001F300-\U0001F5FF" + r"\U0001F600-\U0001F64F" + r"\U0001F680-\U0001F6FF" + r"\U0001f926-\U0001f937" + r"\u200D" + r"\u2640-\u2642" +) +"""A string that can be used in a regular expression to remove most non-lingual characters.""" + +STRIP_RE: Final[Pattern] = compile(f"[{REMOVABLE}{NONLINGUAL}]+") +"""A regular expression for finding all the characters that should be removed.""" + +WHITESPACE_RE: Final[Pattern] = compile(r"\s") +"""A regular expression for finding all the whitespace and turning it into `REPLACEMENT`.""" + + +def slug(text: str) -> str: + """Create a Markdown-friendly slug from the given text. + + Args: + text: The text to generate a slug from. + + Returns: + A slug for the given text. + + The rules used in generating the slug are based on observations of how + GitHub-flavoured Markdown works. + """ + result = text.strip().lower() + for rule, replacement in ( + (STRIP_RE, ""), + (WHITESPACE_RE, WHITESPACE_REPLACEMENT), + ): + result = rule.sub(replacement, result) + return quote(result) + + +class TrackedSlugs: + """Provides a class for generating tracked slugs. + + While [`slug`][textual._slug.slug] will generate a slug for a given + string, it does not guarantee that it is unique for a given context. If + you want to ensure that the same string generates unique slugs (perhaps + heading slugs within a Markdown document, as an example), use an + instance of this class to generate them. + + Example: + ```python + >>> slug("hello world") + 'hello-world' + >>> slug("hello world") + 'hello-world' + >>> unique = TrackedSlugs() + >>> unique.slug("hello world") + 'hello-world' + >>> unique.slug("hello world") + 'hello-world-1' + ``` + """ + + def __init__(self) -> None: + """Initialise the tracked slug object.""" + self._used: defaultdict[str, int] = defaultdict(int) + """Keeps track of how many times a particular slug has been used.""" + + def slug(self, text: str) -> str: + """Create a Markdown-friendly unique slug from the given text. + + Args: + text: The text to generate a slug from. + + Returns: + A slug for the given text. + """ + slugged = slug(text) + used = self._used[slugged] + self._used[slugged] += 1 + if used: + slugged = f"{slugged}-{used}" + return slugged diff --git a/src/textual/app.py b/src/textual/app.py index f505bdfb62..0a5500c2a7 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -371,6 +371,7 @@ def __init__( self._filters.append(DimFilter()) self.console = Console( + color_system=constants.COLOR_SYSTEM, file=_NullFile(), markup=True, highlight=False, @@ -474,11 +475,14 @@ def __init__( # Dev dependencies not installed pass else: - self.devtools = DevtoolsClient() + self.devtools = DevtoolsClient(constants.DEVTOOLS_HOST) self._devtools_redirector = StdoutRedirector(self.devtools) self._loop: asyncio.AbstractEventLoop | None = None self._return_value: ReturnType | None = None + """Internal attribute used to set the return value for the app.""" + self._return_code: int | None = None + """Internal attribute used to set the return code for the app.""" self._exit = False self._disable_tooltips = False self._disable_notifications = False @@ -528,6 +532,23 @@ def return_value(self) -> ReturnType | None: """ return self._return_value + @property + def return_code(self) -> int | None: + """The return code with which the app exited. + + Non-zero codes indicate errors. + A value of 1 means the app exited with a fatal error. + If the app wasn't exited yet, this will be `None`. + + Example: + The return code can be used to exit the process via `sys.exit`. + ```py + my_app.run() + sys.exit(my_app.return_code) + ``` + """ + return self._return_code + @property def children(self) -> Sequence["Widget"]: """A view onto the app's immediate children. @@ -647,17 +668,27 @@ def _screen_stack(self) -> list[Screen]: """ return self._screen_stacks[self._current_mode] + @property + def current_mode(self) -> str: + """The name of the currently active mode.""" + return self._current_mode + def exit( - self, result: ReturnType | None = None, message: RenderableType | None = None + self, + result: ReturnType | None = None, + return_code: int = 0, + message: RenderableType | None = None, ) -> None: """Exit the app, and return the supplied result. Args: result: Return value. + return_code: The return code. Use non-zero values for error codes. message: Optional message to display on exit. """ self._exit = True self._return_value = result + self._return_code = return_code self.post_message(messages.ExitApp()) if message: self._exit_renderables.append(message) @@ -1170,7 +1201,7 @@ def on_app_ready() -> None: """Called when app is ready to process events.""" app_ready_event.set() - async def run_app(app) -> None: + async def run_app(app: App) -> None: if message_hook is not None: message_hook_context_var.set(message_hook) app._loop = asyncio.get_running_loop() @@ -1509,19 +1540,19 @@ def mount_all( return self.mount(*widgets, before=before, after=after) def _init_mode(self, mode: str) -> None: - """Do internal initialisation of a new screen stack mode.""" + """Do internal initialisation of a new screen stack mode. + + Args: + mode: Name of the mode. + """ stack = self._screen_stacks.get(mode, []) if not stack: _screen = self.MODES[mode] - if callable(_screen): - screen, _ = self._get_screen(_screen()) - else: - screen, _ = self._get_screen(self.MODES[mode]) + new_screen: Screen | str = _screen() if callable(_screen) else _screen + screen, _ = self._get_screen(new_screen) stack.append(screen) - self._load_screen_css(screen) - self._screen_stacks[mode] = stack def switch_mode(self, mode: str) -> None: @@ -1731,6 +1762,7 @@ def push_screen( ) self._load_screen_css(next_screen) self._screen_stack.append(next_screen) + self.stylesheet.update(next_screen) next_screen.post_message(events.ScreenResume()) self.log.system(f"{self.screen} is current (PUSHED)") return await_mount @@ -1833,7 +1865,6 @@ def pop_screen(self) -> Screen[object]: ) previous_screen = self._replace_screen(screen_stack.pop()) previous_screen._pop_result_callback() - self.screen._screen_resized(self.size) self.screen.post_message(events.ScreenResume()) self.log.system(f"{self.screen} is active") return previous_screen @@ -1914,6 +1945,7 @@ def _handle_exception(self, error: Exception) -> None: Args: error: An exception instance. """ + self._return_code = 1 # If we're running via pilot and this is the first exception encountered, # take note of it so that we can re-raise for test frameworks later. if self.is_headless and self._exception is None: @@ -1982,6 +2014,9 @@ async def _process_messages( self.log.system(driver=self.driver_class) self.log.system(loop=asyncio.get_running_loop()) self.log.system(features=self.features) + if constants.LOG_FILE is not None: + _log_path = os.path.abspath(constants.LOG_FILE) + self.log.system(f"Writing logs to {_log_path!r}") try: if self.css_path: @@ -2022,6 +2057,7 @@ async def invoke_ready_callback() -> None: try: try: await self._dispatch_message(events.Compose()) + default_screen = self.screen await self._dispatch_message(events.Mount()) self.check_idle() finally: @@ -2030,7 +2066,8 @@ async def invoke_ready_callback() -> None: Reactive._initialize_object(self) self.stylesheet.update(self) - self.refresh() + if self.screen is not default_screen: + self.stylesheet.update(default_screen) await self.animator.start() @@ -2281,12 +2318,12 @@ async def _shutdown(self) -> None: await self._dispatch_message(events.Unmount()) - if self.devtools is not None and self.devtools.is_connected: - await self._disconnect_devtools() - if self._driver is not None: self._driver.close() + if self.devtools is not None and self.devtools.is_connected: + await self._disconnect_devtools() + self._print_error_renderables() if constants.SHOW_RETURN: diff --git a/src/textual/constants.py b/src/textual/constants.py index acc3862d76..d47d0d2c15 100644 --- a/src/textual/constants.py +++ b/src/textual/constants.py @@ -56,6 +56,9 @@ def get_environ_int(name: str, default: int) -> int: LOG_FILE: Final[str | None] = get_environ("TEXTUAL_LOG", None) """A last resort log file that appends all logs, when devtools isn't working.""" +DEVTOOLS_HOST: Final[str] = get_environ("TEXTUAL_DEVTOOLS_HOST", "127.0.0.1") +"""The host where textual console is running.""" + DEVTOOLS_PORT: Final[int] = get_environ_int("TEXTUAL_DEVTOOLS_PORT", 8081) """Constant with the port that the devtools will connect to.""" @@ -70,3 +73,6 @@ def get_environ_int(name: str, default: int) -> int: MAX_FPS: Final[int] = get_environ_int("TEXTUAL_FPS", 60) """Maximum frames per second for updates.""" + +COLOR_SYSTEM: Final[str | None] = get_environ("TEXTUAL_COLOR_SYSTEM", "auto") +"""Force color system override""" diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index a38cc859cc..658932165e 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -614,10 +614,10 @@ def __set__(self, obj: StylesBase, layout: str | Layout | None): _rich_traceback_omit = True if layout is None: if obj.clear_rule("layout"): - obj.refresh(layout=True) + obj.refresh(layout=True, children=True) elif isinstance(layout, Layout): if obj.set_rule("layout", layout): - obj.refresh(layout=True) + obj.refresh(layout=True, children=True) else: try: layout_object = get_layout(layout) @@ -627,7 +627,7 @@ def __set__(self, obj: StylesBase, layout: str | Layout | None): help_text=layout_property_help_text(self.name, context="inline"), ) if obj.set_rule("layout", layout_object): - obj.refresh(layout=True) + obj.refresh(layout=True, children=True) class OffsetProperty: diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 114950f094..641eb36e3c 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -722,8 +722,8 @@ def process_layer(self, name: str, tokens: list[Token]) -> None: def process_layers(self, name: str, tokens: list[Token]) -> None: layers: list[str] = [] for token in tokens: - if token.name != "token": - self.error(name, token, "{token.name} not expected here") + if token.name not in {"token", "string"}: + self.error(name, token, f"{token.name} not expected here") layers.append(token.value) self.styles._rules["layers"] = tuple(layers) diff --git a/src/textual/dom.py b/src/textual/dom.py index 3cc5051728..a65b8beeea 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -612,13 +612,21 @@ def display(self, new_val: bool | str) -> None: @property def visible(self) -> bool: - """Is the visibility style set to a visible state? + """Is this widget visible in the DOM? - May be set to a boolean to make the node visible (`True`) or invisible (`False`), or to any valid value for the `visibility` rule. + If a widget hasn't had its visibility set explicitly, then it inherits it from its + DOM ancestors. - When a node is invisible, Textual will reserve space for it, but won't display anything there. + This may be set explicitly to override inherited values. + The valid values include the valid values for the `visibility` rule and the booleans + `True` or `False`, to set the widget to be visible or invisible, respectively. + + When a node is invisible, Textual will reserve space for it, but won't display anything. """ - return self.styles.visibility != "hidden" + own_value = self.styles.get_rule("visibility") + if own_value is not None: + return own_value != "hidden" + return self.parent.visible if self.parent else True @visible.setter def visible(self, new_value: bool | str) -> None: diff --git a/src/textual/drivers/_byte_stream.py b/src/textual/drivers/_byte_stream.py index 02dc144002..4c6bb602f0 100644 --- a/src/textual/drivers/_byte_stream.py +++ b/src/textual/drivers/_byte_stream.py @@ -151,7 +151,7 @@ def parse( read = self.read from_bytes = int.from_bytes while not self.is_eof: - packet_type = (yield read1()).decode("utf-8") + packet_type = (yield read1()).decode("utf-8", "ignore") size = from_bytes((yield read(4)), "big") payload = (yield read(size)) if size else b"" on_token(BytePacket(packet_type, payload)) diff --git a/src/textual/drivers/_input_reader.py b/src/textual/drivers/_input_reader.py new file mode 100644 index 0000000000..84c72d3633 --- /dev/null +++ b/src/textual/drivers/_input_reader.py @@ -0,0 +1,10 @@ +import platform + +__all__ = ["InputReader"] + +WINDOWS = platform.system() == "Windows" + +if WINDOWS: + from ._input_reader_windows import InputReader +else: + from ._input_reader_linux import InputReader diff --git a/src/textual/drivers/_input_reader_linux.py b/src/textual/drivers/_input_reader_linux.py new file mode 100644 index 0000000000..82c032e0b6 --- /dev/null +++ b/src/textual/drivers/_input_reader_linux.py @@ -0,0 +1,49 @@ +import os +import selectors +import sys +from threading import Event +from typing import Iterator + +from textual import log + + +class InputReader: + """Read input from stdin.""" + + def __init__(self, timeout: float = 0.1) -> None: + """ + + Args: + timeout: Seconds to block for input. + """ + self._fileno = sys.__stdin__.fileno() + self.timeout = timeout + self._selector = selectors.DefaultSelector() + self._selector.register(self._fileno, selectors.EVENT_READ) + self._exit_event = Event() + + def more_data(self) -> bool: + """Check if there is data pending.""" + EVENT_READ = selectors.EVENT_READ + for _key, events in self._selector.select(0.01): + if events & EVENT_READ: + return True + return False + + def close(self) -> None: + """Close the reader (will exit the iterator).""" + self._exit_event.set() + + def __iter__(self) -> Iterator[bytes]: + """Read input, yield bytes.""" + fileno = self._fileno + read = os.read + exit_set = self._exit_event.is_set + EVENT_READ = selectors.EVENT_READ + while not exit_set(): + for _key, events in self._selector.select(self.timeout): + if events & EVENT_READ: + data = read(fileno, 1024) + if not data: + return + yield data diff --git a/src/textual/drivers/_input_reader_windows.py b/src/textual/drivers/_input_reader_windows.py new file mode 100644 index 0000000000..7f9aeb1ebb --- /dev/null +++ b/src/textual/drivers/_input_reader_windows.py @@ -0,0 +1,37 @@ +import os +import sys +from threading import Event +from typing import Iterator + + +class InputReader: + """Read input from stdin.""" + + def __init__(self, timeout: float = 0.1) -> None: + """ + + Args: + timeout: Seconds to block for input. + """ + self._fileno = sys.__stdin__.fileno() + self.timeout = timeout + self._exit_event = Event() + + def more_data(self) -> bool: + """Check if there is data pending.""" + return True + + def close(self) -> None: + """Close the reader (will exit the iterator).""" + self._exit_event.set() + + def __iter__(self) -> Iterator[bytes]: + """Read input, yield bytes.""" + while not self._exit_event.is_set(): + try: + data = os.read(self._fileno, 1024) or None + except Exception: + break + if not data: + break + yield data diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index d77a12f64b..c13512e8ab 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -242,8 +242,9 @@ def run_input_thread(self) -> None: def more_data() -> bool: """Check if there is more data to parse.""" + EVENT_READ = selectors.EVENT_READ for key, events in selector.select(0.01): - if events: + if events & EVENT_READ: return True return False @@ -259,7 +260,7 @@ def more_data() -> bool: while not self.exit_event.is_set(): selector_events = selector.select(0.1) for _selector_key, mask in selector_events: - if mask | EVENT_READ: + if mask & EVENT_READ: unicode_data = decode(read(fileno, 1024)) for event in feed(unicode_data): self.process_event(event) diff --git a/src/textual/drivers/web_driver.py b/src/textual/drivers/web_driver.py index 8920182db8..7b0976df94 100644 --- a/src/textual/drivers/web_driver.py +++ b/src/textual/drivers/web_driver.py @@ -15,7 +15,6 @@ import json import os import platform -import selectors import signal import sys from codecs import getincrementaldecoder @@ -28,23 +27,36 @@ from ..driver import Driver from ..geometry import Size from ._byte_stream import ByteStream +from ._input_reader import InputReader WINDOWS = platform.system() == "Windows" +class _ExitInput(Exception): + """Internal exception to force exit of input loop.""" + + class WebDriver(Driver): """A headless driver that may be run remotely.""" def __init__( self, app: App, *, debug: bool = False, size: tuple[int, int] | None = None ): + if size is None: + try: + width = int(os.environ.get("COLUMNS", 80)) + height = int(os.environ.get("ROWS", 24)) + except ValueError: + pass + else: + size = width, height super().__init__(app, debug=debug, size=size) self.stdout = sys.__stdout__ self.fileno = sys.__stdout__.fileno() - self.in_fileno = sys.__stdin__.fileno() self._write = partial(os.write, self.fileno) self.exit_event = Event() self._key_thread: Thread = Thread(target=self.run_input_thread) + self._input_reader = InputReader() def write(self, data: str) -> None: """Write data to the output device. @@ -56,6 +68,15 @@ def write(self, data: str) -> None: data_bytes = data.encode("utf-8") self._write(b"D%s%s" % (len(data_bytes).to_bytes(4, "big"), data_bytes)) + def write_meta(self, data: dict[str, object]) -> None: + """Write meta to the controlling process (i.e. textual-web) + + Args: + data: Meta dict. + """ + meta_bytes = json.dumps(data).encode("utf-8", errors="ignore") + self._write(b"M%s%s" % (len(meta_bytes).to_bytes(4, "big"), meta_bytes)) + def flush(self) -> None: pass @@ -128,68 +149,61 @@ def disable_input(self) -> None: def stop_application_mode(self) -> None: """Stop application mode, restore state.""" self.exit_event.set() - self._key_thread.join() + self._input_reader.close() + self.write_meta({"type": "exit"}) def run_input_thread(self) -> None: """Wait for input and dispatch events.""" - selector = selectors.DefaultSelector() - fileno = self.in_fileno - selector.register(fileno, selectors.EVENT_READ) - - def more_data() -> bool: - """Check if there is more data to parse.""" - for key, events in selector.select(0.01): - if events: - return True - return False - - parser = XTermParser(more_data, debug=self._debug) - feed = parser.feed - + input_reader = self._input_reader + parser = XTermParser(input_reader.more_data, debug=self._debug) utf8_decoder = getincrementaldecoder("utf-8")().decode decode = utf8_decoder - read = os.read - EVENT_READ = selectors.EVENT_READ - # The server sends us a stream of bytes, which contains the equivalent of stdin, plus # in band data packets. byte_stream = ByteStream() try: - while not self.exit_event.is_set(): - selector_events = selector.select(0.1) - for _selector_key, mask in selector_events: - if mask | EVENT_READ: - data = read(fileno, 1024) # raw data - - for packet_type, payload in byte_stream.feed(data): - if packet_type == "D": - # Treat as stdin - for event in feed(decode(payload)): - self.process_event(event) - else: - # Process meta information separately - self._on_meta(packet_type, payload) - except Exception as error: - log(error) + for data in input_reader: + for packet_type, payload in byte_stream.feed(data): + if packet_type == "D": + # Treat as stdin + for event in parser.feed(decode(payload)): + self.process_event(event) + else: + # Process meta information separately + self._on_meta(packet_type, payload) + except _ExitInput: + pass + except Exception: + from traceback import format_exc + + log(format_exc()) finally: - selector.close() + input_reader.close() def _on_meta(self, packet_type: str, payload: bytes) -> None: + """Private method to dispatch meta. + + Args: + packet_type: Packet type (currently always "M") + payload: Meta payload (JSON encoded as bytes). + """ payload_map = json.loads(payload) _type = payload_map.get("type") if isinstance(payload_map, dict): self.on_meta(_type, payload_map) def on_meta(self, packet_type: str, payload: dict) -> None: + """Process meta information. + + Args: + packet_type: The type of the packet. + payload: meta dict. + """ if packet_type == "resize": self._size = (payload["width"], payload["height"]) size = Size(*self._size) - event = events.Resize(size, size) - asyncio.run_coroutine_threadsafe( - self._app._post_message(event), - loop=self._loop, - ) + self._app.post_message(events.Resize(size, size)) elif packet_type == "quit": - asyncio.run_coroutine_threadsafe( - self._app._post_message(messages.ExitApp()), loop=self._loop - ) + self._app.post_message(messages.ExitApp()) + elif packet_type == "exit": + raise _ExitInput() diff --git a/src/textual/drivers/win32.py b/src/textual/drivers/win32.py index 4942bad92f..214fb28dc5 100644 --- a/src/textual/drivers/win32.py +++ b/src/textual/drivers/win32.py @@ -124,7 +124,7 @@ class INPUT_RECORD(Structure): _fields_ = [("EventType", wintypes.WORD), ("Event", InputEvent)] -def _set_console_mode(file: IO, mode: int) -> bool: +def set_console_mode(file: IO, mode: int) -> bool: """Set the console mode for a given file (stdout or stdin). Args: @@ -139,7 +139,7 @@ def _set_console_mode(file: IO, mode: int) -> bool: return success -def _get_console_mode(file: IO) -> int: +def get_console_mode(file: IO) -> int: """Get the console mode for a given file (stdout or stdin) Args: @@ -164,22 +164,22 @@ def enable_application_mode() -> Callable[[], None]: terminal_in = sys.stdin terminal_out = sys.stdout - current_console_mode_in = _get_console_mode(terminal_in) - current_console_mode_out = _get_console_mode(terminal_out) + current_console_mode_in = get_console_mode(terminal_in) + current_console_mode_out = get_console_mode(terminal_out) def restore() -> None: """Restore console mode to previous settings""" - _set_console_mode(terminal_in, current_console_mode_in) - _set_console_mode(terminal_out, current_console_mode_out) + set_console_mode(terminal_in, current_console_mode_in) + set_console_mode(terminal_out, current_console_mode_out) - _set_console_mode( + set_console_mode( terminal_out, current_console_mode_out | ENABLE_VIRTUAL_TERMINAL_PROCESSING ) - _set_console_mode(terminal_in, ENABLE_VIRTUAL_TERMINAL_INPUT) + set_console_mode(terminal_in, ENABLE_VIRTUAL_TERMINAL_INPUT) return restore -def _wait_for_handles(handles: List[HANDLE], timeout: int = -1) -> Optional[HANDLE]: +def wait_for_handles(handles: List[HANDLE], timeout: int = -1) -> Optional[HANDLE]: """ Waits for multiple handles. (Similar to 'select') Returns the handle which is ready. Returns `None` on timeout. @@ -244,7 +244,7 @@ def run(self) -> None: while not exit_requested(): # Wait for new events - if _wait_for_handles([hIn], 200) is None: + if wait_for_handles([hIn], 200) is None: # No new events continue diff --git a/src/textual/events.py b/src/textual/events.py index f70647a20e..7cff7d01d0 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -651,3 +651,7 @@ def __init__(self, text: str, stderr: bool = False) -> None: super().__init__() self.text = text self.stderr = stderr + + def __rich_repr__(self) -> rich.repr.Result: + yield self.text + yield self.stderr diff --git a/src/textual/message.py b/src/textual/message.py index d80d512e8c..931c5aa21b 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -11,7 +11,6 @@ from . import _time from ._context import active_message_pump -from ._types import MessageTarget from .case import camel_to_snake if TYPE_CHECKING: diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index affdf08431..7ed468dca2 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -118,7 +118,7 @@ def __init__(self, parent: MessagePump | None = None) -> None: self._last_idle: float = time() self._max_idle: float | None = None self._mounted_event = asyncio.Event() - self._next_callbacks: list[CallbackType] = [] + self._next_callbacks: list[events.Callback] = [] self._thread_id: int = threading.get_ident() @property @@ -417,7 +417,9 @@ def call_next(self, callback: Callback, *args: Any, **kwargs: Any) -> None: *args: Positional arguments to pass to the callable. **kwargs: Keyword arguments to pass to the callable. """ - self._next_callbacks.append(partial(callback, *args, **kwargs)) + callback_message = events.Callback(callback=partial(callback, *args, **kwargs)) + callback_message._prevent.update(self._get_prevented_messages()) + self._next_callbacks.append(callback_message) self.check_idle() def _on_invoke_later(self, message: messages.InvokeLater) -> None: @@ -562,7 +564,7 @@ async def _flush_next_callbacks(self) -> None: self._next_callbacks.clear() for callback in callbacks: try: - await invoke(callback) + await self._dispatch_message(callback) except Exception as error: self.app._handle_exception(error) break diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 328a458329..d361bd0049 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -220,11 +220,15 @@ async def await_watcher(awaitable: Awaitable) -> None: obj.post_message(events.Callback(callback=partial(Reactive._compute, obj))) def invoke_watcher( - watch_function: Callable, old_value: object, value: object + watcher_object: Reactable, + watch_function: Callable, + old_value: object, + value: object, ) -> None: """Invoke a watch function. Args: + watcher_object: The object watching for the changes. watch_function: A watch function, which may be sync or async. old_value: The old value of the attribute. value: The new value of the attribute. @@ -239,17 +243,15 @@ def invoke_watcher( watch_result = watch_function() if isawaitable(watch_result): # Result is awaitable, so we need to await it within an async context - obj.post_message( - events.Callback(callback=partial(await_watcher, watch_result)) - ) + watcher_object.call_next(partial(await_watcher, watch_result)) private_watch_function = getattr(obj, f"_watch_{name}", None) if callable(private_watch_function): - invoke_watcher(private_watch_function, old_value, value) + invoke_watcher(obj, private_watch_function, old_value, value) public_watch_function = getattr(obj, f"watch_{name}", None) if callable(public_watch_function): - invoke_watcher(public_watch_function, old_value, value) + invoke_watcher(obj, public_watch_function, old_value, value) # Process "global" watchers watchers: list[tuple[Reactable, Callable]] @@ -263,7 +265,7 @@ def invoke_watcher( ] for reactable, callback in watchers: with reactable.prevent(*obj._prevent_message_types_stack[-1]): - invoke_watcher(callback, old_value, value) + invoke_watcher(reactable, callback, old_value, value) @classmethod def _compute(cls, obj: Reactable) -> None: diff --git a/src/textual/screen.py b/src/textual/screen.py index 3abfc88d13..e482046232 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -6,6 +6,7 @@ from __future__ import annotations from functools import partial +from operator import attrgetter from typing import ( TYPE_CHECKING, Awaitable, @@ -293,18 +294,42 @@ def focus_chain(self) -> list[Widget]: widgets: list[Widget] = [] add_widget = widgets.append - stack: list[Iterator[Widget]] = [iter(self.focusable_children)] - pop = stack.pop - push = stack.append + focus_sorter = attrgetter("_focus_sort_key") + # We traverse the DOM and keep track of where we are at with a node stack. + # Additionally, we manually keep track of the visibility of the DOM + # instead of relying on the property `.visible` to save on DOM traversals. + # node_stack: list[tuple[iterator over node children, node visibility]] + node_stack: list[tuple[Iterator[Widget], bool]] = [ + ( + iter(sorted(self.displayed_children, key=focus_sorter)), + self.visible, + ) + ] + pop = node_stack.pop + push = node_stack.append - while stack: - node = next(stack[-1], None) + while node_stack: + children_iterator, parent_visibility = node_stack[-1] + node = next(children_iterator, None) if node is None: pop() else: + if node.disabled: + continue + node_styles_visibility = node.styles.get_rule("visibility") + node_is_visible = ( + node_styles_visibility != "hidden" + if node_styles_visibility + else parent_visibility # Inherit visibility if the style is unset. + ) if node.is_container and node.can_focus_children: - push(iter(node.focusable_children)) - if node.focusable: + sorted_displayed_children = sorted( + node.displayed_children, key=focus_sorter + ) + push((iter(sorted_displayed_children), node_is_visible)) + # Same check as `if node.focusable`, but we cached inherited visibility + # and we also skipped disabled nodes altogether. + if node_is_visible and node.can_focus: add_widget(node) return widgets diff --git a/src/textual/widget.py b/src/textual/widget.py index bf06ca091d..b1b89e893b 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1507,17 +1507,7 @@ def _self_or_ancestors_disabled(self) -> bool: @property def focusable(self) -> bool: """Can this widget currently be focused?""" - return self.can_focus and not self._self_or_ancestors_disabled - - @property - def focusable_children(self) -> list[Widget]: - """Get the children which may be focused. - - Returns: - List of widgets that can receive focus. - """ - focusable = [child for child in self._nodes if child.display and child.visible] - return sorted(focusable, key=attrgetter("_focus_sort_key")) + return self.can_focus and self.visible and not self._self_or_ancestors_disabled @property def _focus_sort_key(self) -> tuple[int, int]: diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index f70ec9bd2e..31574e7788 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -32,6 +32,7 @@ from ._radio_button import RadioButton from ._radio_set import RadioSet from ._rich_log import RichLog + from ._rule import Rule from ._select import Select from ._selection_list import SelectionList from ._sparkline import Sparkline @@ -68,6 +69,7 @@ "ProgressBar", "RadioButton", "RadioSet", + "Rule", "Select", "SelectionList", "Sparkline", diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi index f02b2b95f1..1c756d9524 100644 --- a/src/textual/widgets/__init__.pyi +++ b/src/textual/widgets/__init__.pyi @@ -22,6 +22,7 @@ from ._progress_bar import ProgressBar as ProgressBar from ._radio_button import RadioButton as RadioButton from ._radio_set import RadioSet as RadioSet from ._rich_log import RichLog as RichLog +from ._rule import Rule as Rule from ._select import Select as Select from ._selection_list import SelectionList as SelectionList from ._sparkline import Sparkline as Sparkline diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index 40be74adbf..fee0db2030 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -158,7 +158,7 @@ class Button(Static, can_focus=True): variant = reactive("default") """The variant name for the button.""" - class Pressed(Message, bubble=True): + class Pressed(Message): """Event sent when a `Button` is pressed. Can be handled using `on_button_pressed` in a subclass of diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 6201983dd6..4f1f574cf7 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -322,7 +322,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): ) """The coordinate of the `DataTable` that is being hovered.""" - class CellHighlighted(Message, bubble=True): + class CellHighlighted(Message): """Posted when the cursor moves to highlight a new cell. This is only relevant when the `cursor_type` is `"cell"`. @@ -359,7 +359,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class CellSelected(Message, bubble=True): + class CellSelected(Message): """Posted by the `DataTable` widget when a cell is selected. This is only relevant when the `cursor_type` is `"cell"`. Can be handled using @@ -394,7 +394,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class RowHighlighted(Message, bubble=True): + class RowHighlighted(Message): """Posted when a row is highlighted. This message is only posted when the @@ -423,7 +423,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class RowSelected(Message, bubble=True): + class RowSelected(Message): """Posted when a row is selected. This message is only posted when the @@ -452,7 +452,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class ColumnHighlighted(Message, bubble=True): + class ColumnHighlighted(Message): """Posted when a column is highlighted. This message is only posted when the @@ -481,7 +481,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class ColumnSelected(Message, bubble=True): + class ColumnSelected(Message): """Posted when a column is selected. This message is only posted when the @@ -510,7 +510,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class HeaderSelected(Message, bubble=True): + class HeaderSelected(Message): """Posted when a column header/label is clicked.""" def __init__( @@ -540,7 +540,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class RowLabelSelected(Message, bubble=True): + class RowLabelSelected(Message): """Posted when a row label is clicked.""" def __init__( @@ -582,6 +582,7 @@ def __init__( show_cursor: bool = True, cursor_foreground_priority: Literal["renderable", "css"] = "css", cursor_background_priority: Literal["renderable", "css"] = "renderable", + cursor_type: CursorType = "cell", name: str | None = None, id: str | None = None, classes: str | None = None, @@ -669,6 +670,8 @@ def __init__( self.cursor_background_priority = cursor_background_priority """Should we prioritize the cursor component class CSS background or the renderable background in the event where a cell contains a renderable with a background color.""" + self.cursor_type = cursor_type + """The type of cursor of the `DataTable`.""" @property def hover_row(self) -> int: diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 1855aa2d90..d35510a8f8 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -65,7 +65,7 @@ class DirectoryTree(Tree[DirEntry]): PATH: Callable[[str | Path], Path] = Path """Callable that returns a fresh path object.""" - class FileSelected(Message, bubble=True): + class FileSelected(Message): """Posted when a file is selected. Can be handled using `on_directory_tree_file_selected` in a subclass of diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index daf298e7aa..24596d5572 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -421,10 +421,11 @@ async def _on_click(self, event: events.Click) -> None: cell_offset = 0 _cell_size = get_character_cell_size for index, char in enumerate(self.value): - if cell_offset >= click_x: + cell_width = _cell_size(char) + if cell_offset <= click_x < (cell_offset + cell_width): self.cursor_position = index break - cell_offset += _cell_size(char) + cell_offset += cell_width else: self.cursor_position = len(self.value) diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index a3bf67b896..615d37ae5a 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -38,7 +38,7 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False): index = reactive[Optional[int]](0, always_update=True) - class Highlighted(Message, bubble=True): + class Highlighted(Message): """Posted when the highlighted item changes. Highlighted item is controlled using up/down keys. @@ -65,7 +65,7 @@ def control(self) -> ListView: """ return self.list_view - class Selected(Message, bubble=True): + class Selected(Message): """Posted when a list item is selected, e.g. when you press the enter key on it. Can be handled using `on_list_view_selected` in a subclass of `ListView` or in diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 1569195626..dafd64ee19 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -565,7 +565,7 @@ def __init__( self._markdown = markdown self._parser_factory = parser_factory - class TableOfContentsUpdated(Message, bubble=True): + class TableOfContentsUpdated(Message): """The table of contents was updated.""" def __init__( @@ -586,7 +586,7 @@ def control(self) -> Markdown: """ return self.markdown - class TableOfContentsSelected(Message, bubble=True): + class TableOfContentsSelected(Message): """An item in the TOC was selected.""" def __init__(self, markdown: Markdown, block_id: str) -> None: @@ -605,7 +605,7 @@ def control(self) -> Markdown: """ return self.markdown - class LinkClicked(Message, bubble=True): + class LinkClicked(Message): """A link in the document was clicked.""" def __init__(self, markdown: Markdown, href: str) -> None: diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index 3a0ee116bf..27581af68d 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -76,7 +76,7 @@ class RadioSet(Container, can_focus=True, can_focus_children=False): """The index of the currently-selected radio button.""" @rich.repr.auto - class Changed(Message, bubble=True): + class Changed(Message): """Posted when the pressed button in the set changes. This message can be handled using an `on_radio_set_changed` method. @@ -124,7 +124,7 @@ def __init__( """Initialise the radio set. Args: - buttons: A collection of labels or [`RadioButton`][textual.widgets.RadioButton]s to group together. + buttons: The labels or [`RadioButton`][textual.widgets.RadioButton]s to group together. name: The name of the radio set. id: The ID of the radio set in the DOM. classes: The CSS classes of the radio set. diff --git a/src/textual/widgets/_rule.py b/src/textual/widgets/_rule.py new file mode 100644 index 0000000000..f172c0bda5 --- /dev/null +++ b/src/textual/widgets/_rule.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +from rich.text import Text +from typing_extensions import Literal + +from ..app import RenderResult +from ..css._error_tools import friendly_list +from ..reactive import Reactive, reactive +from ..widget import Widget + +RuleOrientation = Literal["horizontal", "vertical"] +"""The valid orientations of the rule widget.""" + +LineStyle = Literal[ + "ascii", + "blank", + "dashed", + "double", + "heavy", + "hidden", + "none", + "solid", + "thick", +] +"""The valid line styles of the rule widget.""" + + +_VALID_RULE_ORIENTATIONS = {"horizontal", "vertical"} + +_VALID_LINE_STYLES = { + "ascii", + "blank", + "dashed", + "double", + "heavy", + "hidden", + "none", + "solid", + "thick", +} + +_HORIZONTAL_LINE_CHARS: dict[LineStyle, str] = { + "ascii": "-", + "blank": " ", + "dashed": "╍", + "double": "═", + "heavy": "━", + "hidden": " ", + "none": " ", + "solid": "─", + "thick": "█", +} + +_VERTICAL_LINE_CHARS: dict[LineStyle, str] = { + "ascii": "|", + "blank": " ", + "dashed": "╏", + "double": "║", + "heavy": "┃", + "hidden": " ", + "none": " ", + "solid": "│", + "thick": "█", +} + + +class InvalidRuleOrientation(Exception): + """Exception raised for an invalid rule orientation.""" + + +class InvalidLineStyle(Exception): + """Exception raised for an invalid rule line style.""" + + +class Rule(Widget, can_focus=False): + """A rule widget to separate content, similar to a `
` HTML tag.""" + + DEFAULT_CSS = """ + Rule { + color: $primary; + } + + Rule.-horizontal { + min-height: 1; + max-height: 1; + margin: 1 0; + } + + Rule.-vertical { + min-width: 1; + max-width: 1; + margin: 0 2; + } + """ + + orientation: Reactive[RuleOrientation] = reactive[RuleOrientation]("horizontal") + """The orientation of the rule.""" + + line_style: Reactive[LineStyle] = reactive[LineStyle]("solid") + """The line style of the rule.""" + + def __init__( + self, + orientation: RuleOrientation = "horizontal", + line_style: LineStyle = "solid", + *, + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + ) -> None: + """Initialize a rule widget. + + Args: + orientation: The orientation of the rule. + line_style: The line style of the rule. + name: The name of the widget. + id: The ID of the widget in the DOM. + classes: The CSS classes of the widget. + disabled: Whether the widget is disabled or not. + """ + super().__init__(name=name, id=id, classes=classes, disabled=disabled) + self.orientation = orientation + self.line_style = line_style + + def render(self) -> RenderResult: + rule_char: str + if self.orientation == "vertical": + rule_char = _VERTICAL_LINE_CHARS[self.line_style] + return Text(rule_char * self.size.height) + elif self.orientation == "horizontal": + rule_char = _HORIZONTAL_LINE_CHARS[self.line_style] + return Text(rule_char * self.size.width) + else: + raise InvalidRuleOrientation( + f"Valid rule orientations are {friendly_list(_VALID_RULE_ORIENTATIONS)}" + ) + + def watch_orientation( + self, old_orientation: RuleOrientation, orientation: RuleOrientation + ) -> None: + self.remove_class(f"-{old_orientation}") + self.add_class(f"-{orientation}") + + def validate_orientation(self, orientation: RuleOrientation) -> RuleOrientation: + if orientation not in _VALID_RULE_ORIENTATIONS: + raise InvalidRuleOrientation( + f"Valid rule orientations are {friendly_list(_VALID_RULE_ORIENTATIONS)}" + ) + return orientation + + def validate_line_style(self, style: LineStyle) -> LineStyle: + if style not in _VALID_LINE_STYLES: + raise InvalidLineStyle( + f"Valid rule line styles are {friendly_list(_VALID_LINE_STYLES)}" + ) + return style + + @classmethod + def horizontal( + cls, + line_style: LineStyle = "solid", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + ) -> Rule: + """Utility constructor for creating a horizontal rule. + + Args: + line_style: The line style of the rule. + name: The name of the widget. + id: The ID of the widget in the DOM. + classes: The CSS classes of the widget. + disabled: Whether the widget is disabled or not. + + Returns: + A rule widget with horizontal orientation. + """ + return Rule( + orientation="horizontal", + line_style=line_style, + name=name, + id=id, + classes=classes, + disabled=disabled, + ) + + @classmethod + def vertical( + cls, + line_style: LineStyle = "solid", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + ) -> Rule: + """Utility constructor for creating a vertical rule. + + Args: + line_style: The line style of the rule. + name: The name of the widget. + id: The ID of the widget in the DOM. + classes: The CSS classes of the widget. + disabled: Whether the widget is disabled or not. + + Returns: + A rule widget with vertical orientation. + """ + return Rule( + orientation="vertical", + line_style=line_style, + name=name, + id=id, + classes=classes, + disabled=disabled, + ) diff --git a/src/textual/widgets/_select.py b/src/textual/widgets/_select.py index 90e6f18411..508da0487d 100644 --- a/src/textual/widgets/_select.py +++ b/src/textual/widgets/_select.py @@ -226,7 +226,7 @@ class Select(Generic[SelectType], Vertical, can_focus=True): value: var[SelectType | None] = var[Optional[SelectType]](None) """The value of the select.""" - class Changed(Message, bubble=True): + class Changed(Message): """Posted when the select value was changed. This message can be handled using a `on_select_changed` method. diff --git a/src/textual/widgets/_switch.py b/src/textual/widgets/_switch.py index eb0568c618..a6114ff3a8 100644 --- a/src/textual/widgets/_switch.py +++ b/src/textual/widgets/_switch.py @@ -80,7 +80,7 @@ class Switch(Widget, can_focus=True): slider_pos = reactive(0.0) """The position of the slider.""" - class Changed(Message, bubble=True): + class Changed(Message): """Posted when the status of the switch changes. Can be handled using `on_switch_changed` in a subclass of `Switch` diff --git a/src/textual/widgets/_toggle_button.py b/src/textual/widgets/_toggle_button.py index cb9b959012..4c29c236ae 100644 --- a/src/textual/widgets/_toggle_button.py +++ b/src/textual/widgets/_toggle_button.py @@ -232,7 +232,7 @@ async def _on_click(self, _: Click) -> None: """Toggle the value of the widget when clicked with the mouse.""" self.toggle() - class Changed(Message, bubble=True): + class Changed(Message): """Posted when the value of the toggle button changes.""" def __init__(self, toggle_button: ToggleButton, value: bool) -> None: diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 0d7b495eda..b094f75681 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -508,7 +508,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): ), } - class NodeCollapsed(Generic[EventTreeDataType], Message, bubble=True): + class NodeCollapsed(Generic[EventTreeDataType], Message): """Event sent when a node is collapsed. Can be handled using `on_tree_node_collapsed` in a subclass of `Tree` or in a @@ -525,7 +525,7 @@ def control(self) -> Tree[EventTreeDataType]: """The tree that sent the message.""" return self.node.tree - class NodeExpanded(Generic[EventTreeDataType], Message, bubble=True): + class NodeExpanded(Generic[EventTreeDataType], Message): """Event sent when a node is expanded. Can be handled using `on_tree_node_expanded` in a subclass of `Tree` or in a @@ -542,7 +542,7 @@ def control(self) -> Tree[EventTreeDataType]: """The tree that sent the message.""" return self.node.tree - class NodeHighlighted(Generic[EventTreeDataType], Message, bubble=True): + class NodeHighlighted(Generic[EventTreeDataType], Message): """Event sent when a node is highlighted. Can be handled using `on_tree_node_highlighted` in a subclass of `Tree` or in a @@ -559,7 +559,7 @@ def control(self) -> Tree[EventTreeDataType]: """The tree that sent the message.""" return self.node.tree - class NodeSelected(Generic[EventTreeDataType], Message, bubble=True): + class NodeSelected(Generic[EventTreeDataType], Message): """Event sent when a node is selected. Can be handled using `on_tree_node_selected` in a subclass of `Tree` or in a @@ -888,25 +888,29 @@ def watch_show_root(self, show_root: bool) -> None: self.cursor_line = -1 self._invalidate() - def scroll_to_line(self, line: int) -> None: + def scroll_to_line(self, line: int, animate: bool = True) -> None: """Scroll to the given line. Args: line: A line number. + animate: Enable animation. """ region = self._get_label_region(line) if region is not None: - self.scroll_to_region(region) + self.scroll_to_region(region, animate=animate) - def scroll_to_node(self, node: TreeNode[TreeDataType]) -> None: + def scroll_to_node( + self, node: TreeNode[TreeDataType], animate: bool = True + ) -> None: """Scroll to the given node. Args: node: Node to scroll in to view. + animate: Animate scrolling. """ line = node._line if line != -1: - self.scroll_to_line(line) + self.scroll_to_line(line, animate=animate) def refresh_line(self, line: int) -> None: """Refresh (repaint) a given line in the tree. @@ -1156,7 +1160,7 @@ def action_cursor_up(self) -> None: self.cursor_line = self.last_line else: self.cursor_line -= 1 - self.scroll_to_line(self.cursor_line) + self.scroll_to_line(self.cursor_line, animate=False) def action_cursor_down(self) -> None: """Move the cursor down one node.""" @@ -1164,7 +1168,7 @@ def action_cursor_down(self) -> None: self.cursor_line = 0 else: self.cursor_line += 1 - self.scroll_to_line(self.cursor_line) + self.scroll_to_line(self.cursor_line, animate=False) def action_page_down(self) -> None: """Move the cursor down a page's-worth of nodes.""" diff --git a/src/textual/widgets/rule.py b/src/textual/widgets/rule.py new file mode 100644 index 0000000000..a9ab5d23e9 --- /dev/null +++ b/src/textual/widgets/rule.py @@ -0,0 +1,8 @@ +from ._rule import InvalidLineStyle, InvalidRuleOrientation, LineStyle, RuleOrientation + +__all__ = [ + "InvalidLineStyle", + "InvalidRuleOrientation", + "LineStyle", + "RuleOrientation", +] diff --git a/tests/input/test_input_mouse.py b/tests/input/test_input_mouse.py index e4bfbb51d6..491f18fda7 100644 --- a/tests/input/test_input_mouse.py +++ b/tests/input/test_input_mouse.py @@ -6,28 +6,61 @@ from textual.geometry import Offset from textual.widgets import Input +# A string containing only single-width characters +TEXT_SINGLE = "That gum you like is going to come back in style" + +# A string containing only double-width characters +TEXT_DOUBLE = "こんにちは" + +# A string containing both single and double-width characters +TEXT_MIXED = "aこんbcにdちeは" + class InputApp(App[None]): - TEST_TEXT = "That gum you like is going to come back in style" + def __init__(self, text): + super().__init__() + self._text = text def compose(self) -> ComposeResult: - yield Input(self.TEST_TEXT) + yield Input(self._text) @pytest.mark.parametrize( - "click_at, should_land", + "text, click_at, should_land", ( - (0, 0), - (1, 1), - (10, 10), - (len(InputApp.TEST_TEXT) - 1, len(InputApp.TEST_TEXT) - 1), - (len(InputApp.TEST_TEXT), len(InputApp.TEST_TEXT)), - (len(InputApp.TEST_TEXT) * 2, len(InputApp.TEST_TEXT)), + # Single-width characters + (TEXT_SINGLE, 0, 0), + (TEXT_SINGLE, 1, 1), + (TEXT_SINGLE, 10, 10), + (TEXT_SINGLE, len(TEXT_SINGLE) - 1, len(TEXT_SINGLE) - 1), + (TEXT_SINGLE, len(TEXT_SINGLE), len(TEXT_SINGLE)), + (TEXT_SINGLE, len(TEXT_SINGLE) * 2, len(TEXT_SINGLE)), + # Double-width characters + (TEXT_DOUBLE, 0, 0), + (TEXT_DOUBLE, 1, 0), + (TEXT_DOUBLE, 2, 1), + (TEXT_DOUBLE, 3, 1), + (TEXT_DOUBLE, 4, 2), + (TEXT_DOUBLE, 5, 2), + (TEXT_DOUBLE, (len(TEXT_DOUBLE) * 2) - 1, len(TEXT_DOUBLE) - 1), + (TEXT_DOUBLE, len(TEXT_DOUBLE) * 2, len(TEXT_DOUBLE)), + (TEXT_DOUBLE, len(TEXT_DOUBLE) * 10, len(TEXT_DOUBLE)), + # Mixed-width characters + (TEXT_MIXED, 0, 0), + (TEXT_MIXED, 1, 1), + (TEXT_MIXED, 2, 1), + (TEXT_MIXED, 3, 2), + (TEXT_MIXED, 4, 2), + (TEXT_MIXED, 5, 3), + (TEXT_MIXED, 13, 9), + (TEXT_MIXED, 14, 9), + (TEXT_MIXED, 15, 10), + (TEXT_MIXED, 1000, 10), ), ) -async def test_mouse_clicks_within(click_at, should_land): +async def test_mouse_clicks_within(text, click_at, should_land): """Mouse clicks should result in the cursor going to the right place.""" - async with InputApp().run_test() as pilot: + async with InputApp(text).run_test() as pilot: # Note the offsets to take into account the decoration around an # Input. await pilot.click(Input, Offset(click_at + 3, 1)) @@ -37,7 +70,7 @@ async def test_mouse_clicks_within(click_at, should_land): async def test_mouse_click_outwith(): """Mouse clicks outside the input should not affect cursor position.""" - async with InputApp().run_test() as pilot: + async with InputApp(TEXT_SINGLE).run_test() as pilot: pilot.app.query_one(Input).cursor_position = 3 assert pilot.app.query_one(Input).cursor_position == 3 await pilot.click(Input, Offset(0, 0)) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 099ea8096f..bd82c73c8c 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -347,137 +347,136 @@ font-weight: 700; } - .terminal-549632924-matrix { + .terminal-2688126662-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-549632924-title { + .terminal-2688126662-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-549632924-r1 { fill: #008000 } - .terminal-549632924-r2 { fill: #c5c8c6 } - .terminal-549632924-r3 { fill: #e1e1e1 } - .terminal-549632924-r4 { fill: #1e1e1e } - .terminal-549632924-r5 { fill: #0178d4 } - .terminal-549632924-r6 { fill: #121212 } - .terminal-549632924-r7 { fill: #e2e2e2 } + .terminal-2688126662-r1 { fill: #008000 } + .terminal-2688126662-r2 { fill: #c5c8c6 } + .terminal-2688126662-r3 { fill: #e1e1e1 } + .terminal-2688126662-r4 { fill: #1e1e1e } + .terminal-2688126662-r5 { fill: #121212 } + .terminal-2688126662-r6 { fill: #e2e2e2 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - GridApp + GridApp - - - - ────────────────────────────────────────────────────────────────────────────── - foo▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ────────────────────────────────────────────────────────────────────────────── - ────────────────────────────────────────────────────────────────────────────── - foo▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ────────────────────────────────────────────────────────────────────────────── - ────────────────────────────────────────────────────────────────────────────── - foo bar foo bar foo bar foo ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - bar foo bar foo bar foo bar  - foo bar foo bar foo bar ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ────────────────────────────────────────────────────────────────────────────── + + + + ────────────────────────────────────────────────────────────────────────────── + foo▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ────────────────────────────────────────────────────────────────────────────── + ────────────────────────────────────────────────────────────────────────────── + foo▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ────────────────────────────────────────────────────────────────────────────── + ────────────────────────────────────────────────────────────────────────────── + foo bar foo bar foo bar foo ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + bar foo bar foo bar foo bar  + foo bar foo bar foo bar ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ────────────────────────────────────────────────────────────────────────────── @@ -24269,6 +24268,318 @@ ''' # --- +# name: test_rule_horizontal_rules + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HorizontalRulesApp + + + + + + + + + +                         solid (default)                          + + ──────────────────────────────────────────────────────────────── + +                              heavy                               + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +                              thick                               + + ████████████████████████████████████████████████████████████████ + +                              dashed                              + + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + +                              double                              + + ════════════════════════════════════════════════════════════════ + +                              ascii                               + + ---------------------------------------------------------------- + + + + + + ''' +# --- +# name: test_rule_vertical_rules + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VerticalRulesApp + + + + + + + + + + + + solid heavy thick dasheddoubleascii | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + + + + + + + + ''' +# --- # name: test_screen_switch ''' diff --git a/tests/snapshot_tests/snapshot_apps/auto_grid.py b/tests/snapshot_tests/snapshot_apps/auto_grid.py index 7708628475..441ae6401d 100644 --- a/tests/snapshot_tests/snapshot_apps/auto_grid.py +++ b/tests/snapshot_tests/snapshot_apps/auto_grid.py @@ -27,6 +27,8 @@ class GridApp(App): """ + AUTO_FOCUS = None + def compose(self) -> ComposeResult: with Container(id="c1"): yield Label("foo") diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 449e17db33..5580888998 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -289,6 +289,14 @@ def test_progress_bar_completed_styled(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_styled_.py", press=["u"]) +def test_rule_horizontal_rules(snap_compare): + assert snap_compare(WIDGET_EXAMPLES_DIR / "horizontal_rules.py") + + +def test_rule_vertical_rules(snap_compare): + assert snap_compare(WIDGET_EXAMPLES_DIR / "vertical_rules.py") + + def test_select(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "select_widget.py") diff --git a/tests/test_app.py b/tests/test_app.py index 268eebe7b7..09e810bab1 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,3 +1,5 @@ +import contextlib + from textual.app import App, ComposeResult from textual.widgets import Button, Input @@ -67,3 +69,40 @@ def test_setting_sub_title(): app.sub_title = [True, False, 2] assert app.sub_title == "[True, False, 2]" + + +async def test_default_return_code_is_zero(): + app = App() + async with app.run_test(): + app.exit() + assert app.return_code == 0 + + +async def test_return_code_is_one_after_crash(): + class MyApp(App): + def key_p(self): + 1 / 0 + + app = MyApp() + with contextlib.suppress(ZeroDivisionError): + async with app.run_test() as pilot: + await pilot.press("p") + assert app.return_code == 1 + + +async def test_set_return_code(): + app = App() + async with app.run_test(): + app.exit(return_code=42) + assert app.return_code == 42 + + +def test_no_return_code_before_running(): + app = App() + assert app.return_code is None + + +async def test_no_return_code_while_running(): + app = App() + async with app.run_test(): + assert app.return_code is None diff --git a/tests/test_call_later.py b/tests/test_call_x_schedulers.py similarity index 100% rename from tests/test_call_later.py rename to tests/test_call_x_schedulers.py diff --git a/tests/test_focus.py b/tests/test_focus.py index a03b9b53cd..489d808c26 100644 --- a/tests/test_focus.py +++ b/tests/test_focus.py @@ -1,8 +1,10 @@ import pytest from textual.app import App +from textual.containers import Container from textual.screen import Screen from textual.widget import Widget +from textual.widgets import Button class Focusable(Widget, can_focus=True): @@ -201,3 +203,109 @@ def test_focus_next_and_previous_with_str_selector_without_self(screen: Screen): assert screen.focus_previous(".a").id == "foo" assert screen.focus_previous(".a").id == "foo" assert screen.focus_previous(".b").id == "baz" + + +async def test_focus_does_not_move_to_invisible_widgets(): + """Make sure invisible widgets don't get focused by accident. + + This is kind of a regression test for https://github.com/Textualize/textual/issues/3053, + but not really. + """ + + class MyApp(App): + CSS = "#inv { visibility: hidden; }" + + def compose(self): + yield Button("one", id="one") + yield Button("two", id="inv") + yield Button("three", id="three") + + app = MyApp() + async with app.run_test(): + assert app.focused.id == "one" + assert app.screen.focus_next().id == "three" + + +async def test_focus_moves_to_visible_widgets_inside_invisible_containers(): + """Regression test for https://github.com/Textualize/textual/issues/3053.""" + + class MyApp(App): + CSS = """ + #inv { visibility: hidden; } + #three { visibility: visible; } + """ + + def compose(self): + yield Button(id="one") + with Container(id="inv"): + yield Button(id="three") + + app = MyApp() + async with app.run_test(): + assert app.focused.id == "one" + assert app.screen.focus_next().id == "three" + + +async def test_focus_chain_handles_inherited_visibility(): + """Regression test for https://github.com/Textualize/textual/issues/3053 + + This is more or less a test for the interactions between #3053 and #3071. + We want to make sure that the focus chain is computed correctly when going through + a DOM with containers with all sorts of visibilities set. + """ + + class W(Widget): + can_focus = True + + w1 = W(id="one") + c2 = Container(id="two") + w3 = W(id="three") + c4 = Container(id="four") + w5 = W(id="five") + c6 = Container(id="six") + w7 = W(id="seven") + c8 = Container(id="eight") + w9 = W(id="nine") + w10 = W(id="ten") + w11 = W(id="eleven") + w12 = W(id="twelve") + w13 = W(id="thirteen") + + class InheritedVisibilityApp(App[None]): + CSS = """ + #four, #eight, #ten { + visibility: visible; + } + + #six, #thirteen { + visibility: hidden; + } + """ + + def compose(self): + yield w1 # visible, inherited + with c2: # visible, inherited + yield w3 # visible, inherited + with c4: # visible, set + yield w5 # visible, inherited + with c6: # hidden, set + yield w7 # hidden, inherited + with c8: # visible, set + yield w9 # visible, inherited + yield w10 # visible, set + yield w11 # visible, inherited + yield w12 # visible, inherited + yield w13 # invisible, set + + app = InheritedVisibilityApp() + async with app.run_test(): + focus_chain = app.screen.focus_chain + assert focus_chain == [ + w1, + w3, + w5, + w9, + w10, + w11, + w12, + ] diff --git a/tests/test_message_pump.py b/tests/test_message_pump.py index c02978e690..c6f9d921ca 100644 --- a/tests/test_message_pump.py +++ b/tests/test_message_pump.py @@ -87,3 +87,39 @@ async def test_prevent() -> None: await pilot.pause() assert len(app.input_changed_events) == 1 assert app.input_changed_events[0].value == "foo" + + +async def test_prevent_with_call_next() -> None: + """Test for https://github.com/Textualize/textual/issues/3166. + + Does a callback scheduled with `call_next` respect messages that + were prevented when it was scheduled? + """ + + hits = 0 + + class PreventTestApp(App[None]): + def compose(self) -> ComposeResult: + yield Input() + + def change_input(self) -> None: + self.query_one(Input).value += "a" + + def on_input_changed(self) -> None: + nonlocal hits + hits += 1 + + app = PreventTestApp() + async with app.run_test() as pilot: + app.call_next(app.change_input) + await pilot.pause() + assert hits == 1 + + with app.prevent(Input.Changed): + app.call_next(app.change_input) + await pilot.pause() + assert hits == 1 + + app.call_next(app.change_input) + await pilot.pause() + assert hits == 2 diff --git a/tests/test_reactive.py b/tests/test_reactive.py index cb5a6b5f2f..9ab1af192c 100644 --- a/tests/test_reactive.py +++ b/tests/test_reactive.py @@ -499,3 +499,81 @@ def _compute_double(self) -> int: async with PrivateComputeTest().run_test() as pilot: pilot.app.base = 5 assert pilot.app.double == 10 + + +async def test_async_reactive_watch_callbacks_go_on_the_watcher(): + """Regression test for https://github.com/Textualize/textual/issues/3036. + + This makes sure that async callbacks are called. + See the next test for sync callbacks. + """ + + from_app = False + from_holder = False + + class Holder(Widget): + attr = var(None) + + def watch_attr(self): + nonlocal from_holder + from_holder = True + + class MyApp(App): + def __init__(self): + super().__init__() + self.holder = Holder() + + def on_mount(self): + self.watch(self.holder, "attr", self.callback) + + def update(self): + self.holder.attr = "hello world" + + async def callback(self): + nonlocal from_app + from_app = True + + async with MyApp().run_test() as pilot: + pilot.app.update() + await pilot.pause() + assert from_holder + assert from_app + + +async def test_sync_reactive_watch_callbacks_go_on_the_watcher(): + """Regression test for https://github.com/Textualize/textual/issues/3036. + + This makes sure that sync callbacks are called. + See the previous test for async callbacks. + """ + + from_app = False + from_holder = False + + class Holder(Widget): + attr = var(None) + + def watch_attr(self): + nonlocal from_holder + from_holder = True + + class MyApp(App): + def __init__(self): + super().__init__() + self.holder = Holder() + + def on_mount(self): + self.watch(self.holder, "attr", self.callback) + + def update(self): + self.holder.attr = "hello world" + + def callback(self): + nonlocal from_app + from_app = True + + async with MyApp().run_test() as pilot: + pilot.app.update() + await pilot.pause() + assert from_holder + assert from_app diff --git a/tests/test_rule.py b/tests/test_rule.py new file mode 100644 index 0000000000..f754eaf3ff --- /dev/null +++ b/tests/test_rule.py @@ -0,0 +1,26 @@ +import pytest + +from textual.widgets import Rule +from textual.widgets.rule import InvalidLineStyle, InvalidRuleOrientation + + +def test_invalid_rule_orientation(): + with pytest.raises(InvalidRuleOrientation): + Rule(orientation="invalid orientation!") + + +def test_invalid_rule_line_style(): + with pytest.raises(InvalidLineStyle): + Rule(line_style="invalid line style!") + + +def test_invalid_reactive_rule_orientation_change(): + rule = Rule() + with pytest.raises(InvalidRuleOrientation): + rule.orientation = "invalid orientation!" + + +def test_invalid_reactive_rule_line_style_change(): + rule = Rule() + with pytest.raises(InvalidLineStyle): + rule.line_style = "invalid line style!" diff --git a/tests/test_slug.py b/tests/test_slug.py new file mode 100644 index 0000000000..0486966e83 --- /dev/null +++ b/tests/test_slug.py @@ -0,0 +1,62 @@ +import pytest + +from textual._slug import TrackedSlugs, slug + + +@pytest.mark.parametrize( + "text, expected", + [ + ("test", "test"), + ("Test", "test"), + (" Test ", "test"), + ("-test-", "-test-"), + ("!test!", "test"), + ("test!!test", "testtest"), + ("test! !test", "test-test"), + ("test test", "test-test"), + ("test test", "test--test"), + ("test test", "test----------test"), + ("--test", "--test"), + ("test--", "test--"), + ("--test--test--", "--test--test--"), + ("test!\"#$%&'()*+,-./:;<=>?@[]^_`{|}~test", "test-_test"), + ("tëst", "t%C3%ABst"), + ("test🙂test", "testtest"), + ("test🤷test", "testtest"), + ("test🤷🏻‍♀️test", "testtest"), + ], +) +def test_simple_slug(text: str, expected: str) -> None: + """The simple slug function should produce the expected slug.""" + assert slug(text) == expected + + +@pytest.fixture(scope="module") +def tracker() -> TrackedSlugs: + return TrackedSlugs() + + +@pytest.mark.parametrize( + "text, expected", + [ + ("test", "test"), + ("test", "test-1"), + ("test", "test-2"), + ("-test-", "-test-"), + ("-test-", "-test--1"), + ("test!\"#$%&'()*+,-./:;<=>?@[]^_`{|}~test", "test-_test"), + ("test!\"#$%&'()*+,-./:;<=>?@[]^_`{|}~test", "test-_test-1"), + ("tëst", "t%C3%ABst"), + ("tëst", "t%C3%ABst-1"), + ("tëst", "t%C3%ABst-2"), + ("test🙂test", "testtest"), + ("test🤷test", "testtest-1"), + ("test🤷🏻‍♀️test", "testtest-2"), + ("test", "test-3"), + ("test", "test-4"), + (" test ", "test-5"), + ], +) +def test_tracked_slugs(tracker: TrackedSlugs, text: str, expected: str) -> None: + """The tracked slugging class should produce the expected slugs.""" + assert tracker.slug(text) == expected diff --git a/tests/test_visibility_change.py b/tests/test_visibility_change.py deleted file mode 100644 index 7006827ee2..0000000000 --- a/tests/test_visibility_change.py +++ /dev/null @@ -1,43 +0,0 @@ -"""See https://github.com/Textualize/textual/issues/1355 as the motivation for these tests.""" - -from textual.app import App, ComposeResult -from textual.containers import VerticalScroll -from textual.widget import Widget - - -class VisibleTester(App[None]): - """An app for testing visibility changes.""" - - CSS = """ - Widget { - height: 1fr; - } - .hidden { - visibility: hidden; - } - """ - - def compose(self) -> ComposeResult: - yield VerticalScroll( - Widget(id="keep"), Widget(id="hide-via-code"), Widget(id="hide-via-css") - ) - - -async def test_visibility_changes() -> None: - """Test changing visibility via code and CSS.""" - async with VisibleTester().run_test() as pilot: - assert pilot.app.query_one("#keep").visible is True - assert pilot.app.query_one("#hide-via-code").visible is True - assert pilot.app.query_one("#hide-via-css").visible is True - - pilot.app.query_one("#hide-via-code").styles.visibility = "hidden" - await pilot.pause(0) - assert pilot.app.query_one("#keep").visible is True - assert pilot.app.query_one("#hide-via-code").visible is False - assert pilot.app.query_one("#hide-via-css").visible is True - - pilot.app.query_one("#hide-via-css").set_class(True, "hidden") - await pilot.pause(0) - assert pilot.app.query_one("#keep").visible is True - assert pilot.app.query_one("#hide-via-code").visible is False - assert pilot.app.query_one("#hide-via-css").visible is False diff --git a/tests/test_visible.py b/tests/test_visible.py new file mode 100644 index 0000000000..3d991d8588 --- /dev/null +++ b/tests/test_visible.py @@ -0,0 +1,78 @@ +from textual.app import App, ComposeResult +from textual.containers import VerticalScroll +from textual.widget import Widget + + +async def test_visibility_changes() -> None: + """Test changing visibility via code and CSS. + + See https://github.com/Textualize/textual/issues/1355 as the motivation for these tests. + """ + + class VisibleTester(App[None]): + """An app for testing visibility changes.""" + + CSS = """ + Widget { + height: 1fr; + } + .hidden { + visibility: hidden; + } + """ + + def compose(self) -> ComposeResult: + yield VerticalScroll( + Widget(id="keep"), Widget(id="hide-via-code"), Widget(id="hide-via-css") + ) + + async with VisibleTester().run_test() as pilot: + assert pilot.app.query_one("#keep").visible is True + assert pilot.app.query_one("#hide-via-code").visible is True + assert pilot.app.query_one("#hide-via-css").visible is True + + pilot.app.query_one("#hide-via-code").styles.visibility = "hidden" + await pilot.pause(0) + assert pilot.app.query_one("#keep").visible is True + assert pilot.app.query_one("#hide-via-code").visible is False + assert pilot.app.query_one("#hide-via-css").visible is True + + pilot.app.query_one("#hide-via-css").set_class(True, "hidden") + await pilot.pause(0) + assert pilot.app.query_one("#keep").visible is True + assert pilot.app.query_one("#hide-via-code").visible is False + assert pilot.app.query_one("#hide-via-css").visible is False + + +async def test_visible_is_inherited() -> None: + """Regression test for https://github.com/Textualize/textual/issues/3071""" + + class InheritedVisibilityApp(App[None]): + CSS = """ + #four { + visibility: visible; + } + + #six { + visibility: hidden; + } + """ + + def compose(self): + yield Widget(id="one") + with VerticalScroll(id="two"): + yield Widget(id="three") + with VerticalScroll(id="four"): + yield Widget(id="five") + with VerticalScroll(id="six"): + yield Widget(id="seven") + + app = InheritedVisibilityApp() + async with app.run_test(): + assert app.query_one("#one").visible + assert app.query_one("#two").visible + assert app.query_one("#three").visible + assert app.query_one("#four").visible + assert app.query_one("#five").visible + assert not app.query_one("#six").visible + assert not app.query_one("#seven").visible From 9307e973778e7eea50fdf2c02e86ac52f27ffb1b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 09:28:00 +0100 Subject: [PATCH 322/366] Fixing a bunch of typing problems --- src/textual/_text_area_theme.py | 2 +- src/textual/widgets/_text_area.py | 36 +++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index 9dad10b5a7..dbdaf3f83e 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -35,7 +35,7 @@ class TextAreaTheme: be styled bold cyan. """ - name: str | None = None + name: str """The name of the theme.""" base_style: Style | None = None diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index f349f7ef12..24766e3e95 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -269,7 +269,7 @@ def __init__( self._highlight_query: "Query" | None = None """The query that's currently being used for highlighting.""" - self.document: DocumentBase | None = None + self._document: DocumentBase | None = None """The document this widget is currently editing.""" self.theme: TextAreaTheme | None = theme @@ -278,6 +278,13 @@ def __init__( self.language = language """The language of the `TextArea`.""" + @property + def document(self) -> DocumentBase: + if self._document is None: + self._set_document(self.text, self.language) + assert self._document is not None + return self._document + @staticmethod def _get_builtin_highlight_query(language_name: str) -> str: """Get the highlight query for a builtin language. @@ -339,12 +346,13 @@ def _watch_selection(self, selection: Selection) -> None: character = None # Record the location of a matching closing/opening bracket. - match_location = self.find_matching_bracket(character, cursor_location) - self._matching_bracket_location = match_location - if match_location is not None: - match_row, match_column = match_location - if match_row in range(*self._visible_line_indices): - self.refresh_lines(match_row) + if character: + match_location = self.find_matching_bracket(character, cursor_location) + self._matching_bracket_location = match_location + if match_location is not None: + match_row, match_column = match_location + if match_row in range(*self._visible_line_indices): + self.refresh_lines(match_row) def find_matching_bracket( self, bracket: str, search_from: Location @@ -431,8 +439,14 @@ def _watch_theme(self, theme: TextAreaTheme | None) -> None: self.styles.color = None self.styles.background = None else: - self.styles.color = Color.from_rich_color(theme.base_style.color) - self.styles.background = Color.from_rich_color(theme.base_style.bgcolor) + base_style = theme.base_style + if base_style: + color = base_style.color + background = base_style.bgcolor + if color: + self.styles.color = Color.from_rich_color(color) + if background: + self.styles.background = Color.from_rich_color(background) @property def available_themes(self) -> list[str]: @@ -531,7 +545,7 @@ def _set_document(self, text: str, language: str | None) -> None: else: document = Document(text) - self.document = document + self._document = document log.debug(f"setting document: {document!r}") self._build_highlight_map() @@ -560,7 +574,7 @@ def load_document(self, document: DocumentBase) -> None: Args: document: The document to load into the TextArea. """ - self.document = document + self._document = document self.move_cursor((0, 0)) self._refresh_size() From d42dfba232f96b9a7331881a2fec3622bccb183c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 10:21:25 +0100 Subject: [PATCH 323/366] Fixing more typing problems --- docs/examples/widgets/text_area.py | 2 +- src/textual/_text_area_theme.py | 31 ++++++++++++++++++++---------- src/textual/widgets/_text_area.py | 26 ++++++++++++------------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/docs/examples/widgets/text_area.py b/docs/examples/widgets/text_area.py index 90f6f2fe4d..4a9f67e027 100644 --- a/docs/examples/widgets/text_area.py +++ b/docs/examples/widgets/text_area.py @@ -1,6 +1,6 @@ from textual.app import App, ComposeResult -from textual.document import Selection from textual.widgets import TextArea +from textual.widgets.text_area import Selection TEXT = """\ def shrink(self, margin: tuple[int, int, int, int]) -> Region: diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index dbdaf3f83e..d626982d09 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -66,14 +66,24 @@ class TextAreaTheme: def __post_init__(self) -> None: """Generate some styles if they haven't been supplied.""" if self.base_style is None: - self.base_style = Style(color="#f3f3f3", bgcolor=DEFAULT_DARK_SURFACE) + self.base_style = Style() + + if self.base_style.color is None: + self.base_style = Style(color="#f3f3f3", bgcolor=self.base_style.bgcolor) + + if self.base_style.bgcolor is None: + self.base_style = Style( + color=self.base_style.color, bgcolor=DEFAULT_DARK_SURFACE + ) + + assert self.base_style is not None + assert self.base_style.color is not None + assert self.base_style.bgcolor is not None if self.gutter_style is None: self.gutter_style = self.base_style.copy() - background_color = Color.from_rich_color( - self.base_style.background_style.bgcolor - ) + background_color = Color.from_rich_color(self.base_style.bgcolor) if self.cursor_style is None: self.cursor_style = Style( color=background_color.rich_color, @@ -100,7 +110,7 @@ def __post_init__(self) -> None: ) @classmethod - def get_by_name(cls, theme_name: str) -> "TextAreaTheme": + def get_by_name(cls, theme_name: str) -> "TextAreaTheme" | None: """Get a `TextAreaTheme` by name. Given a `theme_name` return the corresponding `TextAreaTheme` object. @@ -111,11 +121,12 @@ def get_by_name(cls, theme_name: str) -> "TextAreaTheme": theme_name: The name of the theme. Returns: - The `TextAreaTheme` corresponding to the name. + The `TextAreaTheme` corresponding to the name or `None` if the theme isn't + found. """ - return _BUILTIN_THEMES.get(theme_name, TextAreaTheme()) + return _BUILTIN_THEMES.get(theme_name) - def get_highlight(self, name: str) -> Style: + def get_highlight(self, name: str) -> Style | None: """Return the Rich style corresponding to the name defined in the tree-sitter highlight query for the current theme. @@ -123,7 +134,7 @@ def get_highlight(self, name: str) -> Style: name: The name of the highlight. Returns: - The `Style` to use for this highlight. + The `Style` to use for this highlight, or `None` if no style. """ return self.token_styles.get(name) @@ -143,7 +154,7 @@ def default(cls) -> TextAreaTheme: Returns: The default TextAreaTheme (probably "monokai"). """ - return DEFAULT_SYNTAX_THEME + return _MONOKAI _MONOKAI = TextAreaTheme( diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 24766e3e95..534466e252 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -272,8 +272,13 @@ def __init__( self._document: DocumentBase | None = None """The document this widget is currently editing.""" - self.theme: TextAreaTheme | None = theme - """The theme of the `TextArea` as set by the user.""" + self.theme: str | None = theme + """The name of the theme of the `TextArea` as set by the user.""" + + self._theme: TextAreaTheme | None = None + """The `TextAreaTheme` corresponding to the set theme name. When the `theme` + reactive is set as a string, the watcher will update this attribute to the + corresponding `TextAreaTheme` object.""" self.language = language """The language of the `TextArea`.""" @@ -424,22 +429,17 @@ def _watch_indent_width(self) -> None: """Changing width of tabs will change document display width.""" self._refresh_size() - def _validate_theme( - self, theme: str | TextAreaTheme | None - ) -> TextAreaTheme | None: - """When the user sets the theme to a string, convert it to a `TextAreaTheme`.""" - if isinstance(theme, str): - theme = TextAreaTheme.get_by_name(theme) - return theme - - def _watch_theme(self, theme: TextAreaTheme | None) -> None: + def _watch_theme(self, theme: str | None) -> None: """We set the styles on this widget when the theme changes, to ensure that if padding is applied, the colours match.""" if theme is None: + self._theme = None self.styles.color = None self.styles.background = None else: - base_style = theme.base_style + theme_object = TextAreaTheme.get_by_name(theme) + self._theme = theme_object + base_style = theme_object.base_style if base_style: color = base_style.color background = base_style.bgcolor @@ -650,7 +650,7 @@ def render_line(self, widget_y: int) -> Strip: if out_of_bounds: return Strip.blank(self.size.width) - theme = self.theme + theme = self._theme # Get the line from the Document. line_string = document.get_line(line_index) From c9eec767b5a12390e5cbf90b659b52780291c5da Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 10:58:06 +0100 Subject: [PATCH 324/366] Correctly setting theme object --- src/textual/widgets/_text_area.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 534466e252..3f1b4a4483 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -269,27 +269,20 @@ def __init__( self._highlight_query: "Query" | None = None """The query that's currently being used for highlighting.""" - self._document: DocumentBase | None = None + self.document: DocumentBase | None = None """The document this widget is currently editing.""" - self.theme: str | None = theme - """The name of the theme of the `TextArea` as set by the user.""" - self._theme: TextAreaTheme | None = None """The `TextAreaTheme` corresponding to the set theme name. When the `theme` reactive is set as a string, the watcher will update this attribute to the corresponding `TextAreaTheme` object.""" + self.theme: str | None = theme + """The name of the theme of the `TextArea` as set by the user.""" + self.language = language """The language of the `TextArea`.""" - @property - def document(self) -> DocumentBase: - if self._document is None: - self._set_document(self.text, self.language) - assert self._document is not None - return self._document - @staticmethod def _get_builtin_highlight_query(language_name: str) -> str: """Get the highlight query for a builtin language. @@ -429,7 +422,7 @@ def _watch_indent_width(self) -> None: """Changing width of tabs will change document display width.""" self._refresh_size() - def _watch_theme(self, theme: str | None) -> None: + def _watch_theme(self, theme: str | TextAreaTheme | None) -> None: """We set the styles on this widget when the theme changes, to ensure that if padding is applied, the colours match.""" if theme is None: @@ -437,7 +430,10 @@ def _watch_theme(self, theme: str | None) -> None: self.styles.color = None self.styles.background = None else: - theme_object = TextAreaTheme.get_by_name(theme) + if isinstance(theme, str): + theme_object = TextAreaTheme.get_by_name(theme) + else: + theme_object = theme self._theme = theme_object base_style = theme_object.base_style if base_style: @@ -545,7 +541,7 @@ def _set_document(self, text: str, language: str | None) -> None: else: document = Document(text) - self._document = document + self.document = document log.debug(f"setting document: {document!r}") self._build_highlight_map() @@ -574,7 +570,7 @@ def load_document(self, document: DocumentBase) -> None: Args: document: The document to load into the TextArea. """ - self._document = document + self.document = document self.move_cursor((0, 0)) self._refresh_size() From b1074ebe42694417288e0bf61180a45ad4d6b928 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 11:25:42 +0100 Subject: [PATCH 325/366] mypy --- src/textual/widgets/_text_area.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 3f1b4a4483..9935b0c71b 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -269,7 +269,7 @@ def __init__( self._highlight_query: "Query" | None = None """The query that's currently being used for highlighting.""" - self.document: DocumentBase | None = None + self.document: DocumentBase = Document(text) """The document this widget is currently editing.""" self._theme: TextAreaTheme | None = None @@ -277,12 +277,12 @@ def __init__( reactive is set as a string, the watcher will update this attribute to the corresponding `TextAreaTheme` object.""" - self.theme: str | None = theme - """The name of the theme of the `TextArea` as set by the user.""" - self.language = language """The language of the `TextArea`.""" + self.theme: str | None = theme + """The name of the theme of the `TextArea` as set by the user.""" + @staticmethod def _get_builtin_highlight_query(language_name: str) -> str: """Get the highlight query for a builtin language. @@ -435,14 +435,15 @@ def _watch_theme(self, theme: str | TextAreaTheme | None) -> None: else: theme_object = theme self._theme = theme_object - base_style = theme_object.base_style - if base_style: - color = base_style.color - background = base_style.bgcolor - if color: - self.styles.color = Color.from_rich_color(color) - if background: - self.styles.background = Color.from_rich_color(background) + if theme_object: + base_style = theme_object.base_style + if base_style: + color = base_style.color + background = base_style.bgcolor + if color: + self.styles.color = Color.from_rich_color(color) + if background: + self.styles.background = Color.from_rich_color(background) @property def available_themes(self) -> list[str]: From 337060b2fdd0f487379fd0223c3ca245b40208bd Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 13:15:03 +0100 Subject: [PATCH 326/366] Small fix to bracket matching --- src/textual/widgets/_text_area.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 9935b0c71b..8b84715d4e 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -341,16 +341,15 @@ def _watch_selection(self, selection: Selection) -> None: try: character = self.document[cursor_row][cursor_column] except IndexError: - character = None + character = "" # Record the location of a matching closing/opening bracket. - if character: - match_location = self.find_matching_bracket(character, cursor_location) - self._matching_bracket_location = match_location - if match_location is not None: - match_row, match_column = match_location - if match_row in range(*self._visible_line_indices): - self.refresh_lines(match_row) + match_location = self.find_matching_bracket(character, cursor_location) + self._matching_bracket_location = match_location + if match_location is not None: + match_row, match_column = match_location + if match_row in range(*self._visible_line_indices): + self.refresh_lines(match_row) def find_matching_bracket( self, bracket: str, search_from: Location From 9dae0ee6865ec3fa01147e38eceb1a738f467e5e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 13:16:14 +0100 Subject: [PATCH 327/366] Improve a docstring --- src/textual/widgets/_text_area.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 8b84715d4e..3afaa357ee 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -361,7 +361,8 @@ def find_matching_bracket( search_from: The location to start the search. Returns: - The `Location` of the matching bracket, or None if it's not found. + The `Location` of the matching bracket, or `None` if it's not found. + If the character is not available for bracket matching, `None` is returned. """ match_location = None bracket_stack = [] From e2bdc4fa75cbb1915a963c27356e3db911832a4a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 13:51:02 +0100 Subject: [PATCH 328/366] Fix docstring --- src/textual/widgets/_text_area.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 3afaa357ee..74b2f8b2e7 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -517,7 +517,14 @@ def register_language( ) def _set_document(self, text: str, language: str | None) -> None: - """Construct and return an appropriate document.""" + """Construct and return an appropriate document. + + Args: + text: The text of the document. + language: The name of the language to use. This must either be a + built-in supported language, or a language previously registered + via the `register_language` method. + """ self._highlight_query = None if TREE_SITTER and language: # Attempt to get the override language. From 7c0d878ebf26a0d608f819df99743b6e3e7c98d6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 14:27:23 +0100 Subject: [PATCH 329/366] Testing builtin and custom languages --- src/textual/widgets/_text_area.py | 12 ++++-- tests/text_area/test_languages.py | 71 +++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 tests/text_area/test_languages.py diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 74b2f8b2e7..65536cf7cd 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -446,7 +446,7 @@ def _watch_theme(self, theme: str | TextAreaTheme | None) -> None: self.styles.background = Color.from_rich_color(background) @property - def available_themes(self) -> list[str]: + def available_themes(self) -> set[str]: """A list of the names of the themes available to the `TextArea`. The values in this list can be assigned `theme` reactive attribute of @@ -459,10 +459,10 @@ def available_themes(self) -> list[str]: (which contain the full theme specification) by calling `TextAreaTheme.builtin_themes()`. """ - return [theme.name for theme in TextAreaTheme.builtin_themes()] + return {theme.name for theme in TextAreaTheme.builtin_themes()} @property - def available_languages(self) -> list[str]: + def available_languages(self) -> set[str]: """A list of the names of languages available to the `TextArea`. The values in this list can be assigned to the `language` reactive attribute @@ -472,7 +472,7 @@ def available_languages(self) -> list[str]: `register_language` method. Builtin languages will be listed before user-registered languages, but there are no other ordering guarantees. """ - return BUILTIN_LANGUAGES + list(self._languages.keys()) + return set(BUILTIN_LANGUAGES) | self._languages.keys() def register_language( self, @@ -515,6 +515,10 @@ def register_language( language=language, highlight_query=highlight_query, ) + # If we updated the currently set language, rebuild the highlights + # using the newly updated highlights query. + if language_name == self.language: + self._set_document(self.text, language_name) def _set_document(self, text: str, language: str | None) -> None: """Construct and return an appropriate document. diff --git a/tests/text_area/test_languages.py b/tests/text_area/test_languages.py new file mode 100644 index 0000000000..747a02a52d --- /dev/null +++ b/tests/text_area/test_languages.py @@ -0,0 +1,71 @@ +from textual.app import App, ComposeResult +from textual.widgets import TextArea + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + yield TextArea("print('hello')", language="python") + + +async def test_setting_builtin_language_via_constructor(): + class MyTextAreaApp(App): + def compose(self) -> ComposeResult: + yield TextArea("print('hello')", language="python") + + app = MyTextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + assert text_area.language == "python" + + text_area.language = "markdown" + assert text_area.language == "markdown" + + +async def test_setting_builtin_language_via_attribute(): + class MyTextAreaApp(App): + def compose(self) -> ComposeResult: + text_area = TextArea("print('hello')") + text_area.language = "python" + yield text_area + + app = MyTextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + assert text_area.language == "python" + + text_area.language = "markdown" + assert text_area.language == "markdown" + + +async def test_register_language(): + app = TextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + # Get the language from py-tree-sitter-languages... + from tree_sitter_languages import get_language + + language = get_language("elm") + # ...and register it with no highlights + text_area.register_language(language, "") + # Switch to the newly registered language + text_area.language = "elm" + + assert text_area.language == "elm" + + +async def test_register_language_existing_language(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + + # Before registering the language, we have highlights as expected. + assert len(text_area._highlights) > 0 + + # Overwriting the highlight query for Python... + text_area.register_language("python", "") + + # We've overridden the highlight query with a blank one, so there are no highlights. + assert text_area._highlights == {} From 40dc36b39a5f1c593eb93ad83296a7500af20409 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 15:06:12 +0100 Subject: [PATCH 330/366] Unit testing theme stuff --- tests/text_area/test_themes.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/text_area/test_themes.py diff --git a/tests/text_area/test_themes.py b/tests/text_area/test_themes.py new file mode 100644 index 0000000000..a0fc2e3f01 --- /dev/null +++ b/tests/text_area/test_themes.py @@ -0,0 +1,30 @@ +import pytest + +from textual.app import App, ComposeResult +from textual.widgets import TextArea + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + yield TextArea("print('hello')", language="python") + + +@pytest.mark.xfail(reason="refactoring") +async def test_default_theme(): + app = TextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + assert text_area.theme == "monokai" + + +async def test_setting_theme_via_constructor(): + class MyTextAreaApp(App): + def compose(self) -> ComposeResult: + yield TextArea("print('hello')", language="python", theme="vscode_dark") + + app = MyTextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + assert text_area.theme == "vscode_dark" From cac28b0d185b71d4b5a106c98436693085649f68 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 16:56:29 +0100 Subject: [PATCH 331/366] Reworking themes --- src/textual/_text_area_theme.py | 4 +-- src/textual/widgets/_text_area.py | 54 ++++++++++++++++++++++++------- tests/text_area/test_languages.py | 5 +++ 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index d626982d09..40c76b8b7c 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -110,7 +110,7 @@ def __post_init__(self) -> None: ) @classmethod - def get_by_name(cls, theme_name: str) -> "TextAreaTheme" | None: + def get_builtin_theme(cls, theme_name: str) -> "TextAreaTheme" | None: """Get a `TextAreaTheme` by name. Given a `theme_name` return the corresponding `TextAreaTheme` object. @@ -348,5 +348,5 @@ def default(cls) -> TextAreaTheme: "github_light": _GITHUB_LIGHT, } -DEFAULT_SYNTAX_THEME = TextAreaTheme.get_by_name("monokai") +DEFAULT_SYNTAX_THEME = TextAreaTheme.get_builtin_theme("monokai") """The default syntax highlighting theme used by Textual.""" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 65536cf7cd..e6a0a50732 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -51,6 +51,10 @@ """A tuple representing a syntax highlight within one line.""" +class ThemeDoesNotExist(Exception): + pass + + @dataclass class TextAreaLanguage: name: str @@ -168,9 +172,7 @@ class TextArea(ScrollView, can_focus=True): it first using `register_language`. """ - theme: Reactive[str | TextAreaTheme | None] = reactive( - TextAreaTheme.default(), always_update=True, init=False - ) + theme: Reactive[str | None] = reactive(None, always_update=True, init=False) """The theme to syntax highlight with. Supply a `SyntaxTheme` object to customise highlighting, or supply a builtin @@ -218,7 +220,7 @@ def __init__( text: str = "", *, language: str | None = None, - theme: str | TextAreaTheme | None = TextAreaTheme.default(), + theme: str | None = None, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -239,7 +241,10 @@ def __init__( self._initial_text = text self._languages: dict[str, TextAreaLanguage] = {} - """Maps language names to their TextAreaLanguage metadata.""" + """Maps language names to TextAreaLanguage.""" + + self._themes: dict[str, TextAreaTheme] = {} + """Maps theme names to TextAreaTheme.""" self.indent_type: Literal["tabs", "spaces"] = "spaces" """Whether to indent using tabs or spaces.""" @@ -422,18 +427,30 @@ def _watch_indent_width(self) -> None: """Changing width of tabs will change document display width.""" self._refresh_size() - def _watch_theme(self, theme: str | TextAreaTheme | None) -> None: + def _watch_theme(self, theme: str | None) -> None: """We set the styles on this widget when the theme changes, to ensure that if padding is applied, the colours match.""" if theme is None: - self._theme = None + self._theme = TextAreaTheme.default() self.styles.color = None self.styles.background = None else: + theme_object = None + + # If the user supplied a string theme name, find it and apply it. if isinstance(theme, str): - theme_object = TextAreaTheme.get_by_name(theme) - else: - theme_object = theme + try: + theme_object = self._themes[theme] + except KeyError: + theme_object = TextAreaTheme.get_builtin_theme(theme) + + if theme_object is None: + raise ThemeDoesNotExist( + f"{theme!r} is not a builtin theme, or has not been registered. " + f"To use a custom theme, register it first using `register_theme`, " + f"then switch to that theme by setting the `TextArea.theme` attribute." + ) + self._theme = theme_object if theme_object: base_style = theme_object.base_style @@ -459,7 +476,22 @@ def available_themes(self) -> set[str]: (which contain the full theme specification) by calling `TextAreaTheme.builtin_themes()`. """ - return {theme.name for theme in TextAreaTheme.builtin_themes()} + return { + theme.name for theme in TextAreaTheme.builtin_themes() + } | self._themes.keys() + + def register_theme(self, theme: TextAreaTheme) -> None: + """Register a theme for use by the `TextArea`. + + After registering a theme, you can set themes by assigning the theme + name to the `TextArea.theme` reactive attribute. For example + `text_area.theme = "my_custom_theme"` where `"my_custom_theme"` is the + name of the theme you registered. + + If you supply a theme with a name that already exists that theme + will be overwritten. + """ + self._themes[theme.name] = theme @property def available_languages(self) -> set[str]: diff --git a/tests/text_area/test_languages.py b/tests/text_area/test_languages.py index 747a02a52d..869aca00f8 100644 --- a/tests/text_area/test_languages.py +++ b/tests/text_area/test_languages.py @@ -48,8 +48,13 @@ async def test_register_language(): from tree_sitter_languages import get_language language = get_language("elm") + # ...and register it with no highlights text_area.register_language(language, "") + + # Ensure that registered language is now available. + assert "elm" in text_area.available_languages + # Switch to the newly registered language text_area.language = "elm" From b987152f474baf540a042a86cf588bf1b0ac9a75 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 17:06:17 +0100 Subject: [PATCH 332/366] Error handling --- src/textual/document/_syntax_aware_document.py | 4 ---- src/textual/widgets/_text_area.py | 11 ++++++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 5ccd806f00..3d64ab85db 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -9,7 +9,6 @@ except ImportError: TREE_SITTER = False -from textual._text_area_theme import TextAreaTheme from textual.document._document import Document, EditResult, Location, _utf8_encode from textual.document._languages import BUILTIN_LANGUAGES @@ -57,9 +56,6 @@ def __init__( self._parser: Parser | None = None """The tree-sitter Parser or None if tree-sitter is unavailable.""" - self._syntax_theme: TextAreaTheme | None = None - """The syntax highlighting theme to use.""" - # If the language is `None`, then avoid doing any parsing related stuff. if isinstance(language, str): if language not in BUILTIN_LANGUAGES: diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index e6a0a50732..6d3eff9629 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -22,7 +22,10 @@ _utf8_encode, ) from textual.document._languages import BUILTIN_LANGUAGES -from textual.document._syntax_aware_document import SyntaxAwareDocument +from textual.document._syntax_aware_document import ( + SyntaxAwareDocument, + SyntaxAwareDocumentError, +) from textual.expand_tabs import expand_tabs_inline if TYPE_CHECKING: @@ -575,8 +578,11 @@ def _set_document(self, text: str, language: str | None) -> None: document: DocumentBase try: document = SyntaxAwareDocument(text, document_language) - except RuntimeError: + except SyntaxAwareDocumentError: document = Document(text) + log.warning( + f"Parser not found for language {document_language!r}. Parsing disabled." + ) else: self._highlight_query = document.prepare_query(highlight_query) elif language and not TREE_SITTER: @@ -586,7 +592,6 @@ def _set_document(self, text: str, language: str | None) -> None: document = Document(text) self.document = document - log.debug(f"setting document: {document!r}") self._build_highlight_map() @property From 0c7102d58f0e91dee29666e516f27ae160757bb4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 17:08:27 +0100 Subject: [PATCH 333/366] Improve error message --- src/textual/widgets/_text_area.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 6d3eff9629..789837c6db 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -586,7 +586,9 @@ def _set_document(self, text: str, language: str | None) -> None: else: self._highlight_query = document.prepare_query(highlight_query) elif language and not TREE_SITTER: - log.warning("Syntax highlighting not available on this architecture.") + log.warning( + "tree-sitter not available in this environment. Parsing disabled." + ) document = Document(text) else: document = Document(text) From a630d7bcfdc03c10aaa84bc156c7466885554c6c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 17:29:34 +0100 Subject: [PATCH 334/366] Testing new theme setting approach, error handling --- src/textual/widgets/_text_area.py | 21 +++++++++++++++++++- src/textual/widgets/text_area.py | 16 ++++++++++------ tests/text_area/test_languages.py | 13 +++++++++++++ tests/text_area/test_themes.py | 32 ++++++++++++++++++++++++++++--- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 789837c6db..f18d86d2fa 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -55,6 +55,18 @@ class ThemeDoesNotExist(Exception): + """Raised when the user tries to use a theme which does not exist. + This means a theme which is not builtin, or has not been registered. + """ + + pass + + +class LanguageDoesNotExist(Exception): + """Raised when the user tries to use a language which does not exist. + This means a language which is not builtin, or has not been registered. + """ + pass @@ -416,6 +428,13 @@ def _validate_selection(self, selection: Selection) -> Selection: def _watch_language(self, language: str | None) -> None: """When the language is updated, update the type of document.""" + if language is not None and language not in self.available_languages: + raise LanguageDoesNotExist( + f"{language!r} is not a builtin language, or it has not been registered. " + f"To use a custom language, register it first using `register_language`, " + f"then switch to it by setting the `TextArea.language` attribute." + ) + self._set_document( self.document.text if self.document is not None else self._initial_text, language, @@ -449,7 +468,7 @@ def _watch_theme(self, theme: str | None) -> None: if theme_object is None: raise ThemeDoesNotExist( - f"{theme!r} is not a builtin theme, or has not been registered. " + f"{theme!r} is not a builtin theme, or it has not been registered. " f"To use a custom theme, register it first using `register_theme`, " f"then switch to that theme by setting the `TextArea.theme` attribute." ) diff --git a/src/textual/widgets/text_area.py b/src/textual/widgets/text_area.py index 6f3a214e9e..82a69e38b3 100644 --- a/src/textual/widgets/text_area.py +++ b/src/textual/widgets/text_area.py @@ -13,21 +13,25 @@ EndColumn, Highlight, HighlightName, + LanguageDoesNotExist, StartColumn, + ThemeDoesNotExist, ) __all__ = [ + "BUILTIN_LANGUAGES", + "Document", + "DocumentBase", "Edit", + "EditResult", "EndColumn", "Highlight", "HighlightName", - "StartColumn", - "TextAreaTheme", - "Document", - "DocumentBase", + "LanguageDoesNotExist", "Location", - "EditResult", "Selection", + "StartColumn", "SyntaxAwareDocument", - "BUILTIN_LANGUAGES", + "TextAreaTheme", + "ThemeDoesNotExist", ] diff --git a/tests/text_area/test_languages.py b/tests/text_area/test_languages.py index 869aca00f8..2daf3089ad 100644 --- a/tests/text_area/test_languages.py +++ b/tests/text_area/test_languages.py @@ -1,5 +1,8 @@ +import pytest + from textual.app import App, ComposeResult from textual.widgets import TextArea +from textual.widgets.text_area import LanguageDoesNotExist class TextAreaApp(App): @@ -39,11 +42,21 @@ def compose(self) -> ComposeResult: assert text_area.language == "markdown" +async def test_setting_unknown_language(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + + with pytest.raises(LanguageDoesNotExist): + text_area.language = "this-language-doesnt-exist" + + async def test_register_language(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) + # Get the language from py-tree-sitter-languages... from tree_sitter_languages import get_language diff --git a/tests/text_area/test_themes.py b/tests/text_area/test_themes.py index a0fc2e3f01..ca2081249b 100644 --- a/tests/text_area/test_themes.py +++ b/tests/text_area/test_themes.py @@ -1,7 +1,9 @@ import pytest +from textual._text_area_theme import TextAreaTheme from textual.app import App, ComposeResult from textual.widgets import TextArea +from textual.widgets._text_area import ThemeDoesNotExist class TextAreaApp(App): @@ -9,16 +11,15 @@ def compose(self) -> ComposeResult: yield TextArea("print('hello')", language="python") -@pytest.mark.xfail(reason="refactoring") async def test_default_theme(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) - assert text_area.theme == "monokai" + assert text_area.theme is None -async def test_setting_theme_via_constructor(): +async def test_setting_builtin_themes(): class MyTextAreaApp(App): def compose(self) -> ComposeResult: yield TextArea("print('hello')", language="python", theme="vscode_dark") @@ -28,3 +29,28 @@ def compose(self) -> ComposeResult: async with app.run_test(): text_area = app.query_one(TextArea) assert text_area.theme == "vscode_dark" + + text_area.theme = "monokai" + assert text_area.theme == "monokai" + + +async def test_setting_unknown_theme(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + with pytest.raises(ThemeDoesNotExist): + text_area.theme = "this-theme-doesnt-exist" + + +async def test_registering_themes(): + app = TextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.register_theme(TextAreaTheme("my-theme")) + + assert "my-theme" in text_area.available_themes + + text_area.theme = "my-theme" + + assert text_area.theme == "my-theme" From 608be9be1c30360dc0388a294847641bde895b56 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 11 Sep 2023 17:40:16 +0100 Subject: [PATCH 335/366] Improvements/tests for theme and language setting --- src/textual/widgets/_text_area.py | 51 +++++++++++++++---------------- tests/text_area/test_languages.py | 8 +++++ tests/text_area/test_themes.py | 11 +++++++ 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index f18d86d2fa..f74ec65ef1 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -452,37 +452,34 @@ def _watch_indent_width(self) -> None: def _watch_theme(self, theme: str | None) -> None: """We set the styles on this widget when the theme changes, to ensure that if padding is applied, the colours match.""" + if theme is None: - self._theme = TextAreaTheme.default() - self.styles.color = None - self.styles.background = None + # If the theme is None, use the default. + theme_object = TextAreaTheme.default() else: - theme_object = None - # If the user supplied a string theme name, find it and apply it. - if isinstance(theme, str): - try: - theme_object = self._themes[theme] - except KeyError: - theme_object = TextAreaTheme.get_builtin_theme(theme) - - if theme_object is None: - raise ThemeDoesNotExist( - f"{theme!r} is not a builtin theme, or it has not been registered. " - f"To use a custom theme, register it first using `register_theme`, " - f"then switch to that theme by setting the `TextArea.theme` attribute." - ) + try: + theme_object = self._themes[theme] + except KeyError: + theme_object = TextAreaTheme.get_builtin_theme(theme) + + if theme_object is None: + raise ThemeDoesNotExist( + f"{theme!r} is not a builtin theme, or it has not been registered. " + f"To use a custom theme, register it first using `register_theme`, " + f"then switch to that theme by setting the `TextArea.theme` attribute." + ) - self._theme = theme_object - if theme_object: - base_style = theme_object.base_style - if base_style: - color = base_style.color - background = base_style.bgcolor - if color: - self.styles.color = Color.from_rich_color(color) - if background: - self.styles.background = Color.from_rich_color(background) + self._theme = theme_object + if theme_object: + base_style = theme_object.base_style + if base_style: + color = base_style.color + background = base_style.bgcolor + if color: + self.styles.color = Color.from_rich_color(color) + if background: + self.styles.background = Color.from_rich_color(background) @property def available_themes(self) -> set[str]: diff --git a/tests/text_area/test_languages.py b/tests/text_area/test_languages.py index 2daf3089ad..dc8a59300a 100644 --- a/tests/text_area/test_languages.py +++ b/tests/text_area/test_languages.py @@ -42,6 +42,14 @@ def compose(self) -> ComposeResult: assert text_area.language == "markdown" +async def test_setting_language_to_none(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.language = None + assert text_area.language is None + + async def test_setting_unknown_language(): app = TextAreaApp() async with app.run_test(): diff --git a/tests/text_area/test_themes.py b/tests/text_area/test_themes.py index ca2081249b..3ea7a1305b 100644 --- a/tests/text_area/test_themes.py +++ b/tests/text_area/test_themes.py @@ -34,6 +34,17 @@ def compose(self) -> ComposeResult: assert text_area.theme == "monokai" +async def test_setting_theme_to_none(): + app = TextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.theme = None + assert text_area.theme is None + # When theme is None, we use the default theme. + assert text_area._theme.name == TextAreaTheme.default().name + + async def test_setting_unknown_theme(): app = TextAreaApp() async with app.run_test(): From fbd0e9dab5494346513faa0983908bfaa59d8eca Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 12 Sep 2023 09:07:17 +0100 Subject: [PATCH 336/366] Remove unused TextArea unfocus snapshot --- tests/snapshot_tests/test_snapshots.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index f26b1f8324..25b70f7a6b 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -708,25 +708,6 @@ def setup_selection(pilot): ) -def test_text_area_unfocus_rendering(snap_compare): - text = """I am a line. - - I am another line. - - I am the final line.""" - - async def setup_selection(pilot): - text_area = pilot.app.query_one(TextArea) - text_area.load_text(text) - text_area.selection = Selection((0, 0), (2, 8)) - - assert snap_compare( - SNAPSHOT_APPS_DIR / "text_area_unfocus.py", - run_before=setup_selection, - terminal_size=(30, text.count("\n") + 1), - ) - - def test_digits(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "digits.py") From 348dde0b90b14ad9b04d6211f315265900f2cccd Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 12 Sep 2023 09:08:14 +0100 Subject: [PATCH 337/366] Update snapshot file --- .../__snapshots__/test_snapshots.ambr | 83 ------------------- 1 file changed, 83 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index d9f6afd2be..bc6c7f9e2c 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -31293,89 +31293,6 @@ ''' # --- -# name: test_text_area_unfocus_rendering - ''' - - - - - - - - - - - - - - - - - - - - - - - TextAreaUnfocusSnapshot - - - - - - - - - - 1  I am a line. - 2  ▌                         - 3      I amanother line.      - 4   - 5      I am the final line.  - - - - - ''' -# --- # name: test_text_log_blank_write ''' From a6a01db87a8c54f4a22b69406801217068313307 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 12 Sep 2023 09:27:49 +0100 Subject: [PATCH 338/366] Adding theme snapshot tests --- src/textual/_text_area_theme.py | 4 +- .../__snapshots__/test_snapshots.ambr | 380 ++++++++++++++++++ tests/snapshot_tests/test_snapshots.py | 35 +- 3 files changed, 416 insertions(+), 3 deletions(-) diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index 40c76b8b7c..a0987a87c6 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -273,7 +273,7 @@ def default(cls) -> TextAreaTheme: "conditional": Style(color="#569cd6"), "keyword.function": Style(color="#569cd6"), "keyword.return": Style(color="#569cd6"), - "keyword.operator": Style(color="#d4d4d4"), + "keyword.operator": Style(color="#569cd6"), "repeat": Style(color="#569cd6"), "exception": Style(color="#569cd6"), "include": Style(color="#569cd6"), @@ -283,7 +283,9 @@ def default(cls) -> TextAreaTheme: "type.class": Style(color="#4EC9B0"), "function": Style(color="#4EC9B0"), "method": Style(color="#4EC9B0"), + "method.call": Style(color="#4EC9B0"), "boolean": Style(color="#7DAF9C"), + "json.null": Style(color="#7DAF9C"), "tag": Style(color="#EFCB43"), "yaml.field": Style(color="#569cd6", bold=True), "json.label": Style(color="#569cd6", bold=True), diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index bc6c7f9e2c..4ec0738b93 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -31293,6 +31293,386 @@ ''' # --- +# name: test_text_area_themes[dracula] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + 1  defhello(name): + 2      x =123 + 3  whilenotFalse:            + 4  print("hello "+ name)  + 5  continue + 6   + + + + + + ''' +# --- +# name: test_text_area_themes[github_light] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + 1  defhello(name): + 2  x=123 + 3  whilenotFalse:            + 4  print("hello "+name + 5  continue + 6   + + + + + + ''' +# --- +# name: test_text_area_themes[monokai] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + 1  defhello(name): + 2      x =123 + 3  whilenotFalse:            + 4  print("hello "+ name)  + 5  continue + 6   + + + + + + ''' +# --- +# name: test_text_area_themes[vscode_dark] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + 1  defhello(name): + 2      x =123 + 3  whilenotFalse:            + 4          print("hello "+ name)  + 5  continue + 6   + + + + + + ''' +# --- # name: test_text_log_blank_write ''' diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 25b70f7a6b..bf8f0132cb 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -5,6 +5,7 @@ from tests.snapshot_tests.language_snippets import SNIPPETS from textual.widgets.text_area import Selection, BUILTIN_LANGUAGES from textual.widgets import TextArea +from textual.widgets.text_area import TextAreaTheme # These paths should be relative to THIS directory. WIDGET_EXAMPLES_DIR = Path("../../docs/examples/widgets") @@ -93,7 +94,8 @@ def test_input_validation(snap_compare): "tab", "3", # This is valid, so -valid should be applied "tab", - *"-2", # -2 is invalid, so -invalid should be applied (and :focus, since we stop here) + *"-2", + # -2 is invalid, so -invalid should be applied (and :focus, since we stop here) ] assert snap_compare(SNAPSHOT_APPS_DIR / "input_validation.py", press=press) @@ -604,15 +606,16 @@ async def run_before(pilot) -> None: def test_command_palette(snap_compare) -> None: - from textual.command_palette import CommandPalette async def run_before(pilot) -> None: await pilot.press("ctrl+@") await pilot.press("A") await pilot.app.query_one(CommandPalette).workers.wait_for_complete() + assert snap_compare(SNAPSHOT_APPS_DIR / "command_palette.py", run_before=run_before) + # --- textual-dev library preview tests --- @@ -708,6 +711,34 @@ def setup_selection(pilot): ) +@pytest.mark.parametrize("theme_name", + [theme.name for theme in TextAreaTheme.builtin_themes()]) +def test_text_area_themes(snap_compare, theme_name): + """Each theme should have its own snapshot with at least some Python + to check that the rendering is sensible. This also ensures that theme + switching results in the display changing correctly.""" + text = """\ +def hello(name): + x = 123 + while not False: + print("hello " + name) + continue +""" + + def setup_theme(pilot): + text_area = pilot.app.query_one(TextArea) + text_area.load_text(text) + text_area.language = "python" + text_area.selection = Selection((0, 1), (1, 9)) + text_area.theme = theme_name + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area.py", + run_before=setup_theme, + terminal_size=(48, text.count("\n") + 2), + ) + + def test_digits(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "digits.py") From 5ca0478f491a61a72a821be362dfc348c7c97750 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 12 Sep 2023 09:31:01 +0100 Subject: [PATCH 339/366] Add `function.call` style binding in dark vscode theme --- src/textual/_text_area_theme.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index a0987a87c6..ceeafc5ac5 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -282,6 +282,7 @@ def default(cls) -> TextAreaTheme: "class": Style(color="#4EC9B0"), "type.class": Style(color="#4EC9B0"), "function": Style(color="#4EC9B0"), + "function.call": Style(color="#4EC9B0"), "method": Style(color="#4EC9B0"), "method.call": Style(color="#4EC9B0"), "boolean": Style(color="#7DAF9C"), From 92fc247f55159929c11e52a15781c4800488a20b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 12 Sep 2023 10:59:10 +0100 Subject: [PATCH 340/366] Renaming a test file --- tests/text_area/{test_themes.py => test_setting_themes.py} | 0 tests/text_area/test_text_area_theme.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/text_area/{test_themes.py => test_setting_themes.py} (100%) create mode 100644 tests/text_area/test_text_area_theme.py diff --git a/tests/text_area/test_themes.py b/tests/text_area/test_setting_themes.py similarity index 100% rename from tests/text_area/test_themes.py rename to tests/text_area/test_setting_themes.py diff --git a/tests/text_area/test_text_area_theme.py b/tests/text_area/test_text_area_theme.py new file mode 100644 index 0000000000..e69de29bb2 From bfc24c8f34722d129c429f5f2798305c135696b3 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 12 Sep 2023 11:05:20 +0100 Subject: [PATCH 341/366] Making active line clearer on vscode theme --- src/textual/_text_area_theme.py | 4 +- .../__snapshots__/test_snapshots.ambr | 56 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index ceeafc5ac5..4c97adc7d2 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -260,9 +260,9 @@ def default(cls) -> TextAreaTheme: base_style=Style(color="#CCCCCC", bgcolor="#1F1F1F"), gutter_style=Style(color="#6E7681", bgcolor="#1F1F1F"), cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"), - cursor_line_style=Style(bgcolor="#252525"), + cursor_line_style=Style(bgcolor="#2b2b2b"), bracket_matching_style=Style(bgcolor="#3a3a3a", bold=True), - cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#232323"), + cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#2b2b2b"), selection_style=Style(bgcolor="#264F78"), token_styles={ "string": Style(color="#ce9178"), diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 4ec0738b93..0ca739cb89 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -31602,70 +31602,70 @@ font-weight: 700; } - .terminal-1700599567-matrix { + .terminal-2963185736-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1700599567-title { + .terminal-2963185736-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1700599567-r1 { fill: #6e7681 } - .terminal-1700599567-r2 { fill: #569cd6 } - .terminal-1700599567-r3 { fill: #cccccc } - .terminal-1700599567-r4 { fill: #4ec9b0 } - .terminal-1700599567-r5 { fill: #c5c8c6 } - .terminal-1700599567-r6 { fill: #b5cea8 } - .terminal-1700599567-r7 { fill: #1e1e1e } - .terminal-1700599567-r8 { fill: #7daf9c } - .terminal-1700599567-r9 { fill: #ce9178 } + .terminal-2963185736-r1 { fill: #6e7681 } + .terminal-2963185736-r2 { fill: #569cd6 } + .terminal-2963185736-r3 { fill: #cccccc } + .terminal-2963185736-r4 { fill: #4ec9b0 } + .terminal-2963185736-r5 { fill: #c5c8c6 } + .terminal-2963185736-r6 { fill: #b5cea8 } + .terminal-2963185736-r7 { fill: #1e1e1e } + .terminal-2963185736-r8 { fill: #7daf9c } + .terminal-2963185736-r9 { fill: #ce9178 } - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - 1  defhello(name): - 2      x =123 - 3  whilenotFalse:            - 4          print("hello "+ name)  - 5  continue - 6   + + + + 1  defhello(name): + 2      x =123 + 3  whilenotFalse:            + 4  print("hello "+ name)  + 5  continue + 6   From b95162085e884865ba671e5f4e2a2801a51501ae Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 12 Sep 2023 11:08:44 +0100 Subject: [PATCH 342/366] Renaming tests --- tests/text_area/test_setting_themes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/text_area/test_setting_themes.py b/tests/text_area/test_setting_themes.py index 3ea7a1305b..8d165a98a9 100644 --- a/tests/text_area/test_setting_themes.py +++ b/tests/text_area/test_setting_themes.py @@ -45,7 +45,7 @@ async def test_setting_theme_to_none(): assert text_area._theme.name == TextAreaTheme.default().name -async def test_setting_unknown_theme(): +async def test_setting_unknown_theme_raises_exception(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) @@ -53,7 +53,7 @@ async def test_setting_unknown_theme(): text_area.theme = "this-theme-doesnt-exist" -async def test_registering_themes(): +async def test_registering_and_setting_theme(): app = TextAreaApp() async with app.run_test(): From 5eb2a4ac218ad2b6571074c185c2d3f470dc25cf Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 12 Sep 2023 16:54:24 +0100 Subject: [PATCH 343/366] A whole lot of docs for TextArea --- docs/examples/widgets/text_area.py | 33 +-- .../widgets/text_area_custom_theme.py | 42 +++ docs/examples/widgets/text_area_selection.py | 23 ++ docs/widget_gallery.md | 6 +- docs/widgets/text_area.md | 277 ++++++++++++++++-- src/textual/document/_document.py | 26 +- .../document/_syntax_aware_document.py | 27 ++ 7 files changed, 368 insertions(+), 66 deletions(-) create mode 100644 docs/examples/widgets/text_area_custom_theme.py create mode 100644 docs/examples/widgets/text_area_selection.py diff --git a/docs/examples/widgets/text_area.py b/docs/examples/widgets/text_area.py index 4a9f67e027..4ee51b66dc 100644 --- a/docs/examples/widgets/text_area.py +++ b/docs/examples/widgets/text_area.py @@ -1,39 +1,20 @@ from textual.app import App, ComposeResult from textual.widgets import TextArea -from textual.widgets.text_area import Selection TEXT = """\ -def shrink(self, margin: tuple[int, int, int, int]) -> Region: - '''Shrink a region by subtracting spacing. +def hello(name): + print("hello" + name) - Args: - margin: Shrink space by `(, , , )`. - - Returns: - The new, smaller region. - ''' - if not any(margin): - return self - top, right, bottom, left = margin - x, y, width, height = self - return Region( - x=x + left, - y=y + top, - width=max(0, width - (left + right)), - height=max(0, height - (top + bottom)), - ) +def goodbye(name): + print("goodbye" + name) """ -class TextAreaExample(App): +class TextAreaSelection(App): def compose(self) -> ComposeResult: - text_area = TextArea() - text_area.load_text(TEXT) - text_area.language = "python" - text_area.selection = Selection((0, 0), (3, 7)) - yield text_area + yield TextArea(TEXT, language="python") -app = TextAreaExample() +app = TextAreaSelection() if __name__ == "__main__": app.run() diff --git a/docs/examples/widgets/text_area_custom_theme.py b/docs/examples/widgets/text_area_custom_theme.py new file mode 100644 index 0000000000..0d9b276c2e --- /dev/null +++ b/docs/examples/widgets/text_area_custom_theme.py @@ -0,0 +1,42 @@ +from rich.style import Style + +from textual._text_area_theme import TextAreaTheme +from textual.app import App, ComposeResult +from textual.widgets import TextArea + +TEXT = """\ +# says hello +def hello(name): + print("hello" + name) + +# says goodbye +def goodbye(name): + print("goodbye" + name) +""" + +MY_THEME = TextAreaTheme( + # This name will be used to refer to the theme... + name="my_cool_theme", + # Basic styles such as background, cursor, selection, gutter, etc... + cursor_style=Style(color="white", bgcolor="blue"), + cursor_line_style=Style(bgcolor="yellow"), + # `token_styles` maps tokens parsed from the document to Rich styles. + token_styles={ + "string": Style(color="red"), + "comment": Style(color="magenta"), + }, +) + + +class TextAreaCustomThemes(App): + def compose(self) -> ComposeResult: + text_area = TextArea(TEXT, language="python") + text_area.cursor_blink = False + text_area.register_theme(MY_THEME) + text_area.theme = "my_cool_theme" + yield text_area + + +app = TextAreaCustomThemes() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/widgets/text_area_selection.py b/docs/examples/widgets/text_area_selection.py new file mode 100644 index 0000000000..3a593ad451 --- /dev/null +++ b/docs/examples/widgets/text_area_selection.py @@ -0,0 +1,23 @@ +from textual.app import App, ComposeResult +from textual.widgets import TextArea +from textual.widgets.text_area import Selection + +TEXT = """\ +def hello(name): + print("hello" + name) + +def goodbye(name): + print("goodbye" + name) +""" + + +class TextAreaExample(App): + def compose(self) -> ComposeResult: + text_area = TextArea(TEXT, language="python") + text_area.selection = Selection(start=(0, 0), end=(2, 0)) # (1)! + yield text_area + + +app = TextAreaExample() +if __name__ == "__main__": + app.run() diff --git a/docs/widget_gallery.md b/docs/widget_gallery.md index 37cbec5160..71940c82e6 100644 --- a/docs/widget_gallery.md +++ b/docs/widget_gallery.md @@ -271,7 +271,7 @@ Displays simple static content. Typically used as a base class. ## Switch -A on / off control, inspired by toggle buttons. +An on / off control, inspired by toggle buttons. [Switch reference](./widgets/switch.md){ .md-button .md-button--primary } @@ -299,11 +299,11 @@ A Combination of Tabs and ContentSwitcher to navigate static content. ## TextArea -A multi-line text area which supports syntax highlighting. +A multi-line text area which supports syntax highlighting various languages. [TextArea reference](./widgets/text_area.md){ .md-button .md-button--primary } -```{.textual path="docs/examples/widgets/text_area.py"} +```{.textual path="docs/examples/widgets/text_area.py" columns="42" lines="8"} ``` ## Tree diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index f9292cec6c..1df3f9dcda 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -1,8 +1,9 @@ # TextArea -!!! tip "Added in version 0.35.0" +!!! tip "Added in version 0.38.0" A widget for editing text which may span multiple lines. +Supports syntax highlighting for a selection of languages. - [x] Focusable - [ ] Container @@ -10,14 +11,13 @@ A widget for editing text which may span multiple lines. ## Guide -### Basic example +### Loading text -Here's an example app which loads some Python code, sets the syntax highlighting language -to Python, and selects some text. +In this example we load some initial text into the `TextArea`, and set the language to `"python"` to enable syntax highlighting. === "Output" - ```{.textual path="docs/examples/widgets/text_area.py"} + ```{.textual path="docs/examples/widgets/text_area.py" columns="42" lines="8"} ``` === "text_area_example.py" @@ -26,33 +26,226 @@ to Python, and selects some text. --8<-- "docs/examples/widgets/text_area.py" ``` -### Styling the `TextArea` +To load content into the `TextArea` after it has already been created, +use the [`load_text`][textual.widgets._text_area.TextArea.load_text] method. -You can use component classes to customise the look and feel of the `TextArea` widget. +To update the parser used for syntax highlighting, set the [`language`][textual.widgets._text_area.TextArea.language] reactive attribute: -TODO +```python +# Set the language to Markdown +text_area.language = "markdown" +``` -### Adding support for custom languages +### Working with the cursor - !!! note - More built-in languages will be added in the future. +The cursor location is available via the `cursor_location` attribute. +Writing a new value to `cursor_location` will immediately update the location of the cursor. +```python +>>> text_area = TextArea() +>>> text_area.cursor_location +(0, 0) +>>> text_area.cursor_location = (0, 4) +>>> text_area.cursor_location +(0, 4) +``` -### Syntax highlighting themes +`cursor_location` is the easiest way to move the cursor programmatically, but it doesn't +allow us to select text. To select text, we can use the `selection` reactive attribute. -### Building on top of `TextArea` +Let's select the first two lines of text in a document by adding `text_area.selection = Selection(start=(0, 0), end=(2, 0))` +to our code: +=== "Output" + + ```{.textual path="docs/examples/widgets/text_area_selection.py" columns="42" lines="8"} + ``` + +=== "text_area_selection.py" + + ```python + --8<-- "docs/examples/widgets/text_area_selection.py" + ``` + + 1. Selects the first two lines of text. + +Note that selections can happen in both directions. That is, `Selection((2, 0), (0, 0))` is also valid. + +!!! tip + + The `end` attribute of the `selection` is always equal to `TextArea.cursor_location`. In other words, + the `cursor_location` attribute is simply a convenience for accessing `text_area.selection.end`. + + +### Reading content from `TextArea` + +You can access the text inside the `TextArea` via the [`text`][textual.widgets._text_area.TextArea.text] property. + +### Editing content inside `TextArea` + +The content of the `TextArea` can be updated using the [`replace`][textual.widgets._text_area.TextArea.replace] method. +This method is the programmatic equivalent of selecting some text then pasting. + +All atomic (single-cursor) edits can be represented by a `replace` operation, but for +convenience, some other utility methods are provided, such as [`insert`][textual.widgets._text_area.TextArea.insert], [`delete`][textual.widgets._text_area.TextArea.delete], and [`clear`][textual.widgets._text_area.TextArea.clear]. + +### Themes + +`TextArea` ships with some builtin themes, and you can easily add your own. + +Themes give you control over the look and feel, including syntax highlighting, +the cursor, the selection, and gutter, and more. + +#### Using builtin themes + +The initial theme of the `TextArea` is determined by the `theme` parameter. + +```python +# Create a TextArea with the 'dracula' theme. +yield TextArea("print(123)", language="python", theme="dracula") +``` + +You can check which themes are available using the [`available_themes`][textual.widgets._text_area.TextArea.available_themes] property. + +```python +>>> text_area = TextArea() +>>> print(text_area.available_themes) +{'dracula', 'github_light', 'monokai', 'vscode_dark'} +``` + +After creating a `TextArea`, you can change the theme by setting the [`theme`][textual.widgets._text_area.TextArea.theme] +attribute to one of the available themes. + +```python +text_area.theme = "vscode_dark" +``` + +On setting this attribute the `TextArea` will immediately refresh to display the updated theme. + +#### Custom themes + +Using custom (non-builtin) themes is two-step process: + +1. Create an instance of [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme]. +2. Register it using [`TextArea.register_theme`][textual.widgets._text_area.TextArea.register_theme]. + +##### 1. Creating a theme + +Let's create a simple theme, `"my_cool_theme"`, which colors the cursor blue, and the cursor line yellow. +Our theme will also syntax highlight strings as red, and comments as magenta. + +```python +from rich.style import Style +from textual.widgets.text_area import TextAreaTheme +# ... +my_theme = TextAreaTheme( + # This name will be used to refer to the theme... + name="my_cool_theme", + # Basic styles such as background, cursor, selection, gutter, etc... + cursor_style=Style(color="white", bgcolor="blue"), + cursor_line_style=Style(bgcolor="yellow"), + # `token_styles` is for syntax highlighting. + # It maps tokens parsed from the document to Rich styles. + token_styles={ + "string": Style(color="red"), + "comment": Style(color="magenta"), + } +) +``` + +The `token_styles` attribute of `TextAreaTheme` is used for syntax highlighting. +For more details, see [syntax highlighting](#syntax-highlighting). + +##### 2. Registering a theme + +With our theme created, we can now register it with the `TextArea` instance. + +```python +text_area.register_theme(my_theme) +``` + +After registering a theme, it'll appear in the `available_themes`: + +```python +>>> print(text_area.available_themes) +{'dracula', 'github_light', 'monokai', 'vscode_dark', 'my_cool_theme'} +``` + +We can now switch to this theme: + +```python +text_area.theme = "my_cool_theme" +``` + +Which immediately updates the appearance of our `TextArea`: + +```{.textual path="docs/examples/widgets/text_area_custom_theme.py" columns="42" lines="8"} +``` + +### Advanced concepts + +#### Syntax highlighting + +Syntax highlighting inside the `TextArea` is powered by a library called [`tree-sitter`](https://tree-sitter.github.io/tree-sitter/). + +Each time you update the document in a `TextArea`, an internal syntax tree is updated. +This tree is frequently _queried_ to find location ranges relevant to syntax highlighting. +We give these ranges _names_, and ultimately map them to Rich styles inside `TextAreaTheme.token_styles`. + +Let's use the `markdown` language to illustrate how this works. + +When the `language` attribute is set to `"markdown"`, a highlight query similar to the one below is used (trimmed for brevity). + +```scheme +(heading_content) @heading +(link) @link +``` + +This highlight query maps `heading_content` nodes returned by the Markdown tree-sitter parser to the name `"heading"`, +and `link` nodes to the name `link`. + +Inside our `TextAreaTheme.token_styles` dict, we can map the name `"heading"` to a Rich style. +Here's a snippet from the "Monokai" theme which does just that: + +```python +TextAreaTheme( + name="monokai", + base_style=Style(color="#f8f8f2", bgcolor="#272822"), + gutter_style=Style(color="#90908a", bgcolor="#272822"), + # ... + token_styles={ + # Colorise headings and make them bold + "heading": Style(color="#F92672", bold=True), + # Colorise and underline Markdown links + "link": Style(color="#66D9EF", underline=True), + # ... + }, +) +``` + +The exact queries used by Textual can be found inside `.scm` files in the GitHub repo. + +#### Adding support for custom languages + +To add support for a language to a `TextArea`, use the [`register_language`][textual.widgets._text_area.TextArea.register_language] method. + +[`py-tree-sitter-languages`](https://github.com/grantjenks/py-tree-sitter-languages) + +!!! note + More built-in languages will be added in the future. ## Reactive attributes -| Name | Type | Default | Description | -|---------------------|---------------------------|-------------------------|---------------------------------------------------| -| `language` | `str \| Language \| None` | `None` | The language to use for syntax highlighting. | -| `theme` | `str \| TextAreaTheme` | `TextAreaTheme.default()` | The theme to use for syntax highlighting. | -| `selection` | `Selection` | `Selection()` | The current selection. | -| `show_line_numbers` | `bool` | `True` | Show or hide line numbers. | -| `indent_width` | `int` | `4` | The number of spaces to indent and width of tabs. | +| Name | Type | Default | Description | +|------------------------|--------------------------|--------------------|--------------------------------------------------| +| `language` | `str | None` | `None` | The language to use for syntax highlighting. | +| `theme` | `str | None` | `TextAreaTheme.default()` | The theme to use for syntax highlighting. | +| `selection` | `Selection` | `Selection()` | The current selection. | +| `show_line_numbers` | `bool` | `True` | Show or hide line numbers. | +| `indent_width` | `int` | `4` | The number of spaces to indent and width of tabs. | +| `match_cursor_bracket` | `bool` | `True` | Enable/disable highlighting matching brackets under cursor. | +| `cursor_blink` | `bool` | `True` | Enable/disable blinking of the cursor when the widget has focus. | ## Bindings @@ -66,33 +259,57 @@ The `TextArea` widget defines the following bindings: ## Component classes -The `TextArea` widget provides the following component classes: - -::: textual.widgets._text_area.TextArea.COMPONENT_CLASSES - options: - show_root_heading: false - show_root_toc_entry: false +The `TextArea` widget defines no component classes. +Styling should be done exclusively via [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme]. ## Additional notes -### Tab characters +### Indentation The character(s) inserted when you press tab is controlled by setting the `indent_type` attribute to either `tabs` or `spaces`. If `indent_type == "spaces"`, pressing ++tab++ will insert `indent_width` spaces. -### Python 3.7 is not supported +### Line separators + +When content is loaded into `TextArea`, the content is scanned from beginning to end +and the first occurrence of a line separator is recorded. + +This separator will then be used when content is later read from the `TextArea` via +the `text` property. The `TextArea` widget does not support exporting text which +contains mixed line endings. -Syntax highlighting is not available on Python 3.7. Highlighting will fail _silently_, so end-users who are running Python 3.7 can still edit text without highlighting, even if a `language` and `syntax_theme` is set. +Similarly, newline characters pasted into the `TextArea` will be converted. + +You can check the line separator of the current document by inspecting `TextArea.document.newline`: + +```python +>>> text_area = TextArea() +>>> text_area.document.newline +'\n' +``` + +### The gutter and line numbers + +The gutter (column on the left containing line numbers) can be toggled by setting +the `show_line_numbers` attribute to `True` or `False`. + +Setting this attribute will immediately repaint the `TextArea` to reflect the new value. ## See also - [`Input`][textual.widgets.Input] - for single-line text input. +- [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme] - for theming the `TextArea`. --- +::: textual.widgets._text_area.TextArea + options: + heading_level: 2 + +--- -::: textual.widgets.TextArea +::: textual.widgets.text_area options: heading_level: 2 diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 3eb0ad11cf..4314f3b7e3 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -3,11 +3,10 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from functools import lru_cache -from typing import TYPE_CHECKING, Any, NamedTuple, Tuple, overload - -from rich.text import Text +from typing import TYPE_CHECKING, NamedTuple, Tuple, overload if TYPE_CHECKING: + from tree_sitter import Node from tree_sitter.binding import Query from textual._cells import cell_len @@ -116,7 +115,7 @@ def get_text_range(self, start: Location, end: Location) -> str: def get_size(self, indent_width: int) -> Size: """Get the size of the document. - The height is generally be the number of lines, and the width + The height is generally the number of lines, and the width is generally the maximum cell length of all the lines. Args: @@ -131,8 +130,21 @@ def query_syntax_tree( query: "Query", start_point: tuple[int, int] | None = None, end_point: tuple[int, int] | None = None, - ) -> Any: - """Query the tree-sitter syntax tree.""" + ) -> list[tuple["Node", str]]: + """Query the tree-sitter syntax tree. + + The default implementation always returns an empty list. + + To support querying in a subclass, this must be implemented. + + Args: + query: The tree-sitter Query to perform. + start_point: The (row, column byte) to start the query at. + end_point: The (row, column byte) to end the query at. + + Returns: + A tuple containing the nodes and text captured by the query. + """ return [] def prepare_query(self, query: str) -> "Query" | None: @@ -336,7 +348,7 @@ def __getitem__(self, line_index: int | slice) -> str | list[str]: Location = Tuple[int, int] -"""A location (row, column) within the document.""" +"""A location (row, column) within the document. Indexing starts at 0.""" class Selection(NamedTuple): diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 3d64ab85db..2ab76920a5 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -75,6 +75,18 @@ def language_name(self) -> str | None: return self.language.name if self.language else None def prepare_query(self, query: str) -> Query | None: + """Prepare a tree-sitter tree query. + + Queries should be prepared once, then reused. + + To execute a query, call `query_syntax_tree`. + + Args: + The string query to prepare. + + Returns: + The prepared query. + """ if not TREE_SITTER: raise SyntaxAwareDocumentError( "Couldn't prepare query - tree-sitter is not available on this architecture." @@ -93,6 +105,21 @@ def query_syntax_tree( start_point: tuple[int, int] | None = None, end_point: tuple[int, int] | None = None, ) -> list[tuple["Node", str]]: + """Query the tree-sitter syntax tree. + + The default implementation always returns an empty list. + + To support querying in a subclass, this must be implemented. + + Args: + query: The tree-sitter Query to perform. + start_point: The (row, column byte) to start the query at. + end_point: The (row, column byte) to end the query at. + + Returns: + A tuple containing the nodes and text captured by the query. + """ + if not TREE_SITTER: raise SyntaxAwareDocumentError( "tree-sitter is not available on this architecture." From eb7bea5d6f051e56659edd1699ab1f8b6f5e09cc Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 12 Sep 2023 16:56:38 +0100 Subject: [PATCH 344/366] Update wording in docs --- docs/widgets/text_area.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 1df3f9dcda..3d0c24033c 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -84,7 +84,7 @@ You can access the text inside the `TextArea` via the [`text`][textual.widgets._ ### Editing content inside `TextArea` The content of the `TextArea` can be updated using the [`replace`][textual.widgets._text_area.TextArea.replace] method. -This method is the programmatic equivalent of selecting some text then pasting. +This method is the programmatic equivalent of selecting some text and then pasting. All atomic (single-cursor) edits can be represented by a `replace` operation, but for convenience, some other utility methods are provided, such as [`insert`][textual.widgets._text_area.TextArea.insert], [`delete`][textual.widgets._text_area.TextArea.delete], and [`clear`][textual.widgets._text_area.TextArea.clear]. @@ -223,7 +223,7 @@ TextAreaTheme( ) ``` -The exact queries used by Textual can be found inside `.scm` files in the GitHub repo. +The exact queries `TextArea` uses for highlighting can be found inside `.scm` files in the GitHub repo. #### Adding support for custom languages From 4ee1946985dd8b3c7e6e790f9f7778f5e2669b59 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 12 Sep 2023 17:00:48 +0100 Subject: [PATCH 345/366] A bit more docs --- docs/widgets/text_area.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 3d0c24033c..1e99c23091 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -192,7 +192,7 @@ Each time you update the document in a `TextArea`, an internal syntax tree is up This tree is frequently _queried_ to find location ranges relevant to syntax highlighting. We give these ranges _names_, and ultimately map them to Rich styles inside `TextAreaTheme.token_styles`. -Let's use the `markdown` language to illustrate how this works. +To illustrate how this works, lets look at how the "Monokai" `TextAreaTheme` highlights Markdown files. When the `language` attribute is set to `"markdown"`, a highlight query similar to the one below is used (trimmed for brevity). @@ -201,7 +201,7 @@ When the `language` attribute is set to `"markdown"`, a highlight query similar (link) @link ``` -This highlight query maps `heading_content` nodes returned by the Markdown tree-sitter parser to the name `"heading"`, +This highlight query maps `heading_content` nodes returned by the Markdown parser to the name `"heading"`, and `link` nodes to the name `link`. Inside our `TextAreaTheme.token_styles` dict, we can map the name `"heading"` to a Rich style. From 768e7b5e2b5c64bf45b6bcf0e2a311296136095e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 13 Sep 2023 11:23:24 +0100 Subject: [PATCH 346/366] Example on adding Java as a custom language --- docs/widgets/text_area.md | 60 ++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 1e99c23091..86352fa0f5 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -1,3 +1,4 @@ + # TextArea !!! tip "Added in version 0.38.0" @@ -36,6 +37,9 @@ To update the parser used for syntax highlighting, set the [`language`][textual. text_area.language = "markdown" ``` +!!! note + More built-in languages will be added in the future. For now, you can [add your own](#adding-support-for-custom-languages). + ### Working with the cursor The cursor location is available via the `cursor_location` attribute. @@ -94,7 +98,7 @@ convenience, some other utility methods are provided, such as [`insert`][textual `TextArea` ships with some builtin themes, and you can easily add your own. Themes give you control over the look and feel, including syntax highlighting, -the cursor, the selection, and gutter, and more. +the cursor, selection, gutter, and more. #### Using builtin themes @@ -153,12 +157,16 @@ my_theme = TextAreaTheme( ) ``` -The `token_styles` attribute of `TextAreaTheme` is used for syntax highlighting. +Attributes like `cursor_style` and `cursor_line_style` apply general language-agnostic +styling to the widget. + +The `token_styles` attribute of `TextAreaTheme` is used for syntax highlighting and +depends on the `language` currently in use. For more details, see [syntax highlighting](#syntax-highlighting). ##### 2. Registering a theme -With our theme created, we can now register it with the `TextArea` instance. +Our theme can now be registered with the `TextArea` instance. ```python text_area.register_theme(my_theme) @@ -171,13 +179,13 @@ After registering a theme, it'll appear in the `available_themes`: {'dracula', 'github_light', 'monokai', 'vscode_dark', 'my_cool_theme'} ``` -We can now switch to this theme: +We can now switch to it: ```python text_area.theme = "my_cool_theme" ``` -Which immediately updates the appearance of our `TextArea`: +This immediately updates the appearance of the `TextArea`: ```{.textual path="docs/examples/widgets/text_area_custom_theme.py" columns="42" lines="8"} ``` @@ -201,10 +209,10 @@ When the `language` attribute is set to `"markdown"`, a highlight query similar (link) @link ``` -This highlight query maps `heading_content` nodes returned by the Markdown parser to the name `"heading"`, -and `link` nodes to the name `link`. +This highlight query maps `heading_content` nodes returned by the Markdown parser to the name `@heading`, +and `link` nodes to the name `@link`. -Inside our `TextAreaTheme.token_styles` dict, we can map the name `"heading"` to a Rich style. +Inside our `TextAreaTheme.token_styles` dict, we can map the name `@heading` to a Rich style. Here's a snippet from the "Monokai" theme which does just that: ```python @@ -214,25 +222,49 @@ TextAreaTheme( gutter_style=Style(color="#90908a", bgcolor="#272822"), # ... token_styles={ - # Colorise headings and make them bold + # Colorise @heading and make them bold "heading": Style(color="#F92672", bold=True), - # Colorise and underline Markdown links + # Colorise and underline @link "link": Style(color="#66D9EF", underline=True), # ... }, ) ``` -The exact queries `TextArea` uses for highlighting can be found inside `.scm` files in the GitHub repo. +To understand which names can be mapped inside `token_styles`, we recommend looking at the existing +themes and highlighting queries (`.scm` files) in the Textual repository. #### Adding support for custom languages To add support for a language to a `TextArea`, use the [`register_language`][textual.widgets._text_area.TextArea.register_language] method. -[`py-tree-sitter-languages`](https://github.com/grantjenks/py-tree-sitter-languages) +To register a language, we require two things: + +1. A tree-sitter `Language` object which contains the grammar for the language. +2. A highlight query which is used for [syntax highlighting](#syntax-highlighting). + +##### Example - adding Java support + +The easiest way to obtain a `Language` object is using the [`py-tree-sitter-languages`](https://github.com/grantjenks/py-tree-sitter-languages) package. Here's how we can use this package to obtain a reference to a `Language` object representing Java: + +```python +from tree_sitter_languages import get_language +java_language = get_language("java") +``` + +The exact version of the parser used when you call `get_language` can be checked via +the [`repos.txt` file](https://github.com/grantjenks/py-tree-sitter-languages/blob/a6d4f7c903bf647be1bdcfa504df967d13e40427/repos.txt) in +the version of `py-tree-sitter-languages` you're using. This file contains links to the GitHub +repos and commit hashes of the tree-sitter parsers. Inside these repos, you can often find pre-made highlight queries in `queries/highlights.scm`, +and a JSON file showing all the available node types which can be used in highlight queries at `src/node-types.json`. + +Since we're adding support for Java, lets grab the highlight query from the repo by following these steps. +Be sure to check the license in the repo before copying the query: + +1. Open [`repos.txt` file](https://github.com/grantjenks/py-tree-sitter-languages/blob/a6d4f7c903bf647be1bdcfa504df967d13e40427/repos.txt). +2. Find the link corresponding to `tree-sitter-java` and go to the repo on GitHub (you may also need to go to the specific commit referenced in `repos.txt`). +3. Go to [`queries/highlights.scm`](https://github.com/tree-sitter/tree-sitter-java/blob/83044af4950e9f1adb46a20f616d10934930ce7e/queries/highlights.scm) to see the example highlight query for Java. -!!! note - More built-in languages will be added in the future. ## Reactive attributes From cbcbe94bafde6a8c1870f3f22d80d82afa91aa40 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 13 Sep 2023 12:42:11 +0100 Subject: [PATCH 347/366] More custom language docs --- docs/examples/widgets/java_highlights.scm | 140 ++++++++++++++++++ .../widgets/text_area_custom_language.py | 34 +++++ docs/widgets/text_area.md | 20 ++- 3 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 docs/examples/widgets/java_highlights.scm create mode 100644 docs/examples/widgets/text_area_custom_language.py diff --git a/docs/examples/widgets/java_highlights.scm b/docs/examples/widgets/java_highlights.scm new file mode 100644 index 0000000000..b6259be125 --- /dev/null +++ b/docs/examples/widgets/java_highlights.scm @@ -0,0 +1,140 @@ +; Methods + +(method_declaration + name: (identifier) @function.method) +(method_invocation + name: (identifier) @function.method) +(super) @function.builtin + +; Annotations + +(annotation + name: (identifier) @attribute) +(marker_annotation + name: (identifier) @attribute) + +"@" @operator + +; Types + +(type_identifier) @type + +(interface_declaration + name: (identifier) @type) +(class_declaration + name: (identifier) @type) +(enum_declaration + name: (identifier) @type) + +((field_access + object: (identifier) @type) + (#match? @type "^[A-Z]")) +((scoped_identifier + scope: (identifier) @type) + (#match? @type "^[A-Z]")) +((method_invocation + object: (identifier) @type) + (#match? @type "^[A-Z]")) +((method_reference + . (identifier) @type) + (#match? @type "^[A-Z]")) + +(constructor_declaration + name: (identifier) @type) + +[ + (boolean_type) + (integral_type) + (floating_point_type) + (floating_point_type) + (void_type) +] @type.builtin + +; Variables + +((identifier) @constant + (#match? @constant "^_*[A-Z][A-Z\\d_]+$")) + +(identifier) @variable + +(this) @variable.builtin + +; Literals + +[ + (hex_integer_literal) + (decimal_integer_literal) + (octal_integer_literal) + (decimal_floating_point_literal) + (hex_floating_point_literal) +] @number + +[ + (character_literal) + (string_literal) +] @string + +[ + (true) + (false) + (null_literal) +] @constant.builtin + +[ + (line_comment) + (block_comment) +] @comment + +; Keywords + +[ + "abstract" + "assert" + "break" + "case" + "catch" + "class" + "continue" + "default" + "do" + "else" + "enum" + "exports" + "extends" + "final" + "finally" + "for" + "if" + "implements" + "import" + "instanceof" + "interface" + "module" + "native" + "new" + "non-sealed" + "open" + "opens" + "package" + "private" + "protected" + "provides" + "public" + "requires" + "return" + "sealed" + "static" + "strictfp" + "switch" + "synchronized" + "throw" + "throws" + "to" + "transient" + "transitive" + "try" + "uses" + "volatile" + "while" + "with" +] @keyword diff --git a/docs/examples/widgets/text_area_custom_language.py b/docs/examples/widgets/text_area_custom_language.py new file mode 100644 index 0000000000..70ee7e16b9 --- /dev/null +++ b/docs/examples/widgets/text_area_custom_language.py @@ -0,0 +1,34 @@ +from pathlib import Path + +from tree_sitter_languages import get_language + +from textual.app import App, ComposeResult +from textual.widgets import TextArea + +java_language = get_language("java") +java_highlight_query = (Path(__file__).parent / "java_highlights.scm").read_text() +java_code = """\ +class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } +} +""" + + +class TextAreaCustomLanguage(App): + def compose(self) -> ComposeResult: + text_area = TextArea(text=java_code) + text_area.cursor_blink = False + + # Register the Java language and highlight query + text_area.register_language(java_language, java_highlight_query) + + # Switch to Java + text_area.language = "java" + yield text_area + + +app = TextAreaCustomLanguage() +if __name__ == "__main__": + app.run() diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 86352fa0f5..54b71bf49b 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -263,9 +263,27 @@ Be sure to check the license in the repo before copying the query: 1. Open [`repos.txt` file](https://github.com/grantjenks/py-tree-sitter-languages/blob/a6d4f7c903bf647be1bdcfa504df967d13e40427/repos.txt). 2. Find the link corresponding to `tree-sitter-java` and go to the repo on GitHub (you may also need to go to the specific commit referenced in `repos.txt`). -3. Go to [`queries/highlights.scm`](https://github.com/tree-sitter/tree-sitter-java/blob/83044af4950e9f1adb46a20f616d10934930ce7e/queries/highlights.scm) to see the example highlight query for Java. +3. Go to [`queries/highlights.scm`](https://github.com/tree-sitter/tree-sitter-java/blob/ac14b4b1884102839455d32543ab6d53ae089ab7/queries/highlights.scm) to see the example highlight query for Java. +!!! note + + It's important to use a highlight query which is compatible with the parser, so + pay attention to the commit hash when visiting the repo via `repos.txt`. + +We now have our `Language` and our highlight query, so we can register Java as a language. + +```python +--8<-- "docs/examples/widgets/text_area_custom_languages.py" +``` + +Running our app, we can see that the Java code is highlighted. + +```{.textual path="docs/examples/widgets/text_area_custom_languages.py" columns="52" lines="8"} +``` + +However, some tokens in the document aren't highlighted like we might expect. +TODO - add method for adding style mapping. ## Reactive attributes From cdb3e7a93fbfcb19f042636d17f1033ebef5d658 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 13 Sep 2023 15:46:28 +0100 Subject: [PATCH 348/366] Finishing up custom themeing/syntax highlighting guide for TextArea --- .../widgets/text_area_custom_theme.py | 4 +- docs/widgets/text_area.md | 51 ++++++++++++------- src/textual/_text_area_theme.py | 22 ++++---- src/textual/document/_document.py | 5 ++ src/textual/widgets/_text_area.py | 2 +- 5 files changed, 52 insertions(+), 32 deletions(-) diff --git a/docs/examples/widgets/text_area_custom_theme.py b/docs/examples/widgets/text_area_custom_theme.py index 0d9b276c2e..c2c81a115f 100644 --- a/docs/examples/widgets/text_area_custom_theme.py +++ b/docs/examples/widgets/text_area_custom_theme.py @@ -20,8 +20,8 @@ def goodbye(name): # Basic styles such as background, cursor, selection, gutter, etc... cursor_style=Style(color="white", bgcolor="blue"), cursor_line_style=Style(bgcolor="yellow"), - # `token_styles` maps tokens parsed from the document to Rich styles. - token_styles={ + # `syntax_styles` maps tokens parsed from the document to Rich styles. + syntax_styles={ "string": Style(color="red"), "comment": Style(color="magenta"), }, diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 54b71bf49b..c94a259ffa 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -148,9 +148,9 @@ my_theme = TextAreaTheme( # Basic styles such as background, cursor, selection, gutter, etc... cursor_style=Style(color="white", bgcolor="blue"), cursor_line_style=Style(bgcolor="yellow"), - # `token_styles` is for syntax highlighting. + # `syntax_styles` is for syntax highlighting. # It maps tokens parsed from the document to Rich styles. - token_styles={ + syntax_styles={ "string": Style(color="red"), "comment": Style(color="magenta"), } @@ -160,7 +160,7 @@ my_theme = TextAreaTheme( Attributes like `cursor_style` and `cursor_line_style` apply general language-agnostic styling to the widget. -The `token_styles` attribute of `TextAreaTheme` is used for syntax highlighting and +The `syntax_styles` attribute of `TextAreaTheme` is used for syntax highlighting and depends on the `language` currently in use. For more details, see [syntax highlighting](#syntax-highlighting). @@ -198,7 +198,7 @@ Syntax highlighting inside the `TextArea` is powered by a library called [`tree- Each time you update the document in a `TextArea`, an internal syntax tree is updated. This tree is frequently _queried_ to find location ranges relevant to syntax highlighting. -We give these ranges _names_, and ultimately map them to Rich styles inside `TextAreaTheme.token_styles`. +We give these ranges _names_, and ultimately map them to Rich styles inside `TextAreaTheme.syntax_styles`. To illustrate how this works, lets look at how the "Monokai" `TextAreaTheme` highlights Markdown files. @@ -212,7 +212,7 @@ When the `language` attribute is set to `"markdown"`, a highlight query similar This highlight query maps `heading_content` nodes returned by the Markdown parser to the name `@heading`, and `link` nodes to the name `@link`. -Inside our `TextAreaTheme.token_styles` dict, we can map the name `@heading` to a Rich style. +Inside our `TextAreaTheme.syntax_styles` dict, we can map the name `@heading` to a Rich style. Here's a snippet from the "Monokai" theme which does just that: ```python @@ -221,7 +221,7 @@ TextAreaTheme( base_style=Style(color="#f8f8f2", bgcolor="#272822"), gutter_style=Style(color="#90908a", bgcolor="#272822"), # ... - token_styles={ + syntax_styles={ # Colorise @heading and make them bold "heading": Style(color="#F92672", bold=True), # Colorise and underline @link @@ -231,9 +231,15 @@ TextAreaTheme( ) ``` -To understand which names can be mapped inside `token_styles`, we recommend looking at the existing +To understand which names can be mapped inside `syntax_styles`, we recommend looking at the existing themes and highlighting queries (`.scm` files) in the Textual repository. +!!! tip + + You may also wish to take a look at the contents of `TextArea._highlights` on an + active `TextArea` instance to see which highlights have been generated for the + open document. + #### Adding support for custom languages To add support for a language to a `TextArea`, use the [`register_language`][textual.widgets._text_area.TextArea.register_language] method. @@ -255,35 +261,42 @@ java_language = get_language("java") The exact version of the parser used when you call `get_language` can be checked via the [`repos.txt` file](https://github.com/grantjenks/py-tree-sitter-languages/blob/a6d4f7c903bf647be1bdcfa504df967d13e40427/repos.txt) in the version of `py-tree-sitter-languages` you're using. This file contains links to the GitHub -repos and commit hashes of the tree-sitter parsers. Inside these repos, you can often find pre-made highlight queries in `queries/highlights.scm`, -and a JSON file showing all the available node types which can be used in highlight queries at `src/node-types.json`. +repos and commit hashes of the tree-sitter parsers. In these repos you can often find pre-made highlight queries at `queries/highlights.scm`, +and a file showing all the available node types which can be used in highlight queries at `src/node-types.json`. -Since we're adding support for Java, lets grab the highlight query from the repo by following these steps. -Be sure to check the license in the repo before copying the query: +Since we're adding support for Java, lets grab the Java highlight query from the repo by following these steps: -1. Open [`repos.txt` file](https://github.com/grantjenks/py-tree-sitter-languages/blob/a6d4f7c903bf647be1bdcfa504df967d13e40427/repos.txt). +1. Open [`repos.txt` file](https://github.com/grantjenks/py-tree-sitter-languages/blob/a6d4f7c903bf647be1bdcfa504df967d13e40427/repos.txt) from the `py-tree-sitter-languages` repo. 2. Find the link corresponding to `tree-sitter-java` and go to the repo on GitHub (you may also need to go to the specific commit referenced in `repos.txt`). 3. Go to [`queries/highlights.scm`](https://github.com/tree-sitter/tree-sitter-java/blob/ac14b4b1884102839455d32543ab6d53ae089ab7/queries/highlights.scm) to see the example highlight query for Java. -!!! note +Be sure to check the license in the repo to ensure it can be freely copied. - It's important to use a highlight query which is compatible with the parser, so +!!! warning + + It's important to use a highlight query which is compatible with the parser in use, so pay attention to the commit hash when visiting the repo via `repos.txt`. We now have our `Language` and our highlight query, so we can register Java as a language. ```python ---8<-- "docs/examples/widgets/text_area_custom_languages.py" +--8<-- "docs/examples/widgets/text_area_custom_language.py" ``` Running our app, we can see that the Java code is highlighted. +We can freely edit the text, and the syntax highlighting will update immediately. -```{.textual path="docs/examples/widgets/text_area_custom_languages.py" columns="52" lines="8"} +```{.textual path="docs/examples/widgets/text_area_custom_language.py" columns="52" lines="8"} ``` -However, some tokens in the document aren't highlighted like we might expect. +Recall that we map names (like `@heading`) from the tree-sitter highlight query to Rich style objects inside the `TextAreaTheme.syntax_styles` dictionary. +If you notice some highlights are missing after registering a language, it's likely that the current theme simply doesn't contain a mapping for that name. +Adding a new to `syntax_styles` should resolve the issue. + +!!! tip -TODO - add method for adding style mapping. + The names assigned in tree-sitter highlight queries are often reused across multiple languages. + For example, `@string` is used in many languages to highlight strings. ## Reactive attributes @@ -347,6 +360,8 @@ the `show_line_numbers` attribute to `True` or `False`. Setting this attribute will immediately repaint the `TextArea` to reflect the new value. +### The file system + ## See also - [`Input`][textual.widgets.Input] - for single-line text input. diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index 4c97adc7d2..168ae922ae 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -28,11 +28,11 @@ class TextAreaTheme: node is used (as will be the case when language="markdown"). ``` - TextAreaTheme('my_theme', token_styles={'heading': Style(color='cyan', bold=True)}) + TextAreaTheme('my_theme', syntax_styles={'heading': Style(color='cyan', bold=True)}) ``` - We can supply this theme to our `TextArea`, and headings in our markdown files will - be styled bold cyan. + We can register this theme with our `TextArea` using the `register_theme` method, + and headings in our markdown files will be styled bold cyan. """ name: str @@ -60,7 +60,7 @@ class TextAreaTheme: selection_style: Style | None = None """The style of the selection. If `None` a default selection Style will be generated.""" - token_styles: dict[str, Style] = field(default_factory=dict) + syntax_styles: dict[str, Style] = field(default_factory=dict) """The mapping of tree-sitter names from the `highlight_query` to Rich styles.""" def __post_init__(self) -> None: @@ -136,7 +136,7 @@ def get_highlight(self, name: str) -> Style | None: Returns: The `Style` to use for this highlight, or `None` if no style. """ - return self.token_styles.get(name) + return self.syntax_styles.get(name) @classmethod def builtin_themes(cls) -> list[TextAreaTheme]: @@ -166,7 +166,7 @@ def default(cls) -> TextAreaTheme: cursor_line_gutter_style=Style(color="#c2c2bf", bgcolor="#3e3d32"), bracket_matching_style=Style(bgcolor="#838889", bold=True), selection_style=Style(bgcolor="#65686a"), - token_styles={ + syntax_styles={ "string": Style(color="#E6DB74"), "string.documentation": Style(color="#E6DB74"), "comment": Style(color="#75715E"), @@ -215,7 +215,7 @@ def default(cls) -> TextAreaTheme: cursor_line_gutter_style=Style(color="#c2c2bf", bgcolor="#282b45", bold=True), bracket_matching_style=Style(bgcolor="#99999d", bold=True, underline=True), selection_style=Style(bgcolor="#44475A"), - token_styles={ + syntax_styles={ "string": Style(color="#f1fa8c"), "string.documentation": Style(color="#f1fa8c"), "comment": Style(color="#6272a4"), @@ -264,7 +264,7 @@ def default(cls) -> TextAreaTheme: bracket_matching_style=Style(bgcolor="#3a3a3a", bold=True), cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#2b2b2b"), selection_style=Style(bgcolor="#264F78"), - token_styles={ + syntax_styles={ "string": Style(color="#ce9178"), "string.documentation": Style(color="#ce9178"), "comment": Style(color="#6A9955"), @@ -310,7 +310,7 @@ def default(cls) -> TextAreaTheme: bracket_matching_style=Style(color="#24292e", underline=True), cursor_line_gutter_style=Style(color="#A4A4A4", bgcolor="#ebebeb"), selection_style=Style(bgcolor="#c8c8fa"), - token_styles={ + syntax_styles={ "string": Style(color="#093069"), "string.documentation": Style(color="#093069"), "comment": Style(color="#6a737d"), @@ -351,5 +351,5 @@ def default(cls) -> TextAreaTheme: "github_light": _GITHUB_LIGHT, } -DEFAULT_SYNTAX_THEME = TextAreaTheme.get_builtin_theme("monokai") -"""The default syntax highlighting theme used by Textual.""" +DEFAULT_THEME = TextAreaTheme.get_builtin_theme("monokai") +"""The default TextAreaTheme used by Textual.""" diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 4314f3b7e3..fcb1bcfa03 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -85,6 +85,11 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult def text(self) -> str: """The text from the document as a string.""" + @property + @abstractmethod + def newline(self) -> Newline: + """Return the line separator used in the document.""" + @abstractmethod def get_line(self, index: int) -> str: """Returns the line with the given index from the document. diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index f74ec65ef1..62d4e917f7 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -735,7 +735,7 @@ def render_line(self, widget_y: int) -> Strip: if highlights and theme: line_bytes = _utf8_encode(line_string) byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) - get_highlight_from_theme = theme.token_styles.get + get_highlight_from_theme = theme.syntax_styles.get line_highlights = highlights[line_index] for highlight_start, highlight_end, highlight_name in line_highlights: node_style = get_highlight_from_theme(highlight_name) From 9287264b264b7c1f47b22efb5c718ab6592389a5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 13 Sep 2023 15:55:00 +0100 Subject: [PATCH 349/366] Add note on potential issue --- docs/widgets/text_area.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index c94a259ffa..6dc1da9a3f 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -290,8 +290,10 @@ We can freely edit the text, and the syntax highlighting will update immediately ``` Recall that we map names (like `@heading`) from the tree-sitter highlight query to Rich style objects inside the `TextAreaTheme.syntax_styles` dictionary. -If you notice some highlights are missing after registering a language, it's likely that the current theme simply doesn't contain a mapping for that name. -Adding a new to `syntax_styles` should resolve the issue. +If you notice some highlights are missing after registering a language, the issue may be: + +1. The current `TextAreaTheme` doesn't contain a mapping for the name in the highlight query. Adding a new to `syntax_styles` should resolve the issue. +2. The highlight query doesn't assign a name to the pattern you expect to be highlighted. !!! tip From ba00c33ed14471b3727c0667732043d5abc37d82 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 13 Sep 2023 16:30:43 +0100 Subject: [PATCH 350/366] Fix wording --- docs/widgets/text_area.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 6dc1da9a3f..77dd396b0d 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -293,7 +293,7 @@ Recall that we map names (like `@heading`) from the tree-sitter highlight query If you notice some highlights are missing after registering a language, the issue may be: 1. The current `TextAreaTheme` doesn't contain a mapping for the name in the highlight query. Adding a new to `syntax_styles` should resolve the issue. -2. The highlight query doesn't assign a name to the pattern you expect to be highlighted. +2. The highlight query doesn't assign a name to the pattern you expect to be highlighted. In this case you'll need to update the highlight query to assign to the name. !!! tip From 11f8e092665bbe78225075e78041d7759aea6dc0 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 13 Sep 2023 17:04:14 +0100 Subject: [PATCH 351/366] Add note on Apple Silicon Python 3.7 fallback --- docs/widgets/text_area.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 77dd396b0d..d21a64aad4 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -258,6 +258,10 @@ from tree_sitter_languages import get_language java_language = get_language("java") ``` +!!! note + + `py-tree-sitter-languages` may not be available on some architectures (e.g. Macbooks with Apple Silicon running Python 3.7). + The exact version of the parser used when you call `get_language` can be checked via the [`repos.txt` file](https://github.com/grantjenks/py-tree-sitter-languages/blob/a6d4f7c903bf647be1bdcfa504df967d13e40427/repos.txt) in the version of `py-tree-sitter-languages` you're using. This file contains links to the GitHub From 53dfd16ca15a1e20d7932ef7edbbe045c1897112 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 13 Sep 2023 17:06:06 +0100 Subject: [PATCH 352/366] Add another note on Apple Silicon Python 3.7 fallback --- docs/widgets/text_area.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index d21a64aad4..822139e289 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -37,6 +37,9 @@ To update the parser used for syntax highlighting, set the [`language`][textual. text_area.language = "markdown" ``` +!!! note + Syntax highlighting is unavailable on Apple Silicon machines running Python 3.7. + !!! note More built-in languages will be added in the future. For now, you can [add your own](#adding-support-for-custom-languages). From db3fee9a29f9fd979db736cad2d5e2a7d30cfa88 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 13 Sep 2023 19:24:55 +0100 Subject: [PATCH 353/366] Fix class names in example files --- docs/examples/widgets/text_area.py | 4 ++-- docs/examples/widgets/text_area_selection.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples/widgets/text_area.py b/docs/examples/widgets/text_area.py index 4ee51b66dc..2e0e31c060 100644 --- a/docs/examples/widgets/text_area.py +++ b/docs/examples/widgets/text_area.py @@ -10,11 +10,11 @@ def goodbye(name): """ -class TextAreaSelection(App): +class TextAreaExample(App): def compose(self) -> ComposeResult: yield TextArea(TEXT, language="python") -app = TextAreaSelection() +app = TextAreaExample() if __name__ == "__main__": app.run() diff --git a/docs/examples/widgets/text_area_selection.py b/docs/examples/widgets/text_area_selection.py index 3a593ad451..4165eb2d2d 100644 --- a/docs/examples/widgets/text_area_selection.py +++ b/docs/examples/widgets/text_area_selection.py @@ -11,13 +11,13 @@ def goodbye(name): """ -class TextAreaExample(App): +class TextAreaSelection(App): def compose(self) -> ComposeResult: text_area = TextArea(TEXT, language="python") text_area.selection = Selection(start=(0, 0), end=(2, 0)) # (1)! yield text_area -app = TextAreaExample() +app = TextAreaSelection() if __name__ == "__main__": app.run() From 2613c450bf084faa32886ee4e75279f00cc2dd43 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 14 Sep 2023 11:47:39 +0100 Subject: [PATCH 354/366] Add some documentation for useful TextArea APIs --- .../{text_area.py => text_area_example.py} | 0 docs/widgets/text_area.md | 82 +++++++++++++++---- src/textual/_text_area_theme.py | 4 +- src/textual/widgets/_text_area.py | 49 +++++------ 4 files changed, 93 insertions(+), 42 deletions(-) rename docs/examples/widgets/{text_area.py => text_area_example.py} (100%) diff --git a/docs/examples/widgets/text_area.py b/docs/examples/widgets/text_area_example.py similarity index 100% rename from docs/examples/widgets/text_area.py rename to docs/examples/widgets/text_area_example.py diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 822139e289..cce5c4aca3 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -18,13 +18,13 @@ In this example we load some initial text into the `TextArea`, and set the langu === "Output" - ```{.textual path="docs/examples/widgets/text_area.py" columns="42" lines="8"} + ```{.textual path="docs/examples/widgets/text_area_example.py" columns="42" lines="8"} ``` === "text_area_example.py" ```python - --8<-- "docs/examples/widgets/text_area.py" + --8<-- "docs/examples/widgets/text_area_example.py" ``` To load content into the `TextArea` after it has already been created, @@ -43,9 +43,29 @@ text_area.language = "markdown" !!! note More built-in languages will be added in the future. For now, you can [add your own](#adding-support-for-custom-languages). + +### Reading content from `TextArea` + +There are a number of ways to retrieve content from the `TextArea`: + +- The [`TextArea.text`][textual.widgets._text_area.TextArea.text] property returns all content in the text area as a string. +- The [`TextArea.selected_text`][textual.widgets._text_area.TextArea.selected_text] property returns the text corresponding to the current selection. +- The [`TextArea.get_text_range`][textual.widgets._text_area.TextArea.get_text_range] method returns the text between two locations. + +### Editing content inside `TextArea` + +The content of the `TextArea` can be updated using the [`replace`][textual.widgets._text_area.TextArea.replace] method. +This method is the programmatic equivalent of selecting some text and then pasting. + +All atomic (single-cursor) edits can be represented by a `replace` operation, but for +convenience, some other utility methods are provided, such as [`insert`][textual.widgets._text_area.TextArea.insert], [`delete`][textual.widgets._text_area.TextArea.delete], and [`clear`][textual.widgets._text_area.TextArea.clear]. + ### Working with the cursor -The cursor location is available via the `cursor_location` attribute. +#### Moving the cursor + +The cursor location is available via the [`cursor_location`][textual.widgets._text_area.TextArea.cursor_location] property, which represents +the location of the cursor as a tuple `(row_index, column_index)`. These indices are zero-based. Writing a new value to `cursor_location` will immediately update the location of the cursor. ```python @@ -57,11 +77,12 @@ Writing a new value to `cursor_location` will immediately update the location of (0, 4) ``` -`cursor_location` is the easiest way to move the cursor programmatically, but it doesn't -allow us to select text. To select text, we can use the `selection` reactive attribute. +`cursor_location` is a simple way to move the cursor programmatically, but it doesn't let us select text. -Let's select the first two lines of text in a document by adding `text_area.selection = Selection(start=(0, 0), end=(2, 0))` -to our code: +#### Selecting text + +To select text, we can use the `selection` reactive attribute. +Let's select the first two lines of text in a document by adding `text_area.selection = Selection(start=(0, 0), end=(2, 0))` to our code: === "Output" @@ -70,7 +91,7 @@ to our code: === "text_area_selection.py" - ```python + ```python hl_lines="17" --8<-- "docs/examples/widgets/text_area_selection.py" ``` @@ -83,18 +104,41 @@ Note that selections can happen in both directions. That is, `Selection((2, 0), The `end` attribute of the `selection` is always equal to `TextArea.cursor_location`. In other words, the `cursor_location` attribute is simply a convenience for accessing `text_area.selection.end`. +#### More cursor utilities -### Reading content from `TextArea` +There are a number of additional utility methods available for interacting with the cursor. -You can access the text inside the `TextArea` via the [`text`][textual.widgets._text_area.TextArea.text] property. +##### Location information -### Editing content inside `TextArea` +A number of properties exist on `TextArea` which give information about the current cursor location. +These properties begin with `cursor_at_`, and return booleans. +For example, [`cursor_at_start_of_line`][textual.widgets._text_area.TextArea.cursor_at_start_of_line] tells us if the cursor is at a start of line. -The content of the `TextArea` can be updated using the [`replace`][textual.widgets._text_area.TextArea.replace] method. -This method is the programmatic equivalent of selecting some text and then pasting. +We can also check the location the cursor _would_ arrive at if we were to move it. +For example, [`get_cursor_right_location`][textual.widgets._text_area.TextArea.get_cursor_right_location] returns the location +the cursor would move to if it were to move right. +A number of similar methods exist, with names like `get_cursor_*_location`. -All atomic (single-cursor) edits can be represented by a `replace` operation, but for -convenience, some other utility methods are provided, such as [`insert`][textual.widgets._text_area.TextArea.insert], [`delete`][textual.widgets._text_area.TextArea.delete], and [`clear`][textual.widgets._text_area.TextArea.clear]. +##### Cursor movement methods + +The [`move_cursor`][textual.widgets._text_area.TextArea.move_cursor] method allows you to move the cursor to a new location while selecting +text, or move the cursor while keeping it centered on screen. + +```python +# Move the cursor from its current location to row index 4, +# column index 8, while selecting all the text between. +text_area.move_cursor((4, 8), select=True) +``` + +The [`move_cursor_relative`][textual.widgets._text_area.TextArea.move_cursor_relative] method offers a very similar interface, but moves the cursor relative +to its current location. + +##### Common selections + +There are some methods available which make common selections easier: + +- [`select_line`][textual.widgets._text_area.TextArea.select_line] selects a line by index. Bound to ++f6++ by default. +- [`select_all`][textual.widgets._text_area.TextArea.select_all] selects all text. Bound to ++f7++ by default. ### Themes @@ -167,6 +211,14 @@ The `syntax_styles` attribute of `TextAreaTheme` is used for syntax highlighting depends on the `language` currently in use. For more details, see [syntax highlighting](#syntax-highlighting). +If you wish to build on an existing theme, you can obtain a reference to it using the [`TextAreaTheme.get_builtin_theme`][textual.widgets.text_area.TextAreaTheme.get_builtin_theme] classmethod: + +```python +from textual.widgets.text_area import TextAreaTheme + +monokai = TextAreaTheme.get_builtin_theme("monokai") +``` + ##### 2. Registering a theme Our theme can now be registered with the `TextArea` instance. diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index 168ae922ae..14f9c874aa 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -113,9 +113,7 @@ def __post_init__(self) -> None: def get_builtin_theme(cls, theme_name: str) -> "TextAreaTheme" | None: """Get a `TextAreaTheme` by name. - Given a `theme_name` return the corresponding `TextAreaTheme` object. - - Check the available `TextAreaTheme`s by calling `TextAreaTheme.available_themes()`. + Given a `theme_name`, return the corresponding `TextAreaTheme` object. Args: theme_name: The name of the theme. diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 62d4e917f7..03e5fa5ee3 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -188,10 +188,9 @@ class TextArea(ScrollView, can_focus=True): """ theme: Reactive[str | None] = reactive(None, always_update=True, init=False) - """The theme to syntax highlight with. + """The name of the theme to use. - Supply a `SyntaxTheme` object to customise highlighting, or supply a builtin - theme name as a string. + Themes must be registered using `register_theme` before they can be used. Syntax highlighting is only possible when the `language` attribute is set. """ @@ -203,7 +202,7 @@ class TextArea(ScrollView, can_focus=True): The `Selection.end` always refers to the cursor location. - If no text is selected, then `Selection.end == Selection.start`. + If no text is selected, then `Selection.end == Selection.start` is True. The text selected in the document is available via the `TextArea.selected_text` property. """ @@ -622,7 +621,9 @@ def _visible_line_indices(self) -> tuple[int, int]: return self.scroll_offset.y, self.scroll_offset.y + self.size.height def load_text(self, text: str) -> None: - """Load text from a string into the TextArea. + """Load text into the TextArea. + + This will replace the text currently in the TextArea. Args: text: The text to load into the TextArea. @@ -1193,22 +1194,22 @@ def cursor_location(self, location: Location) -> None: self.move_cursor(location, select=not self.selection.is_empty) @property - def cursor_at_first_row(self) -> bool: - """True if and only if the cursor is on the first row.""" + def cursor_at_first_line(self) -> bool: + """True if and only if the cursor is on the first line.""" return self.selection.end[0] == 0 @property - def cursor_at_last_row(self) -> bool: - """True if and only if the cursor is on the last row.""" + def cursor_at_last_line(self) -> bool: + """True if and only if the cursor is on the last line.""" return self.selection.end[0] == self.document.line_count - 1 @property - def cursor_at_start_of_row(self) -> bool: + def cursor_at_start_of_line(self) -> bool: """True if and only if the cursor is at column 0.""" return self.selection.end[1] == 0 @property - def cursor_at_end_of_row(self) -> bool: + def cursor_at_end_of_line(self) -> bool: """True if and only if the cursor is at the end of a row.""" cursor_row, cursor_column = self.selection.end row_length = len(self.document[cursor_row]) @@ -1216,14 +1217,14 @@ def cursor_at_end_of_row(self) -> bool: return cursor_at_end @property - def cursor_at_start_of_document(self) -> bool: + def cursor_at_start_of_text(self) -> bool: """True if and only if the cursor is at location (0, 0)""" return self.selection.end == (0, 0) @property - def cursor_at_end_of_document(self) -> bool: + def cursor_at_end_of_text(self) -> bool: """True if and only if the cursor is at the very end of the document.""" - return self.cursor_at_last_row and self.cursor_at_end_of_row + return self.cursor_at_last_line and self.cursor_at_end_of_line # ------ Cursor movement actions def action_cursor_left(self, select: bool = False) -> None: @@ -1244,7 +1245,7 @@ def get_cursor_left_location(self) -> Location: Returns: The location of the cursor if it moves left. """ - if self.cursor_at_start_of_document: + if self.cursor_at_start_of_text: return 0, 0 cursor_row, cursor_column = self.selection.end length_of_row_above = len(self.document[cursor_row - 1]) @@ -1269,11 +1270,11 @@ def get_cursor_right_location(self) -> Location: Returns: the location the cursor will move to if it moves right. """ - if self.cursor_at_end_of_document: + if self.cursor_at_end_of_text: return self.selection.end cursor_row, cursor_column = self.selection.end - target_row = cursor_row + 1 if self.cursor_at_end_of_row else cursor_row - target_column = 0 if self.cursor_at_end_of_row else cursor_column + 1 + target_row = cursor_row + 1 if self.cursor_at_end_of_line else cursor_row + target_column = 0 if self.cursor_at_end_of_line else cursor_column + 1 return target_row, target_column def action_cursor_down(self, select: bool = False) -> None: @@ -1292,7 +1293,7 @@ def get_cursor_down_location(self) -> Location: The location the cursor will move to if it moves down. """ cursor_row, cursor_column = self.selection.end - if self.cursor_at_last_row: + if self.cursor_at_last_line: return cursor_row, len(self.document[cursor_row]) target_row = min(self.document.line_count - 1, cursor_row + 1) @@ -1318,7 +1319,7 @@ def get_cursor_up_location(self) -> Location: Returns: The location the cursor will move to if it moves up. """ - if self.cursor_at_first_row: + if self.cursor_at_first_line: return 0, 0 cursor_row, cursor_column = self.selection.end target_row = max(0, cursor_row - 1) @@ -1380,7 +1381,7 @@ def action_cursor_word_left(self, select: bool = False) -> None: Args: select: Whether to select while moving the cursor. """ - if self.cursor_at_start_of_document: + if self.cursor_at_start_of_text: return target = self.get_cursor_word_left_location() self.move_cursor(target, select=select) @@ -1406,7 +1407,7 @@ def get_cursor_word_left_location(self) -> Location: def action_cursor_word_right(self, select: bool = False) -> None: """Move the cursor right by a single word, skipping leading whitespace.""" - if self.cursor_at_end_of_document: + if self.cursor_at_end_of_text: return target = self.get_cursor_word_right_location() @@ -1619,7 +1620,7 @@ def action_delete_to_end_of_line(self) -> None: def action_delete_word_left(self) -> None: """Deletes the word to the left of the cursor and updates the cursor location.""" - if self.cursor_at_start_of_document: + if self.cursor_at_start_of_text: return # If there's a non-zero selection, then "delete word left" typically only @@ -1639,7 +1640,7 @@ def action_delete_word_right(self) -> None: as the location we move to when we move the cursor one word to the right. This action does not skip leading whitespace, whereas cursor movement does. """ - if self.cursor_at_end_of_document: + if self.cursor_at_end_of_text: return start, end = self.selection From f7fdcd1ceecef1219a704ea6f365206140cc5222 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 14 Sep 2023 13:22:51 +0100 Subject: [PATCH 355/366] TextArea docs improvements --- docs/widgets/text_area.md | 75 ++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index cce5c4aca3..5a30990403 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -52,6 +52,8 @@ There are a number of ways to retrieve content from the `TextArea`: - The [`TextArea.selected_text`][textual.widgets._text_area.TextArea.selected_text] property returns the text corresponding to the current selection. - The [`TextArea.get_text_range`][textual.widgets._text_area.TextArea.get_text_range] method returns the text between two locations. +In all cases, when multiple lines of text are retrieved, the [document line separator](#line-separators) will be used. + ### Editing content inside `TextArea` The content of the `TextArea` can be updated using the [`replace`][textual.widgets._text_area.TextArea.replace] method. @@ -122,7 +124,7 @@ A number of similar methods exist, with names like `get_cursor_*_location`. ##### Cursor movement methods The [`move_cursor`][textual.widgets._text_area.TextArea.move_cursor] method allows you to move the cursor to a new location while selecting -text, or move the cursor while keeping it centered on screen. +text, or move the cursor and scroll to keep it centered. ```python # Move the cursor from its current location to row index 4, @@ -245,6 +247,38 @@ This immediately updates the appearance of the `TextArea`: ```{.textual path="docs/examples/widgets/text_area_custom_theme.py" columns="42" lines="8"} ``` +### Indentation + +The character(s) inserted when you press tab is controlled by setting the `indent_type` attribute to either `tabs` or `spaces`. + +If `indent_type == "spaces"`, pressing ++tab++ will insert `indent_width` spaces. + +### Line separators + +When content is loaded into `TextArea`, the content is scanned from beginning to end +and the first occurrence of a line separator is recorded. + +This separator will then be used when content is later read from the `TextArea` via +the `text` property. The `TextArea` widget does not support exporting text which +contains mixed line endings. + +Similarly, newline characters pasted into the `TextArea` will be converted. + +You can check the line separator of the current document by inspecting `TextArea.document.newline`: + +```python +>>> text_area = TextArea() +>>> text_area.document.newline +'\n' +``` + +### Line numbers + +The gutter (column on the left containing line numbers) can be toggled by setting +the `show_line_numbers` attribute to `True` or `False`. + +Setting this attribute will immediately repaint the `TextArea` to reflect the new value. + ### Advanced concepts #### Syntax highlighting @@ -387,46 +421,13 @@ The `TextArea` widget defines no component classes. Styling should be done exclusively via [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme]. -## Additional notes - -### Indentation - -The character(s) inserted when you press tab is controlled by setting the `indent_type` attribute to either `tabs` or `spaces`. - -If `indent_type == "spaces"`, pressing ++tab++ will insert `indent_width` spaces. - -### Line separators - -When content is loaded into `TextArea`, the content is scanned from beginning to end -and the first occurrence of a line separator is recorded. - -This separator will then be used when content is later read from the `TextArea` via -the `text` property. The `TextArea` widget does not support exporting text which -contains mixed line endings. - -Similarly, newline characters pasted into the `TextArea` will be converted. - -You can check the line separator of the current document by inspecting `TextArea.document.newline`: - -```python ->>> text_area = TextArea() ->>> text_area.document.newline -'\n' -``` - -### The gutter and line numbers - -The gutter (column on the left containing line numbers) can be toggled by setting -the `show_line_numbers` attribute to `True` or `False`. - -Setting this attribute will immediately repaint the `TextArea` to reflect the new value. - -### The file system - ## See also - [`Input`][textual.widgets.Input] - for single-line text input. - [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme] - for theming the `TextArea`. +- The tree-sitter documentation [website](https://tree-sitter.github.io/tree-sitter/). +- The tree-sitter Python bindings [repository](https://github.com/tree-sitter/py-tree-sitter). +- `py-tree-sitter-languages` [repository](https://github.com/grantjenks/py-tree-sitter-languages) (provides binary wheels for a large variety of tree-sitter languages). --- From 5050fab64808e11d88d737cb329cb8a65485bee3 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 14 Sep 2023 13:24:41 +0100 Subject: [PATCH 356/366] TextArea docs typo fix --- src/textual/document/_document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index fcb1bcfa03..5e8e37d8d0 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -24,7 +24,7 @@ class EditResult: """Contains information about an edit that has occurred.""" end_location: Location - """The new end Location after the selection is complete.""" + """The new end Location after the edit is complete.""" replaced_text: str """The text that was replaced.""" From 467b75c8d3f90cad3c37e31e3581f44f42877737 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 14 Sep 2023 14:31:10 +0100 Subject: [PATCH 357/366] Note about extending TextArea --- docs/examples/widgets/text_area_extended.py | 23 +++++++++++++++++ docs/widgets/text_area.md | 28 ++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 docs/examples/widgets/text_area_extended.py diff --git a/docs/examples/widgets/text_area_extended.py b/docs/examples/widgets/text_area_extended.py new file mode 100644 index 0000000000..8ac237db88 --- /dev/null +++ b/docs/examples/widgets/text_area_extended.py @@ -0,0 +1,23 @@ +from textual import events +from textual.app import App, ComposeResult +from textual.widgets import TextArea + + +class ExtendedTextArea(TextArea): + """A subclass of TextArea with parenthesis-closing functionality.""" + + def _on_key(self, event: events.Key) -> None: + if event.character == "(": + self.insert("()") + self.move_cursor_relative(columns=-1) + event.prevent_default() + + +class TextAreaKeyPressHook(App): + def compose(self) -> ComposeResult: + yield ExtendedTextArea(language="python") + + +app = TextAreaKeyPressHook() +if __name__ == "__main__": + app.run() diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 5a30990403..1934b62169 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -99,7 +99,7 @@ Let's select the first two lines of text in a document by adding `text_area.sele 1. Selects the first two lines of text. -Note that selections can happen in both directions. That is, `Selection((2, 0), (0, 0))` is also valid. +Note that selections can happen in both directions, so `Selection((2, 0), (0, 0))` is also valid. !!! tip @@ -279,6 +279,32 @@ the `show_line_numbers` attribute to `True` or `False`. Setting this attribute will immediately repaint the `TextArea` to reflect the new value. +### Extending `TextArea` + +Sometimes, you may wish to subclass `TextArea` to add some extra functionality. +In this section, we'll briefly explore how we can extend the widget to achieve common goals. + +#### Hooking into key presses + +You may wish to hook into certain key presses to inject some functionality. +This can be done by over-riding `_on_key` and adding the required functionality. + +##### Example - closing parentheses automatically + +Let's extend `TextArea` to add a feature which automatically closes parentheses and moves the cursor to a sensible location. + +```python +--8<-- "docs/examples/widgets/text_area_extended.py" +``` + +This intercepts the key handler when `"("` is pressed, and inserts `"()"` instead. +It then moves the cursor so that it lands between the open and closing parentheses. + +Typing `def hello(` into the `TextArea` results in the bracket automatically being closed: + +```{.textual path="docs/examples/widgets/text_area_extended.py" columns="36" lines="4" press="d,e,f,space,h,e,l,l,o,left_parenthesis"} +``` + ### Advanced concepts #### Syntax highlighting From 03196ca0f2d23744485d346d169960963c1b2281 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 20 Sep 2023 13:55:03 +0100 Subject: [PATCH 358/366] Tab-stop support when spaces used for indent --- src/textual/widgets/_text_area.py | 25 +++++++++++++++++-- tests/text_area/test_edit_via_bindings.py | 29 +++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 03e5fa5ee3..81a4fbf3b9 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -213,7 +213,7 @@ class TextArea(ScrollView, can_focus=True): Changing this value will immediately re-render the `TextArea`.""" indent_width: Reactive[int] = reactive(4) - """The width of tabs or the number of spaces to insert on pressing the `tab` key. + """The width of tabs or the multiple of spaces to align to on pressing the `tab` key. If the document currently open contains tabs that are currently visible on screen, altering this value will immediately change the display width of the visible tabs. @@ -916,7 +916,7 @@ async def _on_key(self, event: events.Key) -> None: """Handle key presses which correspond to document inserts.""" key = event.key insert_values = { - "tab": " " * self.indent_width if self.indent_type == "spaces" else "\t", + "tab": " " * self._find_columns_to_next_tab_stop(), "enter": "\n", } self._restart_blink() @@ -930,6 +930,27 @@ async def _on_key(self, event: events.Key) -> None: start, end = self.selection self.replace(insert, start, end, maintain_selection_offset=False) + def _find_columns_to_next_tab_stop(self) -> int: + """Get the location of the next tab stop after the cursors position on the current line. + + If the cursor is already at a tab stop, this returns the *next* tab stop location. + + Returns: + The number of cells to the next tab stop from the current cursor column. + """ + cursor_row, cursor_column = self.cursor_location + line_text = self.document[cursor_row] + indent_width = self.indent_width + if not line_text: + return indent_width + + width_before_cursor = self.get_column_width(cursor_row, cursor_column) + spaces_to_insert = indent_width - ( + (indent_width + width_before_cursor) % indent_width + ) + + return spaces_to_insert + def get_target_document_location(self, event: MouseEvent) -> Location: """Given a MouseEvent, return the row and column offset of the event in document-space. diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index a8685b4456..aa99a63ad9 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -50,6 +50,35 @@ async def test_single_keypress_enter(): assert text_area.text == "\n" + TEXT +@pytest.mark.parametrize( + "content,cursor_column,cursor_destination", + [ + ("", 0, 4), + ("x", 0, 4), + ("x", 1, 4), + ("xxx", 3, 4), + ("xxxx", 4, 8), + ("xxxxx", 5, 8), + ("xxxxxx", 6, 8), + ("💩", 1, 3), + ("💩💩", 2, 6), + ], +) +async def test_tab_with_spaces_goes_to_tab_stop( + content, cursor_column, cursor_destination +): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.indent_width = 4 + text_area.load_text(content) + text_area.cursor_location = (0, cursor_column) + + await pilot.press("tab") + + assert text_area.cursor_location[1] == cursor_destination + + async def test_delete_left(): app = TextAreaApp() async with app.run_test() as pilot: From aa035c53550f261103af9bf7db2221398c9f1169 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 20 Sep 2023 13:55:48 +0100 Subject: [PATCH 359/366] Docs update --- docs/widgets/text_area.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 1934b62169..67c634de18 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -251,7 +251,7 @@ This immediately updates the appearance of the `TextArea`: The character(s) inserted when you press tab is controlled by setting the `indent_type` attribute to either `tabs` or `spaces`. -If `indent_type == "spaces"`, pressing ++tab++ will insert `indent_width` spaces. +If `indent_type == "spaces"`, pressing ++tab++ will insert up to `indent_width` spaces in order to align with the next tab stop. ### Line separators From d430e90b9205e80520d067b97766ff61c0f4bf82 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 21 Sep 2023 10:16:59 +0100 Subject: [PATCH 360/366] Text area blog post (#3356) * Start blog post * Add demo script to blog post * Continuing the blog post * Yet more writing for TextArea blog post * Working on closing section * Finishing up * Update docs/blog/posts/text-area-learnings.md Co-authored-by: Dave Pearson * Update docs/blog/posts/text-area-learnings.md Co-authored-by: Dave Pearson * Typo fix * Update docs/blog/posts/text-area-learnings.md Co-authored-by: Dave Pearson --------- Co-authored-by: Dave Pearson --- .../cursor_position_updating_via_api.png | Bin 0 -> 185228 bytes .../text-area-learnings/maintain_offset.gif | Bin 0 -> 169412 bytes .../text-area-api-insert.gif | Bin 0 -> 240829 bytes .../text-area-pyinstrument.png | Bin 0 -> 257978 bytes .../text-area-syntax-error.gif | Bin 0 -> 59077 bytes .../text-area-theme-cycle.gif | Bin 0 -> 215394 bytes .../text-area-learnings/text-area-welcome.gif | Bin 0 -> 91212 bytes docs/blog/posts/text-area-learnings.md | 210 ++++++++++++++++++ 8 files changed, 210 insertions(+) create mode 100644 docs/blog/images/text-area-learnings/cursor_position_updating_via_api.png create mode 100644 docs/blog/images/text-area-learnings/maintain_offset.gif create mode 100644 docs/blog/images/text-area-learnings/text-area-api-insert.gif create mode 100644 docs/blog/images/text-area-learnings/text-area-pyinstrument.png create mode 100644 docs/blog/images/text-area-learnings/text-area-syntax-error.gif create mode 100644 docs/blog/images/text-area-learnings/text-area-theme-cycle.gif create mode 100644 docs/blog/images/text-area-learnings/text-area-welcome.gif create mode 100644 docs/blog/posts/text-area-learnings.md diff --git a/docs/blog/images/text-area-learnings/cursor_position_updating_via_api.png b/docs/blog/images/text-area-learnings/cursor_position_updating_via_api.png new file mode 100644 index 0000000000000000000000000000000000000000..c10f78dc845d50f4273a6ea767dbac5de60f838d GIT binary patch literal 185228 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𝔓!?VAzzu^%T~V1j&D;54PnVMTBOQCRvTPLG4oVCB^?q&VHBYoey0v{iCgbMjcd zQhZK}XX#`RPDBGt$R6(LibqZU&TRddI*jaTWq_>_hNA|j}+FC-#5rcH$a8F2{PH)`< zlX(rWF#?~w?5*3dR9dJvxGkEgo^h3)GE}laTDFAiq<7>DdxQ+_{B}Icyqy-@a3-zP zR5~Rg6TJiyza2hpeUd_^S$`#ZN+}ZVe9Q+s(3-=EgG?sKsv(yS-IGfC}+wvg1U8TND(9 z2QQCi7=b}f_k4N@p;^bk-}R^>S{?)kL}Rl{HgC~}SS?=4NlwX=_i7yr!*CE7fu|OE zsGW!Geo_|ly7Hs2sC9L5U>gkr$ruU7)BdQ-kqw1RYw#;(Gs49v2xH?kWR+y$Dt<_kg?>K1(C07tC8fMy&L5kZihhXb zPyfSLaIgF-lWDpfNJrEa1~^ZeA(6!x0O&6gozm8W1b2wKlHTah(#3U4`IfBmZ{!f1 zpM2`-UcAx>eT`wd9on=D>W3<24YdXYoZ1T0s(!s>Kc%l153cGeH(z^+9q_c?VRgHr zeyLu=!+)Z~C=ZP-f6}e8T6Lwdg=a#i*{;(lMom=pT=29N41_#dum+zI3vH*Xdr8yG zzOqG!OpdJS_X(@Be7#xDl5?jx=9+hRCpAjr4U|)QrS87ftAhy}f|Lfv+UrF`rz73FU@Vn)kNO{1{!o zNfoO6+%x=?B$1A!qDjTVQFnT20A)yC@D?ue60#ykg{veY!*+266qAG}E7KAh8cn*e zC=){3>U{8E7^>o?97)TKlwq|)G%2Dfte30AvRy`ksx<#nZf zos<*Yp^?qn;nijK47FCv8PHRM=MX?nJ>jV!2W3uZU6*5*zx}NT%E1GN7U%ld-u&w%B-|8JY?pxb3ShQz7jbmSMG`uU^Yp0_%HZ>P)M>}&{ zLl=yokZqdn6n4-W>2W2XKeA#5+8uNmb&alHU-s<1(mWa6#kUv*B$~kDu%OUP2_c36 z5Ni|1oK28CS%{>C@s-An2oK7e9y+AC`UQF#q7OnWOz6NgohG>?<1EROGeqsvbg@V0 zH+O3ejUWp$GFuTAPU}b-!7S**s0(sd+6pU}!P29bn^&z{r*9DIe06#0KJl67_!`xP zMHi6Hy_V7Xe)x5zXXoy+uy3Drh;x=gHu!`w+m#<>5S-ANI>b3WlB+&#^c-3QJY^Fd z`74p)(2MHeY9T?TWvPaAuntDp%N5!Jfe|6oGC$L~I?okavb*qthGANC;L~YzSmP?D|5@>}JWc!m@&kHHbwj5)n{-9m@+bYgl9xI`V5R;EhcSa+atK)R z8R;`R_G5k3-FTv;s*RGZ5D$4M=V&9+ca3EwfmF)&LY(BuMz?Fx*r;_5t?hbg)Jc~6rnIU^Vjwd4B%d`t1MrzYgU zPL^+c?SzkJn$)!GlpND39Z1P%?r{?Az2b{NBO~j|g>pVPrYraE(cy_#dFnNw9pyM9 z_KM*wJR(CLnTEYuIMB2`S44{$i6phYv98E^u?IFNppZCOrj!ZR;ZtdmCUlimV=vib zR z1?FeZlxfX#4}I+$Wk}~|4@rmt3DK)B+|19)dC7^*Ngy0}!>sA^X@D#23IT$y#T?tzCQUW5lh>2L|tlAwwWEfI{pMIGACJkL)S zI$3laE-2H5O!VgDIbBzimuwVlIv(VY5vJDmS^<1|KSqPbR~Zbg9xRVP@wnGW_}UHU z5Sb{dD1eh!q9Fr%RDh^GKUUWC;-F|O{B}R!V#D!iHNR1SyTTRp|gyE)4&06&y{s27Lxg^*u;^dtjrMMRr|wnH?10haA#0j8E%Dhu!55)Q;#B z(~TQ9l%2bF>*KUK>{7bAZt3ds*3~9ZhRuOMgnEgaC==R=ZLdVnDgK!M(8B+!a`fn7*BjcrT7YL=kY?1S@j^#P)*ujq>hE-@ ztOyix_AYi-$~SXSgxK24_m*vMd}CRw`TC4TLeCsIQl8Nlna7VD(o+I$^kfcA#ZUpT zi8CmbaNvE93#|5PCg-Eqd{{Ns|#a=!=SPA=K@o~Wr4t7#G=`B_FPx@XkGANJz0HlwVtpVHLR zv+2@%m()NAE^0M8_6pL$`cqswPMgudYB_w)iyzgrYCvg>X1EfLs@}ipO>dABS)&et z5pOy^_))ejZnJN&^3e;wv?=~@WV}zuvPYlyS?RI=s-g0UKYF12;2nF*1zXoh2jOBj z*@`w5Q!TXx9IKw5W3wqa(OtVQGoMp>2#|JEZ8d7_e-sUqQCTB+EpwvIAP-yUKWzvY z=-`Mlpn=O!>Eu}>!$en+@aPR3bqpA`V^}7ovC9Dal1HuaXb;|kbGy@3uFWDD$bu^I z*@DHZ70X27u7DF^h`ax(r!^{CDo^fzTAv&LJ%139FQr^~(M4sOyyx&*P483)oR5W5 zq$i;valqJ|Diz1Wjval*bMNCvkGXLTsMlbAy-w4@wHoH)h*qlt(4#WF8+i0bO}zYydH4mp1xg8x~XX`aT_CY1(kW z8lFmy5UPZ*T*h7UuKFR;jFJKQix~U)~MyqxRqUY6V49vRlg@%4L}oF>7bg7RwQ9u z29hbSna-$$Cte@@=*Qf6am-9}_~WxtT>I4UX#^Zy(yxZUMrZUh`k*7^1FUuqy5~xL zq0w+83-z0xALm4zIvX&GJ1=0E?orCck!VQ&6X*k4N>4 zRI7bbzqC77+ymDhRpN;$ZL~bAW0bUz!}O5yhfduu-&e8@=z98w~!xrrpfMZRg)G<%S04+bi9wAji)@WCG| zgq|9YR!m)D2rYqB5n9RC1wp*AOK=ttW7a>M$6=lN`3t}JZ%php|Lj%ejcWEHyG3hh&i9Sd3J|mh#picg5)c>(L6_;JM+B;b6Dk}=3Ve#`uom@+^9dq` z=QLOvvC!PuPU=f27hHT%S*0oIDm7SlkD|vkl3vS|Rh3Xlu7E_?1czMFSTz;Z;|R7# z*oiR2=TAR))2) zk9PpJZW$GC)-$4~Vbn9Gsq)zPl;31$lVCg+pDF+F#F28}!O2p-@VGX-tSfu2(FZcN zjh0b8DO#sp#B227k3NkohosYhoZ`^XsNGEp%U+ZNb|nxGjk(5sIu5d8iU@RrQE?33`~;ol_)R7e@l6Wc2JLnV zY0*(iLZ4Lo>%Z}#a_1d)cqHi#5eK6`%V%-<5e*a5MfKZFHB@OtcDnTxjCt!faq2zt zGm_yCl5R-KGaOd}I_U59Z+N}!Nqq&6KQsy2)Y*Vbo3wQY-|(~x5LEZZPtT}4yHz(d zDpBW&1I;pkzu|=!0eulm9kZM&p~+7W@o&-sDSj&r=K*5@VwBSu3-ZWCp5WoYt&?>cov1lYy8aN}5Rcc&}LKfx3rgSDd z{FW7faOmmyipZ!I+>y&p4dMAOF>ol1Hu(4=4Gm{>=+L2Z@aXY!>_l(*+C#_6BS*%| z(_hh?Sf4*tsn@F%&^;XOG9lf*DoX@SwUSwR8>Ti(a45;z+q^ z=i0JYL-9@P^&~~pRn`iqbWCqqo1ntaYJba^D%SDtGiBY*OUm1yBa)l+ltug*)$|C5 zcfveU(l!gO*l$!*;C_8ual_~a7cSG9(^Io@ZrjT3(sAJpFOieh?qWT0Vxe+Dw5Bx5 zk`GtVg9kZTHV>Yeo-b$Q$T_)jo_#e^iK)>RtqrVS+gmQ$zDY9Z@wgh%1*3iC1$!?x ze=3X{iKux?0%U8*vUA&!{qw$4AE@|EbxABIBy7^khM4!~T%oUy47(C)+fj@TuGT%} zv6qLP< znzm)vEp^>{4Z(A+AQY!PG3+r-(#6-j;fIlt&w~cw!eKDln>wTI5~`DYz*eg(K=b2Z z`SYfxSoWIc_FUt(i{ju|7s=!}`N-DXi?>IBHQ%y;2VZ3Kf9vEll51$_t+-G9p(r*@ zKy5m3o(3}2r750O<;6z?Y;+X8EJ_N-LgE0(#Jgc2=G++21~v#xluzT7B32xsMdQ?| z@pARmSC^eTca|esPr!kA2#xXehrw_tgL>Rck8||MQNyfbx~Gq=GGCqb^e~2l;LwYC zok6_Q;8_SB4OZGH+RF$-BfZh}qvedIQIlr}%5$z-U+%tZOPPP49-qo7uxW2m-{hLn zUNN`G}ej-bBq_-p$6>Abnd}tluXEc-?V-*y`haU5Xk&=Y2hYM zqgSIv8Pc@D3Bq&^lz;OuKynaVW1wAevBkJPu)0niDYFGW&YBMtvOyaWc<=~{yG%_3 zT0z@cH9u_sHl4Bw-8O_LEv(khctp{V4Qv_~g@?n*kQIXac443hvKmK@gPpAyn^6?&J(!L> z))80~{nP*Tr&=?4sBGH2+3N*|wP4C4*cZO|MP<*PJzlQ@juD0(9)*d}a-i5T_@x;_ z7C%i;K{JJ<;5mVG+vxhTxO%?q*)^iaq>_pK2-1_Oi}oQ)E{uagkugR{Ss`QOrUhB? z(~^(g(UL}bJ)*m-U-Z{1nMM-ErLx)0OGef}q;1$oj)*lVMu5D5Z`y)+?6JqnbM*Ok z)i#7T4~gK{nXpwBB%?K^s_C$?C;A%&2L>=}YeJ?{A_sOSkVmI7s3G=hIpU2Y8sRD5 zGLR`-E$RN|#f!=x|H+@12Oi&7USon#t0EzqK+`7B4c>9(z8w-S>jt>Q^S%QccFXj{ z>*Ny?GUZZba$XRuc~vf@F*0$ZY{8;KhUCM&siFk&HoVff=PzY$=NqB^tV}n&WH2r? zQs+t!qfsYAqnNF<u3#JjPZh{Agq?+u) z3OY)xfjrWZx8vgBQ2Ay&67^fNxwo#mA_Hq{1hSEsT4yOC9nQ65&}E=A ztr^$Q2+#keDfST!@Sy7{VKah)r824PJUsvaXJY}$2oe@FiIE(F@isa;S+!ZTdREeD zaD20H?AY=0^FROpmb>n{tK9R`_joh}J)F$`{rk&TzxuWE>EHcSx#iZEmfLRAv{Iw4 zaqTgIf7@}|7$7RNm3A*BSy#Rs33CZs?bYD39>O{gyMJ*w z=#F#a6$IAEf?qfWS1lVZcG6=b49P$ZIP6H{iJ%OaVIQTFkJI7JeZG0fmJUv)2&WkZ z177Nc6U3HGt-Z*9mZw*&*HJAHLO9|NPkLPJ6eGsq8S6Y$Y#f@LrsZJWijfqZr4t8=mV=i>PhlyXu`*Kp&Y|LN?E%kCdYkUvb41T8FrzoO<1B%l%*eN_qF&-dW!Ho_Cj*zVszp13TlVeyik6 z%(2lRq{v^(Z0EyQLiFPG>a&`{$*}rQgGO_72hIhiI9D9VLk~V!{_qb!=Ml3XS!;T> zXYZbJ#~rV*UNn9W=PNU4ps$NULdDdFX;`Q60vftge8CGG{&+8IPz&5)LBT23#wE@J zf(bo_KCu6QMx&dwBYACX@t}M}0~n_4>^-7a8ju5oHZ(I-wM8F!Ysn+K0J!H#Bz@v~ zt()aY&N?YSpi@Gb5ieJ%uap&jp=;>LJ+dS`_Y&9lSc8UE8CVZ;mgCzZBs(}y1mK&& zX(+fNo%{x_f?M924qnQiafE4bl0gf;mDGWQw}NRTK$WkZMqEgNN51X@v*MCQzG;Q* z;PB@uDk#dEQ-?aCHO+M++Ow$6NtGb{KZs2LvL={jW@ftXNg_{wPOb8pgyP0fM#xQS zr2#N@^2Ij+Lu%ZLW^`D9z$4oXbthn|oqdDOqE^ZKP7aE}38Ur+$QXVEI9#`16T*{a zV*;PbizBfot*B7O)a27Z)2InEpvT5+Q?R0fK?kN*Oa)J#I9U!JJm{}F^Mz-7PE}|| zVl+0Ue~ix9^!5X<|AF#?o1b4kAqV&O-}t-oFYbA-ohXm{8F6G~lReH2N~EDP66sqd zhouHNrA2B!vdVf9c52WNP&YW&K|Shz*-Kxh{VKb>Z)NJtWWXj9A$Iu2ND5?V5^(q~ z9O+9+C2bt?;p+-0Ir_Bif_*`DE=eCOkc`b$G-Vsst3LO+&y|y7CmiQB3ikY92evve zQsbMCRFt27-_MrKTeM!M{Y6<(+z73-=);S+f-8zD0&n1JEbYyfS&qx)*9g!RCd%8$ z72EWbkmYS&^&Z_wr=m708X4+!Oki=UJS2;5Lkl}N%}Bwq-U9=#2A+w}78j4;v88~{Olyyw z{M4|X2=j6vMfusHfywuMxzY(#zsy$af@e@DMep7q7{@4HIr`u2d{fWyGfu4wyd@GpEUBfR7vp0oOUfB)~< zhqaYMvnlBWE|tmxJ(~)-Tgjdy@AaX9qZcx*Gbup;R?20l=o5_iY|zJO$-@$%_8XdIMn5#hm~f|l;^~=QV$&;*n6yP z(T$RIx~t9QJ3>7ZF+DyMiJ>qkzbfTm^+#e-3iZERGk&Nz;@!5Ovk6b zO)nhm_FYeIHkm(hB+oG*%B6*9YU#=Qurwf_(G`Gi`spT^_d!}PYwKPp&KgL~v|z%3 zG8)dn!)1g8SRpP+Xq2pT)l7;_pf(g^6i^zv=bk&*F1_Ru&$i5$G8=ZLT_QM2vhRvc z2tMmtTrz&|_kB;hP@ypyI;TZZ-2Gz+O3vs;&#m|>;BVamI3FS|@lY)GvwI z-{4S})-ODC0w?LvX>GsLDj?PYiqVkM@ytii_yis)E8R*9CPeV`7y(QDS>Miw{E9Q0 z4%PaERj~m^B5TPtOG*cWVT2Whe#MYW6$UIqoB$reFVd6PpU_552BP`0u5Qs^eDTFz zTtBWy2&g0ive?^nRQNyiLqFJl{^$QqyZ`?C{ct3gtK|TrT9@c!<0X8p9y~pD>bMr} zH?&8d)J7$yw%zEWs^2)0%QW=&1Ead^Lf=iAde=+zaRY;5T}#{kk%`ia@?-rHQpek@ z!G|a4U9mI_DmJLjlVfJP!fRLDco5h-;90L?)`uUncEzj;9~V_M=I|?%dnI-m$cz8# z6*Sr~t68S^VU<=AZ;NGk;Zh$tYH(r~cy@zVouNY>aY589V`#+Ip|43eji&a*5YPUk z&f?qfGnsy>uKN`L4jRH}Bj9D)F(3Zmjp@qsdoDeA4EXRNU9k{J+a8nk0j<*DP5hVj zEfn+dK*eC81K)7cWITNQ$^GpGS8Npwc`A2lberu@JLoJO8HRkQ6{F{*OHbP;lr4u2 zLk}P3f))z4pLF*}$ngz--9M3K8R5U+1N1RN(}ACLbq|W2hDptenu?Vqei`8*;QE=_ zSA^D`s%V7Gm8P7*g*{O9j^?T|3l$GK*Hu?v*}m#;d{uksp@)r!5_ABb+hU;DpnX9+ zR*}Ihv|dcsZtb7>-~W00t>1o^w9|@;=ps8Y_K-#w?tHD)^IgXeA86NKeMx)ZaXmlf zS%$H|?1zZ}>T+sbyYIgH^i18o?W2GCk@il_=zinZe?zmpV_x3@hbBRVL0>Z#oLtl3 zR0MI-(G7pb>A;u`v16aJB)GsP+@8*$%)`i!J+{lz-GGHJBKyDthkU`pES)DE2%gFF zb_@=7->+PueQ4g=QMTYmHXvdcdtP`QO7K_*T%ng5RvVBE@yvRPl(D{kOiAl!tfl20 zMy#upZ@{V$pHyq%7at}t*bSX+$rr(jkEYml*!2*N6q!l#kxV`Ag1zLh> z6&Jlhi+XhDP|lrtZhSGOpsiZ7={%zqOuUP!X{_|3f-uoP;uZwXaCQ-#_CVx^Pj#k^ zagc$>+4rq)eM`Ig>Z`oKPG=!dv0RRDxx?EDJs7c)C^buYeJoWoh+DNG^`6h%rH81W z_F4`65~9ET*m>3~D)f%`HGGAH$9FJ(aIj1+{mUjE=lJ=b|JUsg-uvG6#FJ07E3eS* z@;82s*P9qL@oqWL7Io3ppvZ(*(!}url}4Har*F{A4L>m`VefL|1_voDx?ZZiYLD-F z+_>>0bm)X%pp|grm)ESS<&+NmVxu~d4xb@~Z)hiAcFkG=Cns+l)>;C5*$Uy9v<~#4 zp1ns+_*uilH|B>H<_+TvWjxd4HyQJr70~X`dp~ru23kf8*O6@qbt?!_mp1 zM+>rj1yTc0@bE{{#M9+d7Z7^n5R4lc1vm#zCnvxgo3H~Pa}wYC%t3eJt$JTOH)70R z2iI8y&41bnw&PWLxJ!k%P+3&oU)X{b`afn2OD#V^?H&*#!WZ zle~_IOyJOmAJ2H&4@iWDKk=fi>WWw+7(^U$FlTN$4R_jv?a>l*A+V7)KIRvU9E-k; z{xtMdR8)nQOPRpCuEkR$F}u@w9S7y;q7#P7%0MnNcns#Xul-6rT>YrlZS-Xv?UYZ5 zk>ukLW_$2Cp+`5?s3S7V@Ni|dOZQyjL33C$o?rZ>Uubu{`VP&0cY>u^~3|I`l_G_mhqVi7bwIbW}u6iCwW}&^F zV%Ke5&3-cY@TiXmZwMqCtvK*|X$V*;mHP~<*m+?{Wv`7$x^6V&GMjp~r%Yph-Bx&7 zvChwVU_&T;0GfH~&-IC3Ix;IA(7$Z5XwH=~`>HG?4{q>@6O5~JxKKYrk{)ypAT#gE zcaYIK5AZraa3Tl(k>fTE&VXxO`d|g_R2k0HPv{5G#M85B$mpPIi%;y{->$lBv+7<; z1J)ZQXC1uf3jI2>$uhxM^oIm5P;Q(heN8slXth-Mifhf;a0KisDD- z#ugs;;T`krVPO!IFt~OnR>y<~Z+GC18=qP_q~{+wtI^&fKiui`=74>oGfT6r4g(%E zQ|LI(D~-eX*eibFMgy0Jx%@+q&|0nrGCti+g&h?x?|Is{_c=eG$#Zl(*t}Wq)yCFw z(3)f`VSdslXYcSw76+Gvbc*A8^k_VpU}O1e;D+t!=%U{A><0k`lS6_3Yw!b7^2+f9z_zlxP70)JG5}@{CCQN$4A~{$oMv5A$~fYsAsKEQbWp#cE#V8j zO{LGO1RQ4ruqL3u7*_^cs=~=LRYMS%%%pg*nT{Bo`Ak~|1P(1}UK#VWF88c{*+7x? zp`38xMHjXw^qSX^7J(U|fd@AOC10Y(ONX`F{lX3Ukfk07E_=|UrqDDDp11R(FVX;7 z4$93>Y^yf`d)}sg2pnfG(g^(lJj^1a+|W9S2-yRhEbi9S7T4 z$EuTELHP_Bwrb7Y&;9yZIgajh3sIan0u93;9WYy?H-xU%UMzNT^A0NpE^rvEIG6>X z@N~9;wug<{vcOgXhpst13B(+B0HBa5hJTgKl}4Tc2YpuPvTmb3rF2ve|6a6_w{K#2 z(ZSn60A4D{>s+{;#o6#c5d0H>kPE~kzMP9*z>05S7yT^9bmBz?u}ID(>k)06*nYuw z%fk2O-|fLqi01Ak13wRyzwULf^Cjq3%|`h+z<0duJKJTtJdU4{JjpFJtbg<#%faK| z{oK^YA>G|Y9{zd05Z+UHj26IW^r-;PjeaUW+NR)$*5Z0TyhneRQ_H3io;rurbln!x z!K+3;cNoet8#29UEx*%-4xZspodh0xh$Xc9s#_2qU17`e2@mA%i;h!}`Pcd@J9k&r z0CM$5TVZR{{`WL(1RX8b$K2aDN)|ptG zHw-@d!;I)X$-x1l<<1|kAK{Ye#3_U^o2d0Q_<@5C$j3G|U&a^lXz5yHy2k4k4A3xKc z;2)XbxmZ5hDUNcf353yu)es)Bg&L^}8LW7f1aEcZagj-&;BhPhlP>YlO0GLht$)dCK`f~l z8wDnM-K<_&bJbN>Dp?ianN6=f1o{^xwBz`RIEV4bt6fY1`Wyn94*ua$26j^DX4$ovj(qg4p{MGt z>JDwnL!uEH-$Bd($KC6qES|oh#~{Vu6qFotb4JS9S*U}e{B?zE(OzGX)gm!3<0G8o z*e)mV;#KQrtm!Z_^Wwazi=HHJD?O>Vl|8xp$##VXtlVvsXv?s{I5PqLb!^mSDQ_iR zzhSL9X5Cv+iG7Le1gDip2kfCEb5`^K((Oy-mECZGPp=e3z{{Sw10f+&C@Vh;rkU8f zzO?}kuvyf{&~97tE}fVKflEiN*({0BLS-GWO-IrxkG;V|=K~rvH>=LpcotAJ;DrX< zF$69Q+nlo4Y2E@RKb9}UQdZctO$D72W?6yEu}$n*@NxuWq#e`czeWGJQ*<7#-z6Ou z=m(jR<#m`|54ns&KkVW!Z39Sr&srhfl(|Fct~yn_qn>@JK3G%TuUYs3)~~j0^p=w8 z0rt)vo^A{!(Ag1&K1e|Z?QiyON9}ah0X*YYnQ8l=3uF?Zgna^F0t!Jsh<#;K@D|Zh zhBqL`7v_hIyqd{RXdqR=GQd%ijia7op=l2!C&PN87-n|ip=zPQ+EUT6;p><-TjKm` zd^M=$QmafVLGUE+bsga|VC7ZCDf8P*RdG-2(HTC{xPAL}--)DBjYH!abSypLUpw!) z=3l)=FYMD|yG9DY)+>!D4OH%s{Q61>x@PbsZQaq8Mk&s9AtIr6>gU@g#6ULZF9)TIxE;J+^ns!6ig5XY^w};+1|=e9yEe zo;sk1o%I@Qy#%uM5=Lh@S52qB!`nOuIWF(EQsx#TApOpBK$TUHZlWU_KapYPUA&iT zPKcCuS;eDx0Sk@gA-BU~dvVjR*9R(ImIY1PEe!|SRUXW$FI%QXmRt$3PKGm!8ij@t zcNy^=W$apKEoxrdnH4RA7Jse0WY9QR$HIvi=+~mk5YB#dq|bc%uJ&r}kpiE}1fPJ+ z^94E~{7&h`d2nPyw_o)N_f9i8PI>sT=L!8YgF{Yac`()12pI;WS{o<@O8ii9@QsYQ zLeaCsrQ2rR9pr->Y{V@5?r>p+*7`{nPv)}&9e)?D-91$d_@OCf5LNtukNP+blGLG7 zt=CPG=MFs4ww~2mpB|$2jS)JI>$7v~8S7)#ToQA+!d>85m-E$xDW23gl1o>E=;3(RCtm!0+VX2q|cFLrU+7v%d0HmL8cI>S1ENhj5ufWdNL=Pd(N8Q&{fe8Y6L8hti(qEMY_P-vDhrDX&-+!7_e zMz`x3ocz3@Hb=6ValIDKpVg~X9@Mu6zy2G(-p?v#wt?dbt}yi1Kk0(8B|v+z#N&+C zf6nMJ6Ne(Hw0_G`XWN&)^riNp4}Q4q+_}>*9-hW|3}7twZ`z`dQtAy7Y~bWUY8-0$ z3p$9Q^415qbXjy^<*=1k6nnS2*85iKZf5y&#?JZz9H=N3^n>FDr;1Z8SRN*YQed|g z-*pijJ{jngo$%mcJ=GKT=?Mev+to~TojOeoRydP}WM&YK0Uu-D1Ve+R;KCUrrKz)# zVxjE~enWS3oI>|!c%&@xh?jZ*W;@~AL-5BqJcA>mDM@0)O~FVL8>-EVR`MbdJh(j{ z58xv&_>*UE+^(k&w^!VFl}=hbZ6TiU8t@sY+_}5HDJwi=(st_P4&Ss$mm5%aqAjVk z8;TOFH?rdsbf6GDurh5^P_OO6OJ$j~fMrrUL>iPxD2?ZAWjwZseb=)P(HVX5t>(Yl z)?kDprof>yb;l7x2%Hy=zj`CMl@e|$ic7)_Sg^3M#jz_Pry#JGkzx?!OSLb%{pIe& ztRDe`H~fC@KfJrW@$25`yM279G8J$R@2+$4W&;`@oX{OejaAPb(yNno`2$Vpu&GtW#YO>={^p(L~RKVo7LUAK99tb@cN65_9 zu!}q&;oN#5Gn+mD3Ku#C8Ez3UMdr*qYg*-SwYdl%f+v6}Qw$z{gkM7RXI6PauiQDR zJy`43ZPM9W12H?^nUe~C*1RZ-4qJN%Fi&9(h%-)aq>xJf#BcCN^a+F5h^DX%FYreH znwR(h!-lbJ8J9dEyhdDL&~v2yl3otr%`)Lt7!yyj-LAkuvK@j`hf`2;)FJTjClB@- zEx13mpSO)PolNwB*psB^_M5Z^kuDqBz)2G<3xzqrgSi6HbkZeleHj?a{@4M%d2}2_ zsvGg4f)qTMm|1Tx@S(Wt3Qh*q)Gx4v=m!hWqSWuXI~npfl5W2y9{a z!#{Yh*Lz;_l3P3w`WCTxFNc6wlq`eUi$z+`$gS2sBYkazmw$Cl7RVV8uD|{|e|aUc zK_KmCxfno&!{sHf46eq7)>u~hr(*3`l;N;#U6k_Wbnz9hwVGk^(!j%fR6uWrIIGL6 zgj6DVF7Mea>Sp8-vlrt1Q?S>Z5ppkhfCQrjO+{XLI^@=`6w0U*+)BD=Go3^Q8 z3D=JznR(GbFFfB1f(9yW&AG(9vQane778}F;;YtLK~@ve0q<>6D`tQ|2h@a1P7 z)%l}8{G)dN=Re=R?K{5R>pg>b@hN@WIF!Ub+lVZsX~gx$0WH$0qtzF3(wKyxP);bv zu@RG*A+N__4KBKX$wR+L^PVX2;*3FKK{iJk4l=D5x^-ouXXOhY7?;(&vwp8GccBMX zTA>-Qq5z_Y&RLli9aA@0^0Peh;4B_Ek% zJeCEHsAYJS!Cf+CYvh7UDEC$Ej(jK!uS(-);fV)tvVCCJ-nR3~3)Mds-_8IIG9m{a zkU|Qf3<5tS}J=sx89**Cx z_kcb6rH4&B%J-T?m&f$>E#LZ9ec@W`OZF+}m#Qr~FIJRZuvc2J3{)kzYN>uftrYAv z)Otbxt6p(ayJ-8xp7~|)jjd&$*_xfn5$S1rNdOnFp^Xemg*t`6Z7T{WK!+7wt%4~N ze;5Ex$4?x1c#=;VoTQD1zN3In3~c0=bYgsombNYq?6k%aeZb^hA6?GGW02L8MYAM3 z`a#Zv_s{8ZoIM9m>UA=FfTZ5)0ey51|Kvk&8ThK}5t__nq2mC5qzON4hBi3RPibf^ zz`!m-3Il}BLHYXcbk#S)zhusQ>RPH(qLY$&7P)j3VA2)cw9P5koJCe=2}rGnkWt3O z9Rbj}p*qTH?U-ifG!0nlWHAOFD9-kQ?|$14v>*TPf6On;qk}OZ9XxQr-!o*U^QLcl zQ+v7Aw2o^Z6?f7ySe4k z-rCM^h8sP)IfBm#qr7wS9S0ppz82cc%%t-nJ=sHM38z4`E8&spe9IbUKuVQbRVK)! zpW)I2x{0swA#uLAP>d&eyoKSL+GG40eXff^`4sPx*Wi_JB^s|g3Hn-aEM8+JP9B*W0oU*~dyCAXPvwxC z1N4(D=?L!}G(^%Rb*6Eu>&E4&E1W#E;=yb+yS!abf=x0+Y@QtkEoIbW0up6~c2Vzd zw_Lf=j|!Qm>IiME53?wX>~zTV?rW8c=fJM#;VK%)#=;2ganVK>B+d2%U8apKISIlh zJc3I;f%Z0r;Eu4+1qQ`Acr(kOofDr!BaF0nkdad-hrZ_KdYknd^)_?v*lV5UpAv z76mh8L?AJ|xFg2<$gjQbI^|TZf;f}P<(*mwwUxl%2h=&8@>z$HX~LrJzmZZ5nE^2) zIePe*A3HgHM(>njL0Q^ZRyQ^Y z?g72$iuF2lon+uM@gr|v3D;>4B;8Li21gOPpoBlQ*5M)`Rd%AAf*~EwnVOTwQQbEG z&|MF;7r$Vue9fC0YEKxWySGx5HKvbF=v|klCBuc|Pl;Qw0eRR!z$!OR(jDdk|4<^l zuJa79)QC@-Mor9(nz1ap5+_)Po*8jdP7FM>BRqUUH{t{q{N!zCdS;eSWFgh`5e^5fsH@0}iAnPe8uTnNGa^yTZh)vaugBO;uunHAW)tZefDF7Bj zsoFa^`U-&Y$0dWea&FL|uu^Mg-X|dKROAr6IDL`poGTmWeDE18a%qU}(PPFOQe9|x z;fLK%@7BBH^~rh$9ocI8V8VeNkoR=q0O$dJ;Al{`9U9mfgvyI(QvqGgCv=KgBU_>8 zEBEP@yZ7up(!Tx`SGttDmka{TMool_&IvxuKv5@8)PrRH00+EEtM&|LP;o6<26Y-< z07}nE+rkkcI0S67eLW8>Wy^7V8#uUAfKSh)JoppN|3~7lgFotu9Mg2!RfZ7R78@12 z9SN71htQwgtCO|XE-&`?2bl-s3);sVfW@K(zF>y;kPau!U(E~KMZ^dWZRLALB9fWyoup){=AtXVHr;RR}32M!%=FWt1h zZP$Con9V{%gYo_xP~mUg%MyM$F_%8%7{F?lGnpuhHTO{&4P24Klvv1vnSDRKpre<~ z=)*yX?~_I^mLCEqU%DFooS(5QG$Yb-dCJPx*j&)Sg0mbi^B?o6q3BCH<%p%U|6NZV zYCA96);6qN?d6M;fUUnaDklP^_&zQal<#fXut%NBU!TCtPKyIGySJ{KvRxQ8TtL%!M#~` z8V?=P!oAjk)~~7kRFPATtBtGhapw^oG4vb`i6e9JLeUDbj8}Q|0Tun3Bl1ccVJ_gH z1G7+{HNeBqiV5+%9a^9&=gim#$qqKX=|m4NW=S4&sEmX`f8;QmU8(qxE|DTr2i7`Y=ac}A7Gs>HIbbRt|XKHJV7X)Z& z7t{$b+F-@?agx*ePzi=u?~d=h+C{uY%;bVCMNd5Wn8p!KMm4;<%^^cP&~1gC*8vL( zsxZNdcA+9QSXqVHff&MowMp-8I-om#9wirWLs<@vlih?l%V?Ayon`SQBx;mg8`>RU zoffCKWtd(^qf4$$TetY~bCW)Vx=#BW%GqpAWk)x!O_t7}o?B30XA^@j3w4lmfL4jRoK%q+3DDt%q^v<@r+p-cSdWfx9i$s_ zD{ZbO&$a^^0i8I0CFYOIy5m|a{Om{X*M6|A_Q$k_l{$@l$v}H}Mb+Zc+lLiRTDsIU z%=VNm)?P__r|VMqCIw$v-zAy^Ai)Yuegl`lI?q0>|J{A}XJ|4i@13YOaBwD{l2NdK z!n;5VaOyZAzDhoEcq9hCET_6}Q6Mhm+#Mus|GN%^sW@Tvs&#Gc>J9DK=@m^&&h3^L zUhZ}$s)`)U7_jeaukL7B-m?4zx~Pkv2b3VrxV z*g%b?$Y)!J9;s8p#YQPV0fVo$H2^Ika9CEvltM%h1Z8wN(oCTi96ZXd&=+SO*0X;% z-E@gCb)RdW%`Yw%MId~k_JK_b< z>yOnjAKBNAo?4^*Ok4fZ2ESYAw1zC*MrKB`3P*5Bs%;OR-U8E5?hc{!>}1A=l3z}x zGviR<$W{jpg4SLWlrPYTy5T4~=?_$R8#ZogZO8hyMNbq=XS2@7^;w)zrBC^-kfD|X-{=Mu+744j z*~(kzB#j0iFlODrDf9eW>0S=5L)Q&IZ`5bpUV7_G@p>p-N8{jZ`67&Ot5U+Mbf{eE zk#y+nGs#Ro<;!u(7kyTCU>8ALTn7uZ@)bik^gODyfB(Mr@y|Zd9(wYi9`^rayYYsd zdPUOWE40Clyu{&&{OBAQHdD9URAIw5g9l+5Z{!s|6OW!qeT5!80)1ojJ8hs&ZnBwj zbQX>jndPFZ{w#N}DbEQrTjS%gY~i6FfR{EpUkh0LU*~7BvO@v%#L3edsBUim*Z=u* z?e%YXL))@-tKO`6SO%dw4%OLSDSj~$!_Men(5#*DI1`Tksb3{y92H;6gQL)mMKP^^cx}={h!<_$*tY9gu665FrUg-EV{28mR%OoXs%N!cGpy;P8pJ7${%R0vg&w<9 zNqq?O`VtRKvqo`3d&{J_p5Z&J0h4L}JiS(d?Q=ucfLZ$CH~b>6F`(-10kaGm46lc& zhEx86FYnu8cB_`+#>4huf`Q#bP_?x&xu_!*lS$10chjn=mK%{3?t_|Zg@@j;*-h=Y`n`24`f6; zV%mm;PcU#CgjjE7DH^EQG)nMut?rxUeFiSR5XE?*yM*RfJYYOHBT7*?r!n5>6HUt5 zGqhdL#(mA#d~N&1U;Kskj(5C6mtyNI0KPOf9x6VrgFRBbRR5?tFte6bUT71J(tLZ( zW@9mi>5m4=f7~}!Q>4G!h@zDkz$)S^OlOCwKY3s`Nqi=t`ZkL{DPw5WKMqL`N zkrNrH{M8sCStq?Yf!!F#)d?7Qj~+SE_8&aa9(?3!eaP~())}{S3zWQsw zLGL+Qqd{_Y+q81inFl(PK@#i4uM4%*LB0 zPMtlWx2)DzxcPvp7c(_TKm4Wret-S;OSZI!9)7spdd(KS5knuH)#afV@Fj#wkesB5OGW2EVQYbfueE>5slmpC{(AoH|KcjN4&O7@nnx z|1`sjF^MBPc{^Fc;&k*c75POR4dViTlIkm8`&F8OtZD!5*WT&Rt$X$)1UpwoFoOXX zI3HwS5F#)OptI7+OAhKX&~eZ)&-gh%-aJT}J7cK~5fpz>(3u##c*hfI-Uh)X(2=93 z^t##Yej6!w#(3X5j}xuc9hog!ps!C1NjGLQ7-T2bBH}5HQfJQ8<1B~u2GkRJAobKS zy{cG)G=m0jHGNLUy$|ivn<53{jSx@hd4T=IQ)$8k*46f%d6Y@M-J%g z=POS7Q*OQ-lFs;^s}RD-1I@Fa%4Npj2i&q}0s$3|*kZVB1uMFYxMO-OPdf2bH|W`Y z=d!LZd@Msh41B>+Vv-FEjiEz7zQffyGdbR~wPF1VeJX61U+aQS%m|NbW(6${WdLIU-Mo3T-x9G& z+YUDP%?fM%zWN&AR<*5qWMbu}vWJ0DR!Ami2`+^+Gt$6wQUlIO4P?CQe%CX{+yC_8 z&$M^`kKfbQZ`7c!+1v4B=*GrJ4NSfXfUPwhUatXmt=^Q%IKD>oJW|8}h2NM#ty{Vk-E3fg1A2VdiIk^xs`$8_W1py4Jc4Fbq7xSfGWC|b2@G0yW#~KMfhdww7Kznka z9Q~v-q2$i7f&C|C!SmqU%PfCEmVDPcng>MTMMfu?B4`sv8NkLe@*@v?wpw-k?Ed5J zuE+PaTXwB$7ro?KJ(5!!Vnae-sWCrr0t7w;&s5Mw$aDg_qb_J;xc(cCcn*x%_+4q>qU9{Qs9sZ+;&dmNgR zI=Jli8V`obJ_h&a4jgRPz3|$0(~U3IW$b!A4zjbi&>hSAUh0|&VC@KnW-9_1wsu0v!i!ywEW<%k14DAaJ^%ks=e zKa{7;xXOEBSG0tbIn(pYgB5fmpAnaI2BgrX-13qE5548Tm1;Nld~sL%uGhb~z3OFG zX$@=NG*CuPXwffeSVdpM$^=VbMq-nwI@N(xc38k)rZ1G~H@E<>#XOa^4s{mvAY|G2 z#FSl-)8%jdiiVc0K=Gib(Ub>FJN8>ve3Izg6j8UdU*5dpo$h69aarIsDN#zA`FVa| zh(m}8ai|9bbrwR5rz~j~^q%1Ytvp^3dbtaDu%q~uA_C^O~@L~oBjO%XJfa-;H=;7* z+o&(zY+HMgFKu%f%w?XRQxs48yT|L*F}CVeGRJiZ_}~6_KdQTJo2Jh1I+2qzvl{Ek zbt7L9<94)VP>nx*+0yITxWY*&%c^E5b0?J?PAV&PDSVP^)+uysSGFs-WTad)$iz34 zMwaz5dg&OwAPWv@+c?6ILlA9Q*0Yi`hR_8T{?tv%0vluMANj#q&_&F$E_Pferw4Z* zXm7s$GQE>bZ=uyp+OuPDq+@t4%OA@u1GVRlV{~Mcp56X#Yjgw2w2Bw>liV?%L3qMd z+fChCP=4&XB_(=U6{!sW8igMQvYI{lL$T7={el6G?=Wx3TgFhMG`Bb@or$#-~m#k`7;3O2hmzIK_e=f={$5vy868xEW z*}&O@!HKhX!l*9|nn8faCp?SO8HPcGKqm}8$Fd#!R;xqqIdHnY^fPz1zxkHi+a(vS z*7{V<4s5@07_bPDMdR{imtelzhq9d?cwYO{pq4YJs{<4qt`I)h0*u?)fzjyL?g9Uu zJ73^IH)ST)PdeeqPmBug##7#Q0~Wlb%6<~PE*Zec!i#|Ks1^@wofrB&`|NYRlbp** zBkRYkghxQXl1PE%@FxK9lshr}q-+3{f6RD>E@fSCKH8aLPL+R#c8)q?7<%IP5US-g zKv%jRELZSs9XXoE`;ZJpadJeHuc*az+geQNBZpLnEQzDXZL-MFefr``7#Xs`)R zWSLe!*ix^bRzkeck;QEhcs9q3&H*QNJ!}VrN|;|i>qo{4(U8W6G%jXnq?vw`pI9L; zJbPTiif`J1+ z44=xoz2vQn#H06!znl*(_#CZtL7z8h}>`{ zPZ?`gwp9f=n*gUASzNA#HZdC0(T;1c*B{^YO#7Bw`SOXdH7G{jD2H_TEtj*1jSOa6 zF*EYRW2C7vbF}Wz*oGjJAAOI{u*JSE36mUbS72u3fZeG(;01-DWthjnhvHE;ql>w3 zv?#My>wVRkONZdXmo#=WVB-Vw=6^n(9cKDoFTr;#O~*DAK9fU5?;zd43oS6~l7x}! z$F)B>U`k9n1{GcmK#`dkyX{@Ii7!5WpuOM zoLL)oo?1W={nPzeJ+c036KEA-Gs7iKJJXcsF$%Eah7wUiI{Gl92^t*#uT1GDm%ZMc;GdlFMC6wn{wm^r5y#d%Irt zvYndI9QS}1UypJ~2c9_P7jh=EV&o+Rhmv1b zFlB~gz@TSxp|Qdf59Abb$}Ml8Pm*#BJO@v3!mJm%Vs)ns|DGMAvTA^!%&2@XvRpO% z=`|RFpQA^|4SeQ`0>zX!cz*T^kGFTc_Ud-!#T#|!=b-jH$=zDB8Y>STvpoZ+*;z|! z!auqXok{p^1hBAe_(ga+co3bz3+?RlUQT+-L#N=`m~dsZa!^z-UNXHNY(HsabUUD% zD1nDYac*#UeNA~tDQ70dT~+qrFy^p-j61Qb^o9*OEo)pgu=Z~!qQ3%zN!AaM?>`u* zm^XB=(RU^L)`qrZLj8>PkTyLEEd|Ol$c*(ip`psTWG;p_yeE7m;K$&@hf7Qh1(u2$xX5PU7d0aEAdmcU5-v0G3((ZC?%=FAc`25>0(SXn3uoR|pP-xu7lh$h3 z)?PC3T^Xid^6*5j&KtglDewJLit#CFw09#$c4!4NEj$Q{N%^;fqE}+j&_ay)wLEk& zI($1kWjQIj6BdPOxMX5s6tP3wQxG2V zIVn+B@ssfi!!Y0+D|OT6iGBP1I%KxPoYoz}uuJ=!W_Zwa--6StWRhX_;B+|)k>G9* z9cUf#;Sd%mm&$@xc9e9|u`lJ0(Xs1c%&x3Jy?AIBT@Eb<+fM3}bEnVn)P*3bKR=E` zo7EKycj&dqS2HX)AWbmdj9fbR0$n&~fXey<@}gH_V|Ny_3ED5BhWM~PGrm!s z?uv^x=(Qn7q{_UG0iDD1@T82+oijUi!yQU0aMUAd>r>7tzWR-hS*V8}?l8}eg^iKJ z!fl?0R<5w8^w>pOCeN zPE1|!0)p5D9sV}GSa9;1_F+XigpTPbaXuJjQD(v$V3eU_sT+(rUZ4e61_Co(cWiLt z=j6kMGu|8`O%4z+MfRH;D9|2|LsFok8u^@CA+bL^m#wl46;csg-rW1plkH_M+NOnN zb-u%3bH**%GcROz>7oUmGGZ zKRQET#(N%k%I!7rmEmOxbfbKwjobOs0O6~D&3-hn&_9{ZD1VkSsXWCcY(8khW?2Rj zLg@{6>P{Y&EH=uX(5`R>sMWfJOeoxv%^BL8e0Af3Z)fJtZ*3{5OEBz+p2%YmWM7as z2+KBROx*aW105dvV z<1t94P1w_B;nd*g8j2NY2-j#%Mu%E_k=DWCYGPp1J} zB@|CI&h$L#@Sr_|%E_3$joyQ1+F3p_UEji`7nqlU6y&hbuCOr{f65(;u;ju+%-pER zlXl#Z#`H)>I*ti^c0`qp`Ezj=?XeCyn@R^t@hhV>cVpmH`~W5$u4R98tr*Ln$9mDP z7SDrvX7RI+9cVXRvssl;kFbn1xPwr#D=tXMOB&s0{bs!AGvUF@ovIp>eXYt39z00< z5Zr@c=M9hQXmj0%ukp`gYxu@r)=@;58U+tuo!0w;Ac}T3#klT+4av%YpoVu z>8#K|OQiuXUv%xrBTqAus>gyCjs`*vIy`wmVBkOwp)S?%D~sNR$8)dn=kgJm3}`$u z1uq_nA{4B?SHJ+s0k5^Xd__l|k7Zw;2S&wQ;+Ymf3h?R)(UCu^&BmwObNZauC3-7J z_T!b$=hk9-iqJeJKE=&-N)q2gn}zu^6cNjLZ#EXPB3LMfg5|lWt(5s=L^Laz~N_I3i{SurLI^|YTWwlt1 z-q2ON%s-Wpc+_jc71I?ugFpDt{&tDp+;H)>^}g%n=cRN8ox)&iVBt5#Dj-dTj$tat zMsx?SBczOM2C!vcf{))l%McIV1!g9|doUlvg>3>z@O?>2IWZ9XFZco&rp`<%IIx~@ zyVjI(xyA!ez}z3L!;IK+4w+5WFf~e8hqrVr#caXM0EsV z(+Cu-lQ5BKpNTiGDiRavB>l8exNelNpG|9H8rWGxE6u<*j;?5su_iUkp^d$c~eo z2dvn@83Vy%h!Mo1I;XsFqv4#(0--djMYsroixn!|@fR6XHZcvD5Zsba{=D@yv9TsX z0^iO+XUhgw^dKALxa_4$p`DyQlqordiSKBG8N*vUa>eB1_v~q}y7>|{ZoT*Fh`xxe zOO?>s4(m}mhB3OjtOMK;IO8ktX)|#*p3Y@Q^fJ$8ROZIj>EpMaUEpn)bms1x<*ET8 zDKz+j83!RSWVllcZapP0WgXEpft9^38J#UD%w*{JTt?B6b(c^BR?VQO^Xk}qlafm_ zUTz0Idj)9|@P~J)$1ItFsC2b7mnpBN43u(SSLB&hw}HWFS8pVb-r*a-rTJe zSh81npn{fgrVmP%EtzSzl+|pkHtdqOB1By`^Aj6p;=E&xhui72AjTHsLfb=?N-o3+ z@aqMm-&0I14BLodrFoTFj(EkNrQ~88G`;BXVfZ7TwCk%_O=ZTnl*P7DHc5K@n+jOk zE3Z*zm%BG&Gn2B3)t)gI&3%FurQ0Py7l_Gnm<^(&%A>p?=b<@=iya|W=TS*8ohfv-SgX$U@RNty z69BupSpgnX#~Mrm?uID^DJMi8m6;g@i}(ok`jxTt(`lgdg1Yhxjk)tO z|B-h$z!fhVflfT}o`mFH^@7jMoKupcD;d${?cBQu&B32{-FykgQLl zhFDlWIHfnK#ek&=r12{O{c2!v)<0GE!h*4ac}fT18F(lI?l!DA@R8S4-<^lKftz*^ z-U#WzvO8}vcNK*v@@O9fe8`Ijc>b35&fQ`k;tjq}Qd-$mt3cISbce9szf;DOKYzfm z#gg+n7S13pAzM+ApYL+TT!SBaT4RTmk+b7t{HRS-i%#Hh_Vh^+o;`r)TnNZavO%g2 zhKJx`J}U3hK`?q)3PTRnSzOoR@BhTE_D!$c(KfEpvr&gN#pRm32l%9cg|Br*hH$DJ z-n~)v29W_Nd?sDxpSp}u<*IXeK%rywU>W#OAwEeO(}8h#O^bpatVeG!qn>V{9nNJS zoROK?F#gJH5IO=h;55B3ky#vf0kc-*GHloE3BRyGknd8m(9UB`;DS@UsvO303dq%nrRGfRn~VHHCU}wU9(07Z*?)* zptUdYXEx_cN`mwkbAdPvH29Yz`*CSK}LsR#VU!2WyzJRz4KC(najJY7ys0~Kq4 zpfZc)-D}oY{g<0DXU=LZSkBNtH8D4GSP_;LHu2ysoIm2&pdvkoega2%0NrUR8(0@e zxZw})7#937wrugEu|pGl(+_gUCyTsM=EUCx406@6fYU)Bj6M~Ix(3B{>({mic0Jeb z(qj8dUU;cLeo?c7qB9T4tj<%-0zU41bdvDz3I?B;83*MoB)dj29$ykUS-wJ71`qH! z{Kf{sCf=riwEUUaTN@bgpw+=3NEtF}u#t`2`cepg_DM18GQRM*9lcg>SmhFG4I4h` z6kquA4&LZza@P$y6Ut<;_MMUDb1V$7brLti+nWh(<659KmIQAb?TaHs3E zo-++!XybndCE(HVupW9|rvWQ=q&}d*zRA3v7om%$ALYT}a7owI{5vc&Cdyrxzy~^I zJvbAG|ELG)v|00@jcE1=oVw3U7II);E~qD1&tQR%Ke)?#Os`4evf6J!k)KFoook0H z7#)Ou`Y#sv<9sr0;80+P7&oH3!{7+svSSVB=iCtI##o$UuHhkm!GU_h>*eHA?gy9! z4-9xZTr>oh^6QSl7kvepxw3%GQEc71#2AAhcW-|Mc^Ynb&BjKgeN>>UTa zI}^)y2UX_<-u;T*wxjC@x{@xp9&{{qqBJ_n3DB7b$OQZ$+!}Q1EQOqW$1l8qS)NI% zb1d)@9}fb=;4mPN?uS4_8lP4ogzq*d{lrn^9KIxsaaCZ3Y6Mmq<-j^mh@!+e z8pAn58y~u5TC?fAD0|)@oQMQ1@=a2XfYkbR9&wH_W};>V*n1kbn_p-fqJ_)<&$%M1`a z>)mD123Ph*tt6$gxokss`(039($T(^M$SSIFZiIdJCf7lBz~OnPwS2f9ZPKePBhQZ zR7hOwRq&%m%YO7jm#8t;!vFw407*naR26>ky?7cX_$lr2y5ZU@t=9=%88B;MFV+;K zgS!Ft2^h|lljh`nfi6ke0qzr&@za{bN_4IVoj^!4P;tUI&fc=l!|lKftBzzGIS11# zHQ_9!L5iQg91c9gYvM0`Y#U6JTECtEfIxr0?I&m3-o1O<7CoYL(Kby2Xk*~Xf%RT2 zS{Q9?3Z*P|PQjTOKo;S#i!^5{I{+XE38cq-;Kb*F!^?(sGN<8OYKf>PmS_uaW}bXv zN~+U^|DfYcR@@_7&V$}(>j|gmif`v394hOi8uA*w()jOx&r93t3v^k^X%`+7Eha06 zIkWQ&s|x9|2HVDUtD;qPiPmZJ>h^RQBVP~P9S(mFG_H8bVJ6TADar`Ya*TAnz!5TS zOV{0b0q~FCLoa(PT_4koF7V62Ooi>G00IT%!0hWWUK1O{!|i|GFa6mPWcwA*^0oW-U;o? zqFVV7$Z-u4#QK+vT&A+olfh~3pgB3%U*cW*m#>k|#!&|1@_YBO;OIZ;W!C=SL%Sq#rN?R!? z;tF@n0}I^kTXM`>aM}QY7+#Q5&%8`>lU_}JQkRFww-4n1O4~5gX&cV$U_+{DI*Cxq zF>g)>zsjt@5|1b=;$k`cnvWc4O$Yd#EO{l94plssIYmb47voE(l$j?U8T7Erx?OQ~ zcqb1D4bK%v1}!=dHyuDCV#FuUBOtH6{U&t^-Z;uz8T##aQ(|iAl?D=i@C@IMI!B@@ zF&5N2QZKeHf~%sT2hTDTv-s}HRwJRo*%w`R%jzCpQ7QC?UC(n-4R}4Y~t#_uWsoUw_wK?dSgIZ_$UsiA8vwo>ihd$Sym^9GZGfvD>BNbp2lJOf@Z}91C-gRx1J503n>O+~p**?+ zvb?I374?sPel%xf0s{&bl_-qoQ8#^>^cE!BRhhBKfs zKN<$K3ar85g(-C2!3A!5!Rb8sh7O{SluPI0=Vt|_HVw`@%f(ZZ5})yT&=m_cl<19{ z_4=LIz}_rAQO05tvp8&YXO|E7#Mz0%^`)QEO_z$oC#+D%V9)}e8xPpe%!z@e8h!VV zWO@+mgG_8<#>59a7*v^ESs$^6ul*{!nEj&58pSI&Y-$fYyt{3`aD!jc$Sa@M@Ld7@ zozmMwh3;S-L}MSzQ4H#hG9#%v%OU2$lvP;aJ-g>EI!Df?>xI=L}N{y2qO*@upt)Vt5`QUlku zEyyUPgMI}5NV6cW(7dmMvu;NBiAFru=;I5V^>GBu7j-G9i@;DrXmlDOMJB3ncAxc{ z%g_b)2A4dcE;C9RX*c{Taz!oU_afs3H)nM*M_{3#jMJ6Je{@Ph*;Tp#4o(nj^C4M3fs&g9TS1xb9HE6~HN(!6S7*8NF;L-ABQaPWw3M(A@^ zNBGdkiFQacr6c;_$B`cM7Sv;Wa6@PPBS((uqZtR>0`VH^)ZnBdPUDcec!QW zRJC?G4MLJxUreeyOw=(t#r6~2%~Pjf@Z%C5)A&>yAMv2yP@HN5prL2)2w)$(1X^0_|f}Z7xXO#41()k z3yi!mL#mcPq(k&bd!%m>kex85fu%hFcUf<*$uqD*4zG*!9np(4P_d46;J|b8QJoab zM|8;biO5=^Sr0IX4q-f(b-W(IBu8)@qu3C_CovB40nUJH<&{684VQoMS9@fz28^x@ zL(7@dI&ZPBX_5UTsV)Z?vGpYzjn2$SB9jHj{o2s^p7-4)SevZ<} z!h=t@_kZ;BYE*08s4&|!P9y`YcWT9%jR7OjV5<|9E{1h{<956e9@?s;Y8YcVKBKZT z5YpH@`zoYqHo^YUej+{iB*@9M(zen92^TW0PlArMZL$miPyq z(VWXCJ`6%!9|K`fJmSHnI+_LvHmcHYdV6E`#B-jGHHdpK7ds$D?9snY z$jllwxT}r+%71#WJ-BPX+G}m|xcA;C+XZX&iMdT{+NbV$R5vB`MpooXCYzEsY~0`% zL!Mx}hvpg^w3SAc;XP|CT+_zgrer3YZut}(?ReKy&$gFbx20Wv$tFLFrJ8LoyyoKe z-@NUsq#GwC)hU0HwMuRM*r`?R{>S#oSM-yb-Hs=Ju{UmF=+JR$BiPsdobe$sGz7P8 z+Og^c-mGD5)NJ*bK9IuI3NUQZfAN-Yy$n#_vttw=5A*a-1f$HD4n`DBzT$H`z(P4% zD8SPiO@$pUA6|Xk6&htJX39$ky`a7G-JfX>eCeP^|7wV{<0gJ2`Pt%&NTaU z)_V7cpVfy%Ys9mIjafLc5Bv)>vwHjqoz)KtSMs~cu%0NO3J=qQ3Q%FO)M~7t^;LxOzQXV}_+w~%C`Mt4v zD}wX_Hp|3c@T-Pdj#OUtIlY=2zx4xI(AH5jRVG+3iw^JtkA}_ch}jdDrW|}1vc8V3 z^$`X$&8mDeL`+z3V|TR&Dd8~#Jg&uF2EEKe8I)P)fxicD$sq8{3`u0|FFx~ld+m4c zY=8G}zN8)1K&&%JdytNt`}`t-`;rp zHSJIT>T#K&n-3b$Z@=xTcHpVU+wC`8*1q}cUMaon>(6{P?$M`@wfBGQ(WY~*dGa!# zEgSSj@uO$kyWV?ud-sPPXwMyD^QGQ2q0au<2cJ})n*}S{2S2g9-Ehqg4gRft_L2R9 ztKDdj=KGdsSLx-F$GQC0A>SO*)!{$+jl0`F`{lcRW1-g3sHy)IN{cKD$F4R|@_%>+zt_N&*=vcWz52-^CqNEce2!)nIU?UwkMMnpn zX@Wy}(W$KS3?G!KpfXkZam}Q-y{;ZZQ+E+}d!9S8R%cGY8p)OdodiCSXA$*dOp-^B zpKZ6^aH;q87+)obF1-g@$)u6)JEAW|UwWaI0qX1#8W?)?H|Nd+_jg_$tZdok7&A}; z9Vfby8NQQ0RE|!-IbsuJNhgLax-N&2SIR~hX>1=u${pKxRcGYKW0?jvrH4}GP@^zQ znRp6=8yS-713W}9aOvOl zl(;OG&!(A^kl~X$rL%Ae2#x(OzGZ)zSC@1r+wXpQU%TVRZSB^pw)sP-Cp9y9_)CX` zy}I4Hd5PiQuGRs)`9KkD?q_MXqS z3opH{ed>u7?cN8UXsg$4YQOe7pKL$<^MBbs@mCMF2Oiqfe*4!SZ6E*C7us+Cr+eEE z{>(iZ%${y*H*IMj`Ogow@BYa@Y2W{IpKQ+_(|)iOo7ykD`yQ=%ZECN6@x{`6WzAUe zLvJjp&*br2U^3i(WI#%Kp_u#_;;3S53~{mGji(u z>ELygDFU9dimOAyFAXcmC8I-CK83D>B+H0eo8a^9niLwP_|{J9mD;4qtP5*Pc5yLI z_=q*^aRgeFV=mBC_iS6MuRjA7v3X27C;zAl5%$v5tnbOa88$R%l@*dlRGNerSnX)$fJC;C_69@Aj6fDu^aMt5KD-Jd8VzDP7I>-*KiC&a!wV4(qMwo-MFt4^5QR9d4; zUOj@OPt&iFZpb*P{8c+PwV(X4Z`OOVj<$dCs~>B}+t&7xzj(AYtwa6DKfJEJ@|Mf> z4)_D@MLRET_kLk-`>o&K)Al{Lzg@ZRV7ue?>)M@nJ*=fBV+QVrWqE6K`6#9w`LL0Arg?(tnf%5x;rf9w<$@Glw8 z_Hyi83^1rrbyEI-ZVU`O36q=-SkBL#Z$bzuCQZS0u1jW*R3*Uhjr7F7 z)C22a3@)5R2+<3h=Pd`MV`FuYDleD^G?gdL3@P5Q&XQ3xJEP50`q=zo0gr`qA;tJu4aZ9`8;!W+@11H-0jT_s? zKl^0+uiy5fcKbDJ+7G?urERkw7y8prKGeSX^*3t+=KA*Rk<;x@?tH9Wd*v4Gs$bI% z9o*NBX`kR$eSG&neDDkHT|f0L?e-V%XgBOw)xM<5!hio4yV}>kdZ%8G^jN#`hMih( zyr6yf6JNA$tcRtqbe9>P`j6l2JNXd5aX56JR+$DVCFqy53sslmlkIb#ySIJ*zI)X^ z1n3nCPx#|;*6cI^V-2nvRs@%X#a}g-1tw|=iwHw5d}2U^28GH_pyRJzv(Aka<4*EU zYZWt{=j+Ap5JKi|31lZ_fJbh@XtWw_Z45J_b=qxy@SyHKX{6q)Y4im;bMfFYXI4Hv zuV&tk>h8*BUH40%2SHx&#$s%(XK3$-M*Hd}tbw$4Op0vK`U9P9{rYw7wp(B9%SmP; zENEx8g^fPb%LdNM*qGUeZS55hyBA{ILAl*RT`1{vVa0#(XZ%Yku!0W+9bVM%n#)Ls zPvYQ>arkokK8;pW%% zF5X2S9Zh2r!2>)`Y+6a7bI1^5gdcj1Hjzgkw{_u9RRUFXbDDbBJNxyX{_lIsrMiq; zr}dy??QebE&FxP$TNFlf;h+30Xih;SEBa|#Qv=gAvUKPVOaG*HT7h-}qBp+VvF z%J#AUe7`DOcZ{^YwDbBa+RJXpLrQmZszn>kiE`&m3&G z+;FuP@-=Xs*1j3(hTMa?lXXywqZ{?s0Cq=niMnNzUKyi1T?|;81?O2e4(>?O0l9m$ zaf2GL(#LqU%uu1&f8NmjOBt0!r%AwH@fk3D8o$~P5Qt)e;n?K>$`Tm;5=Q=pBU6Rk zEhCJ$1`I!l4{N%3cvT&BL?Mj&!H>aRKW>mJwBg`5I954IBnY3b;Xx1D9wIMscsTBS zL8aUsNoki+FDf7L%|kF^O~UE~1RZ&J=s!n1OpRJF*>PV?8l=33wJTC-MbYT|X4%Oa#m{;3nk+S}fEb9?BK zy=}uf&D?hG(5&r5d;F)dy}FuWJ?>PyeEWv>O)tAx1K-N_9bb8A`~466S$oSH zZfv*d6JuvjA8T86IeP5mk@oVNw`+hq)LwP#i?sH(%6IK_s-emp@SA-q-;4p6{Wt=Z z4*ukCpAcvV+zDLK_Gn1IN5&l z-FLQ2FVyW_tq;8au@&v>Ual<$%wmMYsLS4#!^h6FAN_|PZ9n?MceL;S-dAdq-$8AR zJJ|l?AK%|r=v!wyx36k%edCLDTm7ULD>rDibmylZ*O};n_R5#NsJ;LHxx4+{?|F&# zXz30MJFj)OiH5mBv%mEkSl4JK__WSQN1u6CmjD~vN$GO$!+W(^P`8OSL;L&}pHRiF zZ#TZ^%J#q`PpYAu7|+CmEX5Y3PBMO|rzDSvG|~jag}=p0W#F#hz{5@zSf>t6qAI*VCBcG3x=3S@5<^tJ<5t_9gn-6I)P@ zY8j>OmU_UJtQ!15##^XW?Qps6R_Wqz`BH>BE1c16nyneHxb5ZMbN9YIPpduYu9|#B z|HC&trNG-9Savxhm}giKxX4;`)iNe<2`hMLmT{6N&TV8EI4{GXW6kBLW;=J@v$wtD z+rFZ0yJ&rT`K{NqyYJiE_U_l`s^uWgK6l>rnARQEwr~CBJK9rw4z~9`_)xt+$osSE z{WvrfD)gQI;*IUEKJ}&c&;HH(+7Ey4SL?3GQ|$#iuWgrJzP0_-J3rLk{Kl7PO^Roh zbcWQPpa1)J?{7cww_dBYveoThe)xg*6S^aER4-I}=kI?;I-Y1RzWK(sb;E|XZqtT# zMAz8A{@%~F&3g3aN8a(u_Gf=~U;D@JdAz;yiVNG-7wVEx4R-(G=h`cGpJ^Yt>xuUG z?j!A=yyNC}-PN1Lv-XtOe*EC0YMN&4&M99}?nQ9L1a}m5Gy-M%aXLTK%oVtNItK7* zTnVYS#Nm~hkag=kT(4+pk&7}*R5ea`M=$t5XWHo5^Tz7~gB!fk;Gfa~_eih~AS;kI zZ}F%!{*XO_GZ*21vZYA;OUJ@*%z(hPcX3VUXFWTgC}YIvk&a}37-4w zGPO7DBwOIoLnqI(d6Jgak6FGfAdodVTYzmpA+Ue1GS6|M$HabkBSLdr$eb^E>DM z?&+5qcyOVQ<%8}KeKsC>+N<69$SEx~^hG(ijv}z|TuzERmvdQ8NN}ykE}3PY)_zTy zIkWWXc{{LsXPGx|uK4H!9a==nY60&DRlX!vXAEmXWt%{JV!#E70%~*?)?}d^UxD)B zNf^>7>@K~fjz|H#p=gF5rW-}Y6!jCT5xj+G&l)H*bmsN3?$OMJY;4{A-W0Z) zj&o%=UiRXP%6U&8C_nT+zE$RrPbtq{vZow9xV!wtFW%%eCVDxQd9w$~o_$Bk6Wh*| zkAL!Bt*$*?{_JP3)Z)A8wWJzW0b4>S&NXm^>~-%}phc&fbO z>Y3%xu`}h)f8J8=_{E#cKi#vjti5!kEMGEGZhiBY%U2&iSU&$-FDiF`>&f!zFFsw~ z_TyKoBDlV|U{INKUL(k56f{w(SL;UlNf$axZ3i4_4Mc@)na=@3Gjl5VeMBq4M?LXq4Gf3K#^JbQ^w@M zJ>%enOc)eg!c zN=7S7CP@YbqoO^0#C1xsVL{ojmTIcjB(VsVN82)pmd6Omb3(&j28JIlTSrAxAOygRYU$V6P>Bl#eC$+Zaz`;@;mO)^AiSx8YT@*uZXtVH-?Gl_Xw0359xoF|E zvR5mmpYxnc_3gK#&dD-!`P?!hIk#@!Rb~!rY)loa zY&cOq{8N{dk=_$!)rz5V&yH~|)YBBLD}3f$5~s+DLN}zOp8UpY#SxF=mVQTYiPCNb zgU3X;PT6EUcPcKb!_P0vC}1p6$X0ozjNtiKA7Z+9JYayOIC*abs~&g+{T5$xUuEEd zq2i-2T1xyF8&s5GqVgcgM3=H-2yixhx?6MNnet>_O)w*YZ<8dTA2d9
  • yt=#Kov05(t^c=7?Q>6$#+qf-^uJ+@Wrpg13j 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≪SW~q_-!*X67Gb8X_ta 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ɭt3iMBgA=r4^M`b!|K~ES&$tibF^wv+j=9609nwef_Up*0aKjDY{;}PPY0u zmJj1(RY!l3$TlG|GwS@Q=^5K0FnXcD&kJfVv z+T7fvyzFA;lL*NBlf6b{sGDrpi7e?5)OGQV$&S3)rJ|}P=~uz$QX(oOrj_wrF{Xn@ z-GNocfZV4i`SfkRj# zKfgf7EAj2S_ZC4}4J#x*WrB=~ZbQReGNi2;d{fCe=IwV9c~_@Z@gO%f86@<0E#ugn zii9rf+XtnQxAw~ZRsXaqcKtvy+cErNbTgAj)Ii!lL z%C4=eC&xQkIe6Y5kW{>j@_LqRJVI1-j6I&7J0;bS!3TChiOTP!*PDMkrhb@Rzl|^3 z(7Cglzeu8rnGv^*;W7<>@#1B0Re$ls`Lm^yuwl~F&fn;!clMnJ!Qrv284IEjUDt{` z8I7)!wb^7EUm>jV|32ea5dUAz_;*J9?)_J<&i{GFCj;J`q8(iSd*pIqwR-gbo5GTbXZq|JGJ-%DwSO=auwJ#@)^wIMZM>a;IQv|lu-<(4 zHutVjWmyWhei0Q+Z;Fz~H%$}8I)YtSzoQ0&Sa5`R^i5P?s+`Sab<~{m8L*;Qu%T;= zblOq}yA#I~6ZapoiRPlvtTnc$(0Ej0abp_gN6Hr_VCY?f26a%zlXQX;=54{9n2XdwA? zMmRA4j$zFd*f%q@fB%|t>Pbw9sY&3Tb#dkJwFRqXTjYdMi}Sr4IOqk3-lJXtH8BT3 z7tiB4b6=zKZP{0oJ9;bA;`yAgg4_9u)Ts+Pjkmf32mTnC^h1D=riBBYa?KRQ)pK;k z$(v^sj!485blu1eHubjeD{Km$B<&iif|~w|80Z=0;A1kpKG#_)NvRI5V?T^8m=0OD zwg0`x`#}=9jY~@U`AGBX(ihqyEu@2$eGQp*3ji7-ya|X6kl{N_`5(Awz4n= zSJm#Ya3fP@>ikPZIG^u%$)^RbW1Az9TgsBUc^sM$Xg?ef2NL`m+Hn+adSVTNN}9zk z%z){vfMjpBjNg=GJ)nvZa-sFi%;mLVM{?r43w81~jT!ao2`=ygf_4K9MW&|YM7tz? z_qcC2B@LR=foMrR$4zH*ludac1p668rutlZ-s<15pBGW#%(T!efB)5CBl zDcZ8a=>7GbKP3$q7SCrI&GLl|0ncWe>5r2e4R*iOla> zwn_UL9jE8V*#%+cICL@Gh(hN3%?~-6UAc5T74BvQTBRO-4;suK+@m(S>6#4FH2lTD z80pM?l>>r%O;(N!{l@elahr_F=)J0WJdrJ(KsboGrdD|!6Z3?xaGbtfXZ8eiDJEivy!g}8?%JdN%f?T<>tVboQp}{3XJVd_^wyew#Qt^ zdToSdxbjs}8I~wgD#7`HqMLK*WBd8T*bLZK${<` zE2}2Xa`LrPUzXB_I$$hcqMoM868<67H>Txh@>p&X2Uq<@gZUzjVl-tDBPaCqpJ5+> zb{TaKgv`-)$*f@iv8Je+;eIuv@Sm6?U42jgLa#dPC-aPWC8)v=NmW9ujg+1+(w>~I zWqk}FzM%tC!|HmtEuQK2XF&bEdCdSA%@4yaj;af+{9~xZM9}!a;j7jxU%wwxU5wuF zCwIfj{hDHWk%N>w1vk0lNZW_eaxSlH8Lv##X!Ha`e94!=%AmFUDaM(nU}DrM)xe1l zduZNaK$6>MYe%HMl;L+%#pmld@9)M6_NK|f0o<<05 zHF?)Vxm$GN`ZjON!M>Dojj)QxHQn0(m?l+jT%83Yu3nE>se+uoQ2kMZ|7;B)AVJSXz{O zMuUmM$V79a{op^=;0h7ss`uP;F-Jluh2;czi$Ja-Y5fh1|El_vwLofTA9*zq%XZ`N z!|Rz)pJXW&Q{E745PV+FFG5;of+7kbYm8n)V}V$m5A?+ero{*07gF_xl2YN2a9U^k zH`0ORWnH~C>3kUE+ajPkU{JHjIhiZ8`^GvE`rRGs;~#v*B`8H_L6=yIsjcw zL69=SqcU7QPK0T@5|bW-*!&>$wlj4MSg2SNu0 zGBz>r4(_>`BCIWy2128mUCCy4uLNeSm_fl;ZRKf;a zLn!NlLCNvTX#uPc^I!#G$o*8tenTr3wu53tBf;L3q6w|DzIeR1pL(3PzPSqIS{1Tw11X@sH+bp^= z9Dk3ocw{06rK(*tlS5_Z^ertKib01Hm9P|vgC7#t5scbM$0u``x3kfII>G(2@Tcqc z7mBQKM2FyF7v!na!K^fV{@SURrm9r&3w}(~o-D&!8byC9w7^Z=EUo2p^Rqtr`J{xzqp}b)(UW-xtXk1At_1E(i0uVg ze}R2Mij>ZRUoF7#jGN9~Qffu=7b#&C@}O`DSOkNn%lPC zhte}&Es~Y1uphNJq8M) zPMwkYEg#AFDlz@8pWs=#$fJ4~cR7jzyUrUXk>9;guwe{ff+TO*+iqY5bED>|RxVk&qnSSX!N6A`YCjS*UeL@!08OhDdcD_!yy#g*1B?|S z;RP_e#mkql#l?EH50e09GYB1~RHQcI>a4j9Hs!M>n6OGf)re9C1!H9C%_1>#U|Yq_ zItJCcE%<$iTMLUB3~ym>8f5h^>C(=Vs)~!4k!DceuC8(*1BGwFthtHSX3+N%scT!| z9n=@*8Qpr4G8rKLh>9;+8arp;PaP2YsG>;-Y-XY;yKUM%g1&YYjDx8Df`Yo2Y%}_OSa73#6JqNRxZNl}{p}Ji}2ot64Cp;~^!m zZ9UZq?MEp_o;pLzbh@rGEhB-f`m?ZAD+``p{tMf?J*rIEVgtsJzFvz;oVZ05h9GIb^y=blJHJn)UR3GU zWTE}RQ@SU2t3LKtbV>GF(O0bZQ)diJdUBHEmuf*Zd}J+P@Nnc3gVh54u$4`63uoN< z@Z}&WC6a>nuMy7l^I;Z0{u?aiMUW=I;dfpOH&Ohe@W` zNtUWf*5{M#{~5W&a!m0kP4PKQ3B>+qh)Gnx)F+OadbRWthJ$R!6mt0>3D+>E)>2ZF-~D~?xq%o}u2tM&2+M=|!c z^A9dcvu?|*b`G;3{Rb~vPVn(;z~5PENKqNpT&Tlbc^khNWw4F`ZC zi1PD07LR;7SU&|ci2NUi*yUi^F#>O3KS6`2`)lZa=YE2&(NL&CA#E%$G=K;Ib%_Y+ zz}46e^myM2v;aMg1L>+)$c5cqBDxVz0o|u#K?AJD>!;7xBDm2Ean#TPB)njSKW{y9 z?NhVqr%UJ!LGg|7j#XPcNEfiqQoW(T$$T{NX#)VZIX0yeHq@0juQ{VB)E?RrKS9;D zJc#Q?KQ{dfRsqu{Ky?k42vcj@3fV`VIjsIu0$q&P-O$W1^g1=Q--tjx$BHeTGeL0C);M;wmuyId-6pDWZ=ep&G822P4*?g7Y>re|*+fg}zOo70=tW zj@vF-VLEF6qFuA?@?&-D@7HVGXo^}bVmk!A@|E`A77&QOo44c9f#&)VO}_oB`nO>q z{>>-gn@25KMi1ZJTL)IxoCB7J<5+Zot>iq=qXum+{@pu>?t3itTOxQ`wR=q5^)jQK z6bFGk@N-~a2Kt9_{4M-Xs6rZ{IRj}DS3i+&+*%#G1EGF8__|>{NiU&A}!?7 No*(xYJ`NC2{BJ(ruYv#o literal 0 HcmV?d00001 diff --git a/docs/blog/images/text-area-learnings/text-area-api-insert.gif b/docs/blog/images/text-area-learnings/text-area-api-insert.gif new file mode 100644 index 0000000000000000000000000000000000000000..529eb01e3d63b5909d9537293c5f2b65dcb08e98 GIT binary patch literal 240829 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 z�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 z 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 WU#Ii>V~g8~6>PpjHAQeD)cs#A+rIJu literal 0 HcmV?d00001 diff --git a/docs/blog/images/text-area-learnings/text-area-pyinstrument.png b/docs/blog/images/text-area-learnings/text-area-pyinstrument.png new file mode 100644 index 0000000000000000000000000000000000000000..2a8cc3609cb4de26a7bd87e7c454fe030fc3cf2a GIT binary patch literal 257978 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 Y*dA}9;&6;U{VmU*sXQ%sV)*X=0VtW>WdHyG literal 0 HcmV?d00001 diff --git a/docs/blog/images/text-area-learnings/text-area-syntax-error.gif b/docs/blog/images/text-area-learnings/text-area-syntax-error.gif new file mode 100644 index 0000000000000000000000000000000000000000..0a74cb649e6e26c42245989c17422e472a1bb27a GIT binary patch literal 59077 zcmcG#bx_-Zx-A+5f(LhZcPXVU?h+_gtU!U{4em6-U5ab*;%-HQyK7slNO7xBs@$~a z?0xn*`_7$r-@KQ^uT2+yW9ZatzGuY`j8@Y}}$!_i%~GF;B5_ z@pt2)d4(i6cm+B51X;QHSh)Dux%pUm_}RGym^gT-=~Nnj*danxCAyfx7pZvcXsy$*absE zBe~f4c0U|)@(LUr9C-T&?H?Soaq+VA2p*lDcjNUja`XP?@RCzj|H|dT#KV7ILB-6{ zhMQCH-TD?23&*yY$MEQcw5cE(JW--mX_x*V#L`7(TKvHhU~8 zb8l{8NmAy1XLp~(9l7b5`KznX7ndK!rSEnTpaliR1zGr?dHTwUX`b-AN5v%Ya|$J~ zW+(~ilvPxxXXSq4c=nwgk(iu%etyBpBcMVp_)f}Qlual+DmH{6qDsLDjW=}3i|9oU zNU;dD5qGCaJc^A^^1~J1V|94))M;R76hS1BllPK`Pe|g9tb?dVQ%k!8pRz2sWLrn4 znAjZ&M*cPh*P@d0kBpA*`CM5zg?ov0d*s|k?min~vUo|Mp~oVv$$Kw`PIZad#*9Sx zKC7sh^!-~NpL~_4yVO>1xE-e{O#I02wJ;fPP+66dRUUDrw*cD1v+SGWIdzh8(;i!(2AiRJxjy%2UD) ziQMoWG5bJnwLm6iFPVoy!n%qg_nYxlA4#glC|ldp-?iY8qhsSP<}y~lt73XzyA}32 zm@z_LT=N;LZwO}u^cFw?rB`bx><@#I@tTh{6b(kf*fjFg8jFYHsKuPt#~MpUlb97_ z=_?zM(j#0a_l`)e;W#E(UE^yPDG$;0h1m`b!c4Q|tc^79MziuaN9K8NoeJPiD>pP$*?ypQ27udqQ`A0B{c zv>+c@!}wMlb`zwHqQKAb?!O06i{)hPCM!`omb%u)_dlkKm&37*xnw(aOcE^i1Ad!E;d<13U$lm9_M(DftBf)H%p z!@?z?#$i!3=W%XPtmN@wNv#OpQE8gdM0RP0eU)QbmfP{st0*75qdm*IT=`mntEyz{i}NWDcA}cH$zyz-_#5<6Tel7fcjDu zkGLYKK2C(+Zxl(uIjy)N^K5cy&VQz)bp>&J8}Mn?NFimL*)ss~FD_uu05$-2hnhfX2TAy6?fn?E4=TTR?88Jn!uN(RW zKf&|%ySHb)i59mowVdZvmL-J5FXv(>BBc^pN9Z0Bw(q8Sjwy{;>RNvL0PA!qqVw?- z>5|Hva+PGC_`Uxe) z642pR=BD3nt|45q`+Y%a`QRyP#>aXBDAtQkbVVwFUN$Q8&bSj85e$+^Xcd#5us2i5 z_Y-Unu74ATa+${$laKlh^Mpqa%fzOL7RX`Ktyw9{Y`VukbE0EUyP~Hsmm{Rlr6vz6 z3AJx@7b8&GVe;Npc`zhGrRq?~na`{)r(Z?j0}o?)EQKPNvc6}3neQ%_4A!C#4a`u- z7O__+5HJx=&8v(Qe0C%^&Ayx2oi>9NaTI0)c!EDlJt}EmIcaZsm68J<=0P5fnlChE z^q;bG#J(pqQ+hA3ux>}p^8_VjP(;DZoX2&s*edFR2u7(vxMGtv^z}IzwwYC>*!ypF z-Owe}vqJ>;2T2IL5o{E#4IF|(WW>0UVf3fc4hkqM)%HC_y6;mEiM*o3!~g?^Lpc?> z*W^h-(w~d=5Tj~hPw){9yJ5fb93)<+&FWGQlC_QDNHbOu`J{zWze$^-*RTpDZRrQ~ zm>&c+tSanUg+dh9P^=25rHO7fYUR@~qxQK7x9yUo!89#R6mU`hrl@+|YLI@AccnUn zHIiADk77+P$)!B3Ms(_g@)B*_dtb5kDcvyDM8JxWzLnU~W#OatfR&ayZ_#bPYT-*@ zeE&dt^@cFNnKE-U!{_bhRKHjH)1M0J-dHCLHv)*vuRyaNEQCfTg9`rFi9)w&Hn`mm zk>V!(_}bem#HvB10*537PTOtmygRj5o-5=e~5>4#kh#F`rM9H_xe$^><` zhgUJ10rS*z(N{7qbPB}Hjf`gv0#TYjV;Os4j@W#Zo_5{$xnvaH?7{8)$?`#{5@ zN;uS9VsW}^zMTtls_`OIM6f1>?lfzVGu_XYSdhtGuQ1E(e7xn^0njZK!(n*d^b~=5 zgAM(|54Pl0jYJM>CRtZ-ei<}DEWK0_mEU!V)aCa$!=E(JNJ?xUI_WT}k*nTZagbfP zx*oH`V4GUoRPi^SMAgi}i5b8rysB*Sl+*15*2S)Ukx%OWhn4)D@U9zzd@M;??D zAfUhfw&XaV8CKWKuCTHGG?SL7ijBzTjUAKi2b^|Jr{}xfYL>cVL|Bmpd(@6P z?BX3J=_!{*tEg!$vWHm{b-s2g!SN6H9x*ORj=J2vw}_wogFf~4daCo2D~(-P_q*lu z!Pc_`oS-=OEjmdly2(e;#l3|vd?`Pl8s@0*Bx~nhJ$X9wAE6d5M|~M0USu-?i9QBw z6)*ag^B?uQzVHp$tPJ6tsRSmBI*CdQ;Q(ZFscK zbKC?CLpDfIab9^9Ijbf&Nng5-Y$F88tqH1@wC;9HuDKx;%hPaMI1)tSW(L8G`)XKH zmH~%KPmmYCu@l~k1iMdlSQPG^`5g=0kK56pz4%)2!xmnjNv77{%!3>z$8)>ci+6eU z_?F z6sa_-k0RvNE6aTx+Jgg}f#j!!jUgF&fo2HbH?6*hIL@!1`a~H%&nYr(M+a>+;Qw5G zK5Y%<27AAr$NRnv;z@>DBalTY_;WBq<+PBuQK zjStY_IkO23D|g^rnmVgmXjHoIr}ogFE50F8ICs~)B2>ew#lxuKGH#2J-B*yokuc?C zY%f2WuX9mv)9_C&<+GCs<*YUs&sZ-`2LzAXwod z`d03eU~+fB!9BawR(~Ym=vu8hGs1z2CaoiIZyCRz+FAtSA^ysDF3YponC3Z^pVAkc z!|;e#-1K2Eydq(oVr3Rl0CGBuPIlGw2No_TDr`;}>$E&(l$++f4NGj)r!*Y@U6%UQ zuy=up2I*eN9;MF_HqMxd>Z~-)#7feDt;f6r>Pc2;IxyCeTUXyOju9EQ8*SwV0RKP% zBvKJy*y$PN0Gn%I&SY%#h}aK@@WpZy+E+Lfw^qa~k_kDIZd1v4N%q9z8Nss2smEU| zB@NWgl9TTF6C=I<~861O1q7=xG)|8eY+eWFA zIf|D9l2jy;>|*HI99dk2ajY9Ms1<_FxuN&kt=hLAFIC`{p$ShK^7ffc+9@n~O$dd> zy*K2D-eVXo!b~bgIquNx1)8V?neTU7mY+2SZp<+5hZXG5WG}!Y+fB?Zhx01s;4fk3 ztcPYjFoJY%GfhA~#c)n~k?myqGso>0O6fQ}k3!oW>9v$zz}TR!l`o7C(EGmFOzJd4 zXe2o`>bYU^a$ylUHzDPbxk!5vi5~%*hMl;5@%T{osADlJwzEZ>A*mW+!iN|@jz3GJ zM{_2Q>$QDJOX0+OVfqq9t3tIY62D|$8dG8ap374EW&F`O+XfoV5d@KLPT8zn*>s!Z zIWvJpTG`Ac!45L=8b&z7Tt0&!kUEaGF?5Ab=sqzmpT9$3l~eALQ+yF+I8f+$jV7A0 z3Lpl6tQw$o$iz|3(kT|J;oDD;h9P?S(b6F}@d)6_JAew%@%f^7Wu}v+36A#bLae?a zE&kOs7mBJ*rK++3A9?^@s;O;8XO(PNRo`AEp*n7BP9<430g@&;&a)_1gCRwVLGDm9 zYtQ@Y3R?8NvI|(#|Gj38wst|LcFDAMC7^aKr}k}U?fTo=&F>Lgv~@eYv=NQwyr2{ZZdY(rBW6N*ltdy=(0WwS>YQB z`1zgnH(o=V9|wF6FC~E95r$W6izpwXWkh1Z(?QI1O%JGpt+9hxoIz{J*xxj`7`h&F z;Wt?|G!C*R*b!A3T6PUIWUkE+ArsrQ}?dCejkq-k8h|75ee$USUIK z3fSS2P-xB`zZ_5W2dBytP#n5ddH)eAx%K6H?B_6ugPa&UokZ4Uy|XV|v6KX-1s@a$H;4|T zImiJ3MnQ>_TwP|)QG9I$)=+wyMoIu~UW1itdRxPMlQA5c$jcXi*7r86D+id-tJ&?dlaQuK8!wDJ-|G&0iyg;oc(@OaYA6Wkl7PyuVy{?JgswO{ms+X3H&rU(JozM_=;|jX;Qz7nu z4d|J@3+DXR_aV1l`6rE3-XNPQ4NR-E{?m{&KR)?sZ+I0{pCYluTAYVIbv+B)ybEW1 zG+sG-byuIg(wds#QZe0q{Jx%>)I8je5{wHDi>%zD!Yn;TVMGilKz|)bm`5p;# z;Gp<{xVcfZ|qk#Y@aim*j5Hx3M zoH>Xl6Mb_77%5x9bd0~sR2`64*2-8fkY8De4k+Np$ESRW^IZ$Trj;G-$< z-Mpi~mcUaYt8n}yVf;%tK7hk)tbEK67M5r=c9kN*rr*;?%S{QuqR$3BU~Zm58h)H3 zTB9KdQ&v8K#kf0kr~y*DMiPW;dKxqbdp<*u!f`B!xqA)x-a#}ky8A<@@Nfu+lDcU2 z?m{T1K{$k^DpRyYa;-7KJwGmh2mJj|K6<+C`oQ2u70Oo7lSpXri^6bk?tbQah(P0v(5afs? z#J9(ziA%lehf=aa3p{5+K<$JXU5o03d>SaFZ!)@tvw>F44#s0Z1_+jHinMSZv}ZL2 z*`{)rhH!(15irjtx0EygB9Jh%1%_{9PG_rWFcJS)<(FTH3uGX09$Ukp9lhql+A4S# zcOuZ4x@XnIs#k0}N^#gKCjZU7?I}uGoUx#ZP{IjB3^r{y4Y?CYlhzy2sw>Wm!gHhv zLQcsUzd5)PR=NacHYbw2RlyB%&H^lWi|3Df6AZuz59IuV(QXM!MNeRsnsC>(PW-+CZ+sM77InGa_pS!jJ}b8dsAwH#ku$U)#bGqt z`YIs2*#Z5SkP3&lF`l+KpmCItSVGjGhV}SYt~8S9l)G~8Q4P3^zaQ1*M^e*wymWuV z0!PZ_M^1gk39+ELgegh{Yz!dFM3qw08gU;7Vu=dZ?C2$)vS>{H+VlZ|JPKFutJQlO z6nAgGluIsR4o@@s#W?sYfh)O!^5<+845t&D5#R<^oJ`>kS(e=lDif%;YOvzDeC^pb zN$t8PK)prcve`7&ejaOJ+zYnop~LCjyr+(Bh`^79fZzZ_$Zf%_uN%bQ4bo8fRwP8e z=3S7DBbKyQGaGYz7E5;-LkqJUWQY^%f#0B{)k-blQslX(>$_09!r8WszOYVwYJo7l zAd|BOOY+FdSFq?Zu4)Fr7~&4%nIPkL{Bh-w5mQYF31eoYRWHYP4}?4cOI!Zl9;oC7 zgoGW1|D0_HfP*kVGsp55KKH*2REB@ZJbB?ZBb}PK6ic;sNoYu@qcb{%#T1NlY60-k z0a;qDcNAe4aP(kZb~F?&JNq1}B_BpOT`b(g>PUtvVSz^y_Kf&~;DZp^EGQY_fg<8v z?o4Zpjz*;0IY;8-Ff>R%y)dWIi)i#T<0k}`=w*JZ_GDK0`D>V`%~*nhc3jh4FZyQ0 zXkDS#$3qjMdO75$9ZFBvGhcP0BsnhjRJVnap0@{Xxt3_F-03&*%@@^z!|wOTiROS_ zA$=zK^ER4mG`;gx@>7P&BhLW?T(_W@--n!5S9|QS$ihMLi$}=bD`H`*YH6ChUNpmu z701lA=~Ro7;m5mmZzDFQvw)!R{4Y0w1#8k;t#EV8imrI%S`pyejN*$=d?5MRCMQG` zI}&?VYD|PH#o8`HP(vqQ$S)VREldyu{EFky@S}_AJvR}^>jGqXKjnAU1QqB6q@eFh7t}8``v9_4!p?U8q?BML;jacE zS?V8)APD?WV7){c#mtS0h7}D*DCO`7%7bj&Ig%rAq({S{s5O+DqI?-O=~dHCE(1F$ zOT*Ek7f_4SaxI3;d4f|}e~SGYUcsJ9)h`gj%52~xW@i#IVRbYp33+#$sC%hIvz;;G#ZD4S48MFyz1rG7h=9*#N`(1Y zPE8ZHybUM;_Oao8N9_8|6Nor8W?PiVJ@&~9a%q~9PI1bVS|l-lgDXTxwv%*m25)HdPbm=dLKA|^Ay;Jp_b zUK56~JL>MrHw9{Kf`sEW7itTs!tm$kMW*=-mOHQl=qEuV}??0AsF=>+b@(7fO zYmz)T7MrR_q)3J?pQeA&#*yntjI(2-b_?f~U8d3uIWSDTgV&kh)@l0nGdl5|;CvnH zc{h0W-E1Aqe<4|xeU>QA)6^vP+JXA0_u*EF+Omsm%nqlC{q)+FX#b&viABtcA>log zQNU5&+ok9vnk>TcH)MPhw@=?bENs<@C4PA5rRDlsnTXweJb#wrA<>=i3{9VYTQcmk zb$e*Qd%!(otP|rjvkdqp?sqElbkZTwBN~kdXA7_X$t~6Y5LOZ$Izq#(<}1 zcJh4s{VWq$te55|M4*rKU+{P+o)r@j@Q{0^|0H`=@aR&yBiW(cN7ufyFmgFx-DYQa z&a~d-lF-#}67NIqbG9-$TkE}td)4ihENTjT4HK2w0hCyjMV;TPY6hNfbk!fX@VH2m zEOfY(Qft0HHwl-k0}#uhujZ3?uz+4KmQJ2l?Y_0YkxF^%S9a$~lO^aeLYk$bhR`LT zpGoku=uDa$%&N$;U;OKB)lBfYW%(LOsZK`cy*mefY(Q{^MsldaTsVqDFAV>Q66r>f z6Kmq3h0yPkR`QjRh^8NFU)=UlS$S4O7os*~nUPBAB6uPKKcLIV8z=(u1b=>x28y#y zN^Bx8B&0ORTs73Y&;tpf1E6@X7ibmU=<=0Fm-MjTQ>92SHgc!<@Ov8JTEErToh9@@m*RvXo@yUa%() z@KO0EGfS>VC}CHnuoG!hkbFIxqFY?BhHV_C!}cQGbod{V+No*Tf~MJY#HIwYm|J5^ zuHyKepbv-Ud4+BT<#cjHP-58;Bgr*-SakILA3iv!dL_DwD2zW5+_)nR1kIW8F6Q!> z-RURNSMnFzehU6RTy+1eU&0d#T6N2$QJ&MVXDxh+^OL3nF29o+mI@wjs?<0!m3RY&@VqaHa+^SY2a9UaL{Q!4%c5usl2_O!$NDoH{V%K?w;00a zX1-UDp{^hn>E-MZol`JK+)gM~>W9ft4HSM&3Rh?WvdnCVm8j{XhC3-@yvu(Z!W8N} z`QbZ7m*n^@m_pzcz3&UXmmWL@cpMyvf~8Ocg(|gH0}k>Io``TPB2;ammU|uEqCf-d z(}Q(8o*d?+Dwh4C+U@eI8TBOymP7)Yqt3Aa5%o?jCx`mOhM*3~__YAbw&_t8X$#1JPnMHO?A8s-Tn353=~0C;|tD4Ga2Gg>it z;RjII77hvdiAO2?HC)8N$$*=FU8n~XR%#C|3L6+maU-D>8!D7a3yh%<55oq3!evV+ z-2rnt&ugeY3`icl4cZV!E*=fZx z0$`ZTC0+;T@onxjjID7mSD7RCuR_GMpV>s;jeestYL&s;vLmDr7Gd62tv(V^L|_L6 zlPqkrm{cL3q-#I~i28SvVjDi4sooovbma7Z2igv==+!ESMt+JWDl=l z8PhQwBn;8YFN@}ROzI%T4zX?XJ1smN)-d0o2sVlMNe=f*)?;i~SkxXx!S|6YsxeW{zxf6@o}=iJ;4dyLMNd$qf}xA7N6TIElpBc zk^{mMcbf-4LRWrYzx0<~@wa*Vs=62IuLfSw`WCJkbaW(eqxePp>PAeic~EE%A~|QZ zP~&{>o>1W@3mlh07B+w8ZU^i>;8uA5K}}{@7o?vnMX2jj47EetBEB5Bg)}UX=jkIX zUt#Pgh^PRousm8Zzwdo*CohDv!sfom6OhL_HUsEj;gXjiHGm|INGN=eKt&U5hzyr% zK;i$C=DrLS_oEX+hJsG1vi!p2Vd0|D!1!j;J61ed%@ktrIO}YXN*4IenEcSLJQGCW zD>$1VFE_4=vZ%73k{m*vr;r5&3p<3#A4U@Zz^qwduDodZEU+Xw*&Or^sa)(MfR8b5Od4RpV>-RoiL%we-etlCitWgqsqhvD0RSFmSYND*BG`vwl z@})n9{$s>1O%ZI~AwC%;8-5jty3#|uf*n>KEA|o53i2ya6&V|8$8}t(ju!Fo+8hxa zHV74>;Ez$Ns8My)ufb&oZ6Mo}$l2sd!9*xqk6s%8uk9(9XC$69|@i4)K0vys>> z97&`fkNylcDjZA6!%H_;&u~%C3{uaESI^E<)w=upp_QqNykFKAJJ={5dnjy2(@ zdNIF7iL6GcmPTd^z%VOZ|CqllUZXNkqpC)uI(*!Q8An}OT{(QT`lm*TI8P(JW)r_= zv#e%|mS(HDW}C}oTMNWFTJv?DW@qW74Fd9fLbGRGv-ec9@AIV1oT6_7R`*ZML0PRK zEv;d5tq~Wk(IBm{c&+g~t%(|~$!@J7FHs!!$bliP*`Hb!>=!TH zn|R%CdAi?gbboZ~{+!bNwXXa7RQL9$E`SgYWPtyK=)R%X#a4r3S-`P3w56Hh&;&Sc zJ{+$Wj^6_(n1&N>z==3`6sV^!ZQ&#gdV1U1MECT_we=`0^eA2RsDkyVYjvnT!xe<* zNP6b61mKfHr%%@xy!TLC`=Nxz!&@LjSlfHBbizaF+Fp8TeSD4qY=;6a zjzw$SrDVCBaWsiP<>DOL~r6;k-87OZ&yl)5>V=&ODHPGx?qD~;EOz!n| zH@I6i$NDZpHeZJ>Vd?&co|d+up@pFl1zd_5){zZ)ly7KVxhSSw%90gE&8)8`v8?WD zXhmpb&0usU4(1;R^A&>WxOr&P3#g_aiW^{GxB%9mC`E*k{@JqHFGHI%qi4TXG}6PU z0)$l1D;^Tzp5F7+=ulZCjq?7?n=qiN1EYw<@Vmhx**tZ!nxl@poizM z9l}6O9R!o_PBFjHI4Sarnd|;y6NmZiul3*v$ZKINLH;soD3)=j&_vmsQ&>*HnU3te z>1mF|87zx;Jr*{@AWpcE*O|pPQ26a??HrdQfsn(Bqn&|n&rILOO2)9oLGY#(gCVM{ zX0K;1-&9t{WaErrxz}*>O~k_62+J#mEraFwlHoZoF;m8|^=op7)y|^{3(FZ-%a65> zf3zAf0mIIF7PWCrch7VPEAr6`I^W7HzV|$a=vYZ;ODzx2i5@n;Lq^n|ZJgW6^jd6y zu(!ZQRz&C9)EisJ-Kmtw$AlZ<@c>iO4>~z^)Yo4&MJyhDa5X0`u%UcH-Rk(X&Nq@}`k5I5dm9>^Cl7w_ zekV7c=m@i4{J`BNmLL%7Z% zg}`B;p+i)N1Erk<#2bDaUFQ(D0$(egUSB_aS91{e+acLaKZVipg`i_~u@V z47a0)ZuITbNF6$?6*+;=L~IWE*0FX+I2 z72;HpcwF&YxxCJ)y4R^@#))IisqWmV{YjOhC*D%5>Jct zPamUzuTz!BXI!Q?UHbhC%u$R?E~+z(t_y;$FR8JRwN~a>8BiQpb~eg`MAj_PNbzPbY4+?v%-@`*Y&6Z|ABib7$8D$+HD_ zJ9D4?K7VFo04~WkcldQG_xTz40ir$Z+`=b+#sTYbj2k2gft%FM4!T%XKo+wgmGclp z7l;#i;dHqNynE^W*9fu)?t3EP*gTd-UI=P#1bLD>%@$0J+1==;+kSyN!9b0RK%o)la~F%t!2O!qE^9flPObzGpue!PF0{ z1(-ZpaS+)K7q@BKkBrzoC0DPc8b0FD3`#unlv%xc?=~be>v?a>^ZteBgIiBIVlR0n zF9jhl#RpzWx?akUy;PogsXq5oOY*|V0F8Pt%|0)!SugD^FP#f7-CHj>vA5psvyT}t z=pAo^(R01WU#cl-E#04)d`)M0>TUXURFF&lho!p()%l%z@5e7ym+k}anAqcmfHoqp z9_#vew}fg8(hE)^tzOyKy}U+wUGLW+Zi;w?4dkQ034QEW zEDFvxxkTFf-NxY5h3Wc5y!4B|MR?p^$6xsEy*?-W>X$;57bbL-^5EMs&^=n!KjV3^ z`|VZ8Gym*93a0j(?7nYijUKk6{skAf^nG6oh`;BJorz?HyHEv`wyS{lZc3j87{DX>W>u=zn?i*8`+8+pvUBK*Z&>V5_JX7$3Q1Ie|;3eJQ<;TG*&w^K<2d^aszj+z_wm$e> zU-0^D@Wxg!?&d}C)@|@2G4xM58Ve5-dB&up%Q5L_K0pNK119`DK@S+G1A+m783y1W z6Bvy_F$H&2KbF{A-Ey3K`!QL+WvYd6B5luuxn}ujrWA zxcG#`q~w$rsouU|Xl8a!ZWaw5u&}7Oq&PC|RYhf0bxmzu{VN|(UUO?(E5pmuu9EVG zK6L-U;Lz}hO;h{CWI^{#Veja|;?nZU>YC1Y*5trBpPXr!hnGKm z`}Kid`H9Pum{@MQFRfLXl}YG6juV4<^xdx-@jP_swm1769atpmQQbMM3n+dNT?WgIKZ z0Sr}g8%GHTQ|~6pfu7*Flb9g$u~TcQt&_DR9pm}ce%qCDaD4y5ob>Yb;a-NhhG^d7 zWqIauu!*C|lc$dXf$>P7mzE8^i$sX6XT4czwrbm#;gz1{grv z0!D*mkCSLKdI{qKFN0!U5HDlco@X z0!Xo5k$}3{a3zWHGT4aa=qaOk>1}vf3k2!@zlHVxK=aGFKWH|s$Dlb-8SyV@Ce&{D zgJzxjcNZ8m&tCkv{e$N4h{&jaMRQtuMrIZU&3X9+FaMyqw5bqvMmzS-_?a zNb$qx56h=F-@Zqle}oedf#mMU0g$c1n2deRB5|w~jvkcO9FwtE7A1Ts8*g&CJ&eiN zD~wYU1BOrx%9xD3K6IRuPEW^0z4^mHrk2h9l*9S*+1r;#2*LTIaIU!$we0twv4Obr zoGPi9kbN~vH|s}i^WX_~-sO5L!<5f}!mvj~7n|R9JBOUt+7a>+dxb@6;RFiz(NC&} zT-W=M0VK)D_X*nxRee^5IGxBWAuGI8-g#z5*cM{QnUKq|Frm7`_}BsiEWlE8W9We8wv^2 z=TOeEAuH{4ED+#|rW|Fd6mDM_f@fdB0=OkHzNNIh)K;@V!5KQ<$3U;eEW+l2+~H4K z9+|L4a<6nS+AWv0W

    #oVhSjAG_iNC66fJBU*&;J4-MgFx2HE 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≺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⩔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%0Ag%5 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 uIJNQwk6});?G`w+>c@6So#@4b&& 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 z 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 E4{jt`Qvd(} literal 0 HcmV?d00001 diff --git a/docs/blog/posts/text-area-learnings.md b/docs/blog/posts/text-area-learnings.md new file mode 100644 index 0000000000..d55a6b96e9 --- /dev/null +++ b/docs/blog/posts/text-area-learnings.md @@ -0,0 +1,210 @@ +--- +draft: false +date: 2023-09-18 +categories: + - DevLog +authors: + - darrenburns +--- + +# Things I learned while building Textual's TextArea + +`TextArea` is the latest widget to be added to Textual's [growing collection](https://textual.textualize.io/widget_gallery/). +It provides a multi-line space to edit text, and features optional syntax highlighting for a selection of languages. + +![text-area-welcome.gif](../images/text-area-learnings/text-area-welcome.gif) + +Adding a `TextArea` to your Textual app is as simple as adding this to your `compose` method: + +```python +yield TextArea() +``` + +Enabling syntax highlighting for a language is as simple as: + +```python +yield TextArea(language="python") +``` + +Working on the `TextArea` widget for Textual taught me a lot about Python and my general +approach to software engineering. It gave me an appreciation for the subtle functionality behind +the editors we use on a daily basis — features we may not even notice, despite +some engineer spending hours perfecting it to provide a small boost to our development experience. + +This post is a tour of some of these learnings. + + + +## Vertical cursor movement is more than just `cursor_row++` + +When you move the cursor vertically, you can't simply keep the same column index and clamp it within the line. +Editors should maintain the visual column offset where possible, +meaning they must account for double-width emoji (sigh 😔) and East-Asian characters. + +![maintain_offset.gif](../images/text-area-learnings/maintain_offset.gif){ loading=lazy } + +Notice that although the cursor is on column 11 while on line 1, it lands on column 6 when it +arrives at line 3. +This is because the 6th character of line 3 _visually_ aligns with the 11th character of line 1. + + +## Edits from other sources may move my cursor + +There are two ways to interact with the `TextArea`: + +1. You can type into it. +2. You can make API calls to edit the content in it. + +In the example below, `Hello, world!\n` is repeatedly inserted at the start of the document via the +API. +Notice that this updates the location of my cursor, ensuring that I don't lose my place. + +![text-area-api-insert.gif](../images/text-area-learnings/text-area-api-insert.gif){ loading=lazy } + +This subtle feature should aid those implementing collaborative and multi-cursor editing. + +This turned out to be one of the more complex features of the whole project, and went through several iterations before I was happy with the result. + +Thankfully it resulted in some wonderful Tetris-esque whiteboards along the way! + +

    + ![cursor_position_updating_via_api.png](../images/text-area-learnings/cursor_position_updating_via_api.png){ loading=lazy } +
    A TetrisArea white-boarding session.
    +
    + +Sometimes stepping away from the screen and scribbling on a whiteboard with your colleagues (thanks [Dave](https://fosstodon.org/@davep)!) is what's needed to finally crack a tough problem. + +Many thanks to [David Brochart](https://mastodon.top/@davidbrochart) for sending me down this rabbit hole! + +## Spending a few minutes running a profiler can be really beneficial + +While building the `TextArea` widget I avoided heavy optimisation work that may have affected +readability or maintainability. + +However, I did run a profiler in an attempt to detect flawed assumptions or mistakes which were +affecting the performance of my code. + +I spent around 30 minutes profiling `TextArea` +using [pyinstrument](https://pyinstrument.readthedocs.io/en/latest/home.html), and the result was a +**~97%** reduction in the time taken to handle a key press. +What an amazing return on investment for such a minimal time commitment! + + +
    + ![text-area-pyinstrument.png](../images/text-area-learnings/text-area-pyinstrument.png){ loading=lazy } +
    "pyinstrument -r html" produces this beautiful output.
    +
    + +pyinstrument unveiled two issues that were massively impacting performance. + +### 1. Reparsing highlighting queries on each key press + +I was constructing a tree-sitter `Query` object on each key press, incorrectly assuming it was a +low-overhead call. +This query was completely static, so I moved it into the constructor ensuring the object was created +only once. +This reduced key processing time by around 94% - a substantial and very much noticeable improvement. + +This seems obvious in hindsight, but the code in question was written earlier in the project and had +been relegated in my mind to "code that works correctly and will receive less attention from here on +out". +pyinstrument quickly brought this code back to my attention and highlighted it as a glaring +performance bug. + +### 2. NamedTuples are slower than I expected + +In Python, `NamedTuple`s are slow to create relative to `tuple`s, and this cost was adding up inside +an extremely hot loop which was instantiating a large number of them. +pyinstrument revealed that a large portion of the time during syntax highlighting was spent inside `NamedTuple.__new__`. + +Here's a quick benchmark which constructs 10,000 `NamedTuple`s: + +```toml +❯ hyperfine -w 2 'python sandbox/darren/make_namedtuples.py' +Benchmark 1: python sandbox/darren/make_namedtuples.py + Time (mean ± σ): 15.9 ms ± 0.5 ms [User: 12.8 ms, System: 2.5 ms] + Range (min … max): 15.2 ms … 18.4 ms 165 runs +``` + +Here's the same benchmark using `tuple` instead: + +```toml +❯ hyperfine -w 2 'python sandbox/darren/make_tuples.py' +Benchmark 1: python sandbox/darren/make_tuples.py + Time (mean ± σ): 9.3 ms ± 0.5 ms [User: 6.8 ms, System: 2.0 ms] + Range (min … max): 8.7 ms … 12.3 ms 256 runs +``` + +Switching to `tuple` resulted in another noticeable increase in responsiveness. +Key-press handling time dropped by almost 50%! +Unfortunately, this change _does_ impact readability. +However, the scope in which these tuples were used was very small, and so I felt it was a worthy trade-off. + + +## Syntax highlighting is very different from what I expected + +In order to support syntax highlighting, we make use of +the [tree-sitter](https://tree-sitter.github.io/tree-sitter/) library, which maintains a syntax tree +representing the structure of our document. + +To perform highlighting, we follow these steps: + +1. The user edits the document. +2. We inform tree-sitter of the location of this edit. +3. tree-sitter intelligently parses only the subset of the document impacted by the change, updating the tree. +4. We run a query against the tree to retrieve ranges of text we wish to highlight. +5. These ranges are mapped to styles (defined by the chosen "theme"). +6. These styles to the appropriate text ranges when rendering the widget. + +
    + ![text-area-theme-cycle.gif](../images/text-area-learnings/text-area-theme-cycle.gif){ loading=lazy } +
    Cycling through a few of the builtin themes.
    +
    + +Another benefit that I didn't consider before working on this project is that tree-sitter +parsers can also be used to highlight syntax errors in a document. +This can be useful in some situations - for example, highlighting mismatched HTML closing tags: + +
    + ![text-area-syntax-error.gif](../images/text-area-learnings/text-area-syntax-error.gif){ loading=lazy } +
    Highlighting mismatched closing HTML tags in red.
    +
    + +Before building this widget, I was oblivious as to how we might approach syntax highlighting. +Without tree-sitter's incremental parsing approach, I'm not sure reasonable performance would have +been feasible. + +## Edits are replacements + +All single-cursor edits can be distilled into a single behaviour: `replace_range`. +This replaces a range of characters with some text. +We can use this one method to easily implement deletion, insertion, and replacement of text. + +- Inserting text is replacing a zero-width range with the text to insert. +- Pressing backspace (delete left) is just replacing the character behind the cursor with an empty + string. +- Selecting text and pressing delete is just replacing the selected text with an empty string. +- Selecting text and pasting is replacing the selected text with some other text. + +This greatly simplified my initial approach, which involved unique implementations for inserting and +deleting. + + +## The line between "text area" and "VSCode in the terminal" + +A project like this has no clear finish line. +There are always new features, optimisations, and refactors waiting to be made. + +So where do we draw the line? + +We want to provide a widget which can act as both a basic multiline text area that +anyone can drop into their app, yet powerful and extensible enough to act as the foundation +for a Textual-powered text editor. + +Yet, the more features we add, the more opinionated the widget becomes, and the less that users +will feel like they can build it into their _own_ thing. +Finding the sweet spot between feature-rich and flexible is no easy task. + +I don't think the answer is clear, and I don't believe it's possible to please everyone. + +Regardless, I'm happy with where we've landed, and I'm really excited to see what people build using `TextArea` in the future! From de25760728b8634cdc45ce8be0b64ba6fa133a94 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 21 Sep 2023 10:39:58 +0100 Subject: [PATCH 361/366] Remove redundant pass --- src/textual/document/_syntax_aware_document.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 2ab76920a5..3fd828ae48 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -16,8 +16,6 @@ class SyntaxAwareDocumentError(Exception): """General error raised when SyntaxAwareDocument is used incorrectly.""" - pass - class SyntaxAwareDocument(Document): """A wrapper around a Document which also maintains a tree-sitter syntax From d4c933d291d26edd4410b6661abbcf57c096fc00 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 21 Sep 2023 10:41:45 +0100 Subject: [PATCH 362/366] Add docstring --- src/textual/widgets/_text_area.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 81a4fbf3b9..8b710477b0 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -72,6 +72,14 @@ class LanguageDoesNotExist(Exception): @dataclass class TextAreaLanguage: + """A container for a language which has been registered with the TextArea. + + Attributes: + name: The name of the language. + language: The tree-sitter Language. + highlight_query: The tree-sitter highlight query corresponding to the language, as a string. + """ + name: str language: "Language" highlight_query: str From cc321f1c0cfaee02dfb7114b63bac4ae6e1d4002 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 21 Sep 2023 10:46:35 +0100 Subject: [PATCH 363/366] Docs fix --- docs/widgets/text_area.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 67c634de18..b0e6fd0101 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -38,7 +38,7 @@ text_area.language = "markdown" ``` !!! note - Syntax highlighting is unavailable on Apple Silicon machines running Python 3.7. + Syntax highlighting is unavailable on Python 3.7. !!! note More built-in languages will be added in the future. For now, you can [add your own](#adding-support-for-custom-languages). From 09f0388ca1201f6d37a33a3a6f6c1e3add9de0de Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 21 Sep 2023 10:47:30 +0100 Subject: [PATCH 364/366] Simplify docs --- docs/widgets/text_area.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index b0e6fd0101..2fddae64eb 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -59,8 +59,7 @@ In all cases, when multiple lines of text are retrieved, the [document line sepa The content of the `TextArea` can be updated using the [`replace`][textual.widgets._text_area.TextArea.replace] method. This method is the programmatic equivalent of selecting some text and then pasting. -All atomic (single-cursor) edits can be represented by a `replace` operation, but for -convenience, some other utility methods are provided, such as [`insert`][textual.widgets._text_area.TextArea.insert], [`delete`][textual.widgets._text_area.TextArea.delete], and [`clear`][textual.widgets._text_area.TextArea.clear]. +Some other convenient methods are available, such as [`insert`][textual.widgets._text_area.TextArea.insert], [`delete`][textual.widgets._text_area.TextArea.delete], and [`clear`][textual.widgets._text_area.TextArea.clear]. ### Working with the cursor From 1de14f4237b4936bde683abf0788bc5186ebc697 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 21 Sep 2023 10:48:36 +0100 Subject: [PATCH 365/366] Improve docstring --- src/textual/_text_area_theme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index 14f9c874aa..2b09f3c733 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -45,7 +45,7 @@ class TextAreaTheme: """The style of the gutter. If `None`, a legible TextAreaStyle will be generated.""" cursor_style: Style | None = None - """The style of the cursor. If `None`, the legible TextAreaStyle will be generated.""" + """The style of the cursor. If `None`, a legible TextAreaStyle will be generated.""" cursor_line_style: Style | None = None """The style to apply to the line the cursor is on.""" From f3ea0fdc80455fb1a7e58cb5421ef5945f21dbb6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 21 Sep 2023 10:57:35 +0100 Subject: [PATCH 366/366] Add links in docstrings --- src/textual/_text_area_theme.py | 6 +++--- src/textual/widgets/_text_area.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index 2b09f3c733..93bad81c85 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -31,7 +31,7 @@ class TextAreaTheme: TextAreaTheme('my_theme', syntax_styles={'heading': Style(color='cyan', bold=True)}) ``` - We can register this theme with our `TextArea` using the `register_theme` method, + We can register this theme with our `TextArea` using the [`TextArea.register_theme`][textual.widgets._text_area.TextArea.register_theme] method, and headings in our markdown files will be styled bold cyan. """ @@ -42,10 +42,10 @@ class TextAreaTheme: """The background style of the text area. If `None` the parent style will be used.""" gutter_style: Style | None = None - """The style of the gutter. If `None`, a legible TextAreaStyle will be generated.""" + """The style of the gutter. If `None`, a legible Style will be generated.""" cursor_style: Style | None = None - """The style of the cursor. If `None`, a legible TextAreaStyle will be generated.""" + """The style of the cursor. If `None`, a legible Style will be generated.""" cursor_line_style: Style | None = None """The style to apply to the line the cursor is on.""" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 8b710477b0..f40478f088 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -192,13 +192,13 @@ class TextArea(ScrollView, can_focus=True): If the value is a string, a built-in language parser will be used if available. If you wish to use an unsupported language, you'll have to register - it first using `register_language`. + it first using [`TextArea.register_language`][textual.widgets._text_area.TextArea.register_language]. """ theme: Reactive[str | None] = reactive(None, always_update=True, init=False) """The name of the theme to use. - Themes must be registered using `register_theme` before they can be used. + Themes must be registered using [`TextArea.register_theme`][textual.widgets._text_area.TextArea.register_theme] before they can be used. Syntax highlighting is only possible when the `language` attribute is set. """