From d50f8bcbcc910e7a50833d0acec608cc9140844d Mon Sep 17 00:00:00 2001 From: Paul Ollis Date: Tue, 11 Mar 2025 17:01:56 +0000 Subject: [PATCH 1/9] Make syntax highlight generation scale better. The TextArea widget code queries the entires syntax tree for each edit, using the tree-sitter Query.captures method. This has potential scaling issues, but such issues are exacerbated by the fact the Query.captures method scales very poorly with the number of line it is asked to generate captures for. It appears to be quadratic or something similar I think - I strongly suspect a bug in tree-sitter or its python bindings. On my laptop, this makes editing a 25,000 line Python file painfully unresponsive. A 25,000 lines Python file is large, but not entirely unreasonable. I actually became aware of this behaviour developing a LaTeX editor, which showed symptoms after about 1,000 lines. This change replaces the plain TextArea._highlights dictionary with a dictonary-like class that lazily performs item access to build hilghlight information for small blocks of (50) lines at a time. As well as keeping the TextArea very much more responsive, it will reduce the average memory requirements for larger documents. During regression testing, I discovered that the per-line highlights are not necessarily in the correct (best) order for syntax highlighting. For example, highlighting within string expressions can lost. So I added suitable sorting. This required that the snapshots for some tests to be updated. --- CHANGELOG.md | 11 + .../document/_syntax_aware_document.py | 42 +- src/textual/widgets/_text_area.py | 136 +++++-- ...test_text_area_language_rendering[css].svg | 320 +++++++-------- ...xt_area_language_rendering[javascript].svg | 362 ++++++++--------- ...text_area_language_rendering[markdown].svg | 317 ++++++++------- ...t_text_area_language_rendering[python].svg | 366 +++++++++--------- ...test_text_area_language_rendering[xml].svg | 120 +++--- tests/text_area/test_languages.py | 4 +- 9 files changed, 894 insertions(+), 784 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 115a20b91c..b7d0a81da5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `Style.has_transparent_foreground` property https://github.com/Textualize/textual/pull/5657 +### Fixed + +- Fixed TextArea's syntax highlighting. Some highlighting details were not being + applied. For example, in CSS, the text 'padding: 10px 0;' was shown in a + single colour. Now the 'px' appears in a different colour to the rest of the + text. + +- Fixed a cause of slow editing for syntax highlighed TextArea widgets with + large documents. + + ## [2.1.2] - 2025-02-26 ### Fixed diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 00305dcec2..6602b08ff6 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -1,5 +1,8 @@ from __future__ import annotations +from contextlib import contextmanager +from typing import ContextManager + try: from tree_sitter import Language, Node, Parser, Query, Tree @@ -11,6 +14,35 @@ from textual.document._document import Document, EditResult, Location, _utf8_encode +@contextmanager +def temporary_query_point_range( + query: Query, + start_point: tuple[int, int] | None, + end_point: tuple[int, int] | None, +) -> ContextManager[None]: + """Temporarily change the start and/or end point for a tree-sitter Query. + + Args: + query: The tree-sitter Query. + start_point: The (row, column byte) to start the query at. + end_point: The (row, column byte) to end the query at. + """ + # Note: Although not documented for the tree-sitter Python API, an + # end-point of (0, 0) means 'end of document'. + default_point_range = [(0, 0), (0, 0)] + + point_range = list(default_point_range) + if start_point is not None: + point_range[0] = start_point + if end_point is not None: + point_range[1] = end_point + query.set_point_range(point_range) + try: + yield None + finally: + query.set_point_range(default_point_range) + + class SyntaxAwareDocumentError(Exception): """General error raised when SyntaxAwareDocument is used incorrectly.""" @@ -82,14 +114,8 @@ def query_syntax_tree( Returns: A tuple containing the nodes and text captured by the query. """ - 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) - return captures + with temporary_query_point_range(query, start_point, end_point): + return query.captures(self._syntax_tree.root_node) 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 7c16716ed5..ecbd14ad59 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -90,6 +90,105 @@ class LanguageDoesNotExist(Exception): """ +class HighlightMap: + """Lazy evaluated pseudo dictionary mapping lines to highlight information. + + This allows TextArea syntax highlighting to scale. + + Args: + text_area_widget: The associated `TextArea` widget. + """ + + BLOCK_SIZE = 50 + + def __init__(self, text_area_widget: widgets.TextArea): + self.text_area_widget: widgets.TextArea = text_area_widget + self.uncovered_lines: dict[int, range] = {} + + # A mapping from line index to a list of Highlight instances. + self._highlights: LineToHighlightsMap = defaultdict(list) + self.reset() + + def reset(self) -> None: + """Reset so that future lookups rebuild the highlight map.""" + self._highlights.clear() + line_count = self.document.line_count + uncovered_lines = self.uncovered_lines + uncovered_lines.clear() + i = end_range = 0 + for i in range(0, line_count, self.BLOCK_SIZE): + end_range = min(i + self.BLOCK_SIZE, line_count) + line_range = range(i, end_range) + uncovered_lines.update({j: line_range for j in line_range}) + if end_range < line_count: + line_range = range(i, line_count) + uncovered_lines.update({j: line_range for j in line_range}) + + @property + def document(self) -> DocumentBase: + """The text document being highlighted.""" + return self.text_area_widget.document + + def __getitem__(self, idx: int) -> list[text_area.Highlight]: + if idx in self.uncovered_lines: + self._build_part_of_highlight_map(self.uncovered_lines[idx]) + return self._highlights[idx] + + def _build_part_of_highlight_map(self, line_range: range) -> None: + """Build part of the highlight map.""" + highlights = self._highlights + for line_index in line_range: + self.uncovered_lines.pop(line_index) + start_point = (line_range[0], 0) + end_point = (line_range[-1] + 1, 0) + captures = self.document.query_syntax_tree( + self.text_area_widget._highlight_query, + start_point=start_point, + end_point=end_point, + ) + for highlight_name, nodes in captures.items(): + for node in nodes: + 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) + ) + + # The highlights for each line need to be sorted. Each highlight is of + # the form: + # + # a, b, highlight-name + # + # Where a is a number and b is a number or ``None``. These highlights need + # to be sorted in ascending order of ``a``. When two highlights have the same + # value of ``a`` then the one with the larger a--b range comes first, with ``None`` + # being considered larger than any number. + def sort_key(hl) -> tuple[int, int, int]: + a, b, _ = hl + max_range_ind = 1 + if b is None: + max_range_ind = 0 + b = a + return a, max_range_ind, a - b + + for line_index in line_range: + line_highlights = highlights.get(line_index, []).sort(key=sort_key) + + @dataclass class TextAreaLanguage: """A container for a language which has been registered with the TextArea.""" @@ -473,15 +572,15 @@ 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.""" self.document: DocumentBase = Document(text) """The document this widget is currently editing.""" + self._highlights: HighlightMap = HighlightMap(self) + """Mapping line numbers to the set of highlights for that line.""" + self.wrapped_document: WrappedDocument = WrappedDocument(self.document) """The wrapped view of the document.""" @@ -610,35 +709,10 @@ def check_consume_key(self, key: str, character: str | None = None) -> bool: return character is not None and character.isprintable() 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 highlight_name, nodes in captures.items(): - for node in nodes: - 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) - ) + """Reset the lazily evaluated highlight map.""" - # 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) - ) + if self._highlight_query: + self._highlights.reset() def _watch_has_focus(self, focus: bool) -> None: self._cursor_visible = focus diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[css].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[css].svg index 44ac0c0411..a49c38524e 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[css].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[css].svg @@ -19,330 +19,330 @@ font-weight: 700; } - .terminal-2526263208-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2526263208-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2526263208-r1 { fill: #121212 } -.terminal-2526263208-r2 { fill: #0178d4 } -.terminal-2526263208-r3 { fill: #c5c8c6 } -.terminal-2526263208-r4 { fill: #c2c2bf } -.terminal-2526263208-r5 { fill: #272822 } -.terminal-2526263208-r6 { fill: #75715e } -.terminal-2526263208-r7 { fill: #f8f8f2 } -.terminal-2526263208-r8 { fill: #90908a } -.terminal-2526263208-r9 { fill: #a6e22e } -.terminal-2526263208-r10 { fill: #ae81ff } -.terminal-2526263208-r11 { fill: #e6db74 } -.terminal-2526263208-r12 { fill: #f92672 } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #c2c2bf } +.terminal-r5 { fill: #272822 } +.terminal-r6 { fill: #75715e } +.terminal-r7 { fill: #f8f8f2 } +.terminal-r8 { fill: #90908a } +.terminal-r9 { fill: #a6e22e } +.terminal-r10 { fill: #ae81ff } +.terminal-r11 { fill: #e6db74 } +.terminal-r12 { fill: #f92672 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - 1  /* This is a comment in CSS */ - 2   - 3  /* Basic selectors and properties */ - 4  body {                                                                   - 5  font-familyArialsans-serif;                                      - 6  background-color#f4f4f4;                                           - 7  margin0;                                                           - 8  padding0;                                                          - 9  }                                                                        -10   -11  /* Class and ID selectors */ -12  .header {                                                                -13  background-color#333;                                              -14  color#fff;                                                         -15  padding10px0;                                                     -16  text-aligncenter;                                                  -17  }                                                                        -18   -19  #logo {                                                                  -20  font-size24px;                                                     -21  font-weightbold;                                                   -22  }                                                                        -23   -24  /* Descendant and child selectors */ -25  .navul {                                                                -26  list-style-typenone;                                               -27  padding0;                                                          -28  }                                                                        -29   -30  .nav > li {                                                              -31  displayinline-block;                                               -32  margin-right10px;                                                  -33  }                                                                        -34   -35  /* Pseudo-classes */ -36  a:hover {                                                                -37  text-decorationunderline;                                          -38  }                                                                        -39   -40  input:focus {                                                            -41  border-color#007BFF;                                               -42  }                                                                        -43   -44  /* Media query */ -45  @media (max-width768px) {                                              -46  body {                                                               -47  font-size16px;                                                 -48      }                                                                    -49   -50      .header {                                                            -51  padding5px0;                                                  -52      }                                                                    -53  }                                                                        -54   -55  /* Keyframes animation */ -56  @keyframes slideIn {                                                     -57  from {                                                               -58  transformtranslateX(-100%);                                    -59      }                                                                    -60  to {                                                                 -61  transformtranslateX(0);                                        -62      }                                                                    -63  }                                                                        -64   -65  .slide-in-element {                                                      -66  animationslideIn0.5sforwards;                                    -67  }                                                                        -68   - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 1  /* This is a comment in CSS */ + 2   + 3  /* Basic selectors and properties */ + 4  body {                                                                   + 5  font-familyArialsans-serif;                                      + 6  background-color: #f4f4f4;                                           + 7  margin0;                                                           + 8  padding0;                                                          + 9  }                                                                        +10   +11  /* Class and ID selectors */ +12  .header {                                                                +13  background-color: #333;                                              +14  color: #fff;                                                         +15  padding10px0;                                                     +16  text-aligncenter;                                                  +17  }                                                                        +18   +19  #logo {                                                                  +20  font-size24px;                                                     +21  font-weightbold;                                                   +22  }                                                                        +23   +24  /* Descendant and child selectors */ +25  .navul {                                                                +26  list-style-typenone;                                               +27  padding0;                                                          +28  }                                                                        +29   +30  .nav > li {                                                              +31  displayinline-block;                                               +32  margin-right10px;                                                  +33  }                                                                        +34   +35  /* Pseudo-classes */ +36  a:hover {                                                                +37  text-decorationunderline;                                          +38  }                                                                        +39   +40  input:focus {                                                            +41  border-color: #007BFF;                                               +42  }                                                                        +43   +44  /* Media query */ +45  @media (max-width768px) {                                              +46  body {                                                               +47  font-size16px;                                                 +48      }                                                                    +49   +50      .header {                                                            +51  padding5px0;                                                  +52      }                                                                    +53  }                                                                        +54   +55  /* Keyframes animation */ +56  @keyframes slideIn {                                                     +57  from {                                                               +58  transformtranslateX(-100%);                                    +59      }                                                                    +60  to {                                                                 +61  transformtranslateX(0);                                        +62      }                                                                    +63  }                                                                        +64   +65  .slide-in-element {                                                      +66  animationslideIn0.5sforwards;                                    +67  }                                                                        +68   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[javascript].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[javascript].svg index 645ea326fa..77ae9609f9 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[javascript].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[javascript].svg @@ -19,371 +19,371 @@ font-weight: 700; } - .terminal-2506662657-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2506662657-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2506662657-r1 { fill: #121212 } -.terminal-2506662657-r2 { fill: #0178d4 } -.terminal-2506662657-r3 { fill: #c5c8c6 } -.terminal-2506662657-r4 { fill: #c2c2bf } -.terminal-2506662657-r5 { fill: #272822 } -.terminal-2506662657-r6 { fill: #75715e } -.terminal-2506662657-r7 { fill: #f8f8f2 } -.terminal-2506662657-r8 { fill: #90908a } -.terminal-2506662657-r9 { fill: #f92672 } -.terminal-2506662657-r10 { fill: #e6db74 } -.terminal-2506662657-r11 { fill: #ae81ff } -.terminal-2506662657-r12 { fill: #66d9ef;font-style: italic; } -.terminal-2506662657-r13 { fill: #a6e22e } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #c2c2bf } +.terminal-r5 { fill: #272822 } +.terminal-r6 { fill: #75715e } +.terminal-r7 { fill: #f8f8f2 } +.terminal-r8 { fill: #90908a } +.terminal-r9 { fill: #f92672 } +.terminal-r10 { fill: #e6db74 } +.terminal-r11 { fill: #ae81ff } +.terminal-r12 { fill: #66d9ef;font-style: italic; } +.terminal-r13 { fill: #a6e22e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - 1  // Variable declarations - 2  const name = "John";                                                     - 3  let age = 30;                                                            - 4  var isStudent = true;                                                    - 5   - 6  // Template literals - 7  console.log(`Hello, ${name}! You are ${age} years old.`);                - 8   - 9  // Conditional statements -10  if (age >= 18 && isStudent) {                                            -11    console.log("You are an adult student.");                              -12  elseif (age >= 18) {                                                  -13    console.log("You are an adult.");                                      -14  else {                                                                 -15    console.log("You are a minor.");                                       -16  }                                                                        -17   -18  // Arrays and array methods -19  const numbers = [12345];                                         -20  const doubledNumbers = numbers.map((num) => num * 2);                    -21  console.log("Doubled numbers:", doubledNumbers);                         -22   -23  // Objects -24  const person = {                                                         -25    firstName: "John",                                                     -26    lastName: "Doe",                                                       -27    getFullName() {                                                        -28  return`${this.firstName}${this.lastName}`;                         -29    },                                                                     -30  };                                                                       -31  console.log("Full name:", person.getFullName());                         -32   -33  // Classes -34  class Rectangle {                                                        -35    constructor(width, height) {                                           -36      this.width = width;                                                  -37      this.height = height;                                                -38    }                                                                      -39   -40    getArea() {                                                            -41  return this.width * this.height;                                     -42    }                                                                      -43  }                                                                        -44  const rectangle = new Rectangle(53);                                   -45  console.log("Rectangle area:", rectangle.getArea());                     -46   -47  // Async/Await and Promises -48  asyncfunctionfetchData() {                                             -49  try {                                                                  -50  const response = awaitfetch("https://api.example.com/data");        -51  const data = await response.json();                                  -52      console.log("Fetched data:", data);                                  -53    } catch (error) {                                                      -54      console.error("Error:", error);                                      -55    }                                                                      -56  }                                                                        -57  fetchData();                                                             -58   -59  // Arrow functions -60  constgreet = (name) => {                                                -61    console.log(`Hello, ${name}!`);                                        -62  };                                                                       -63  greet("Alice");                                                          -64   -65  // Destructuring assignment -66  const [a, b, ...rest] = [12345];                                 -67  console.log(a, b, rest);                                                 -68   -69  // Spread operator -70  const arr1 = [123];                                                  -71  const arr2 = [456];                                                  -72  const combinedArr = [...arr1, ...arr2];                                  -73  console.log("Combined array:", combinedArr);                             -74   -75  // Ternary operator -76  const message = age >= 18 ? "You are an adult." : "You are a minor.";    -77  console.log(message);                                                    -78   - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 1  // Variable declarations + 2  const name = "John";                                                     + 3  let age = 30;                                                            + 4  var isStudent = true;                                                    + 5   + 6  // Template literals + 7  console.log(`Hello, ${name}! You are ${age} years old.`);                + 8   + 9  // Conditional statements +10  if (age >= 18 && isStudent) {                                            +11    console.log("You are an adult student.");                              +12  elseif (age >= 18) {                                                  +13    console.log("You are an adult.");                                      +14  else {                                                                 +15    console.log("You are a minor.");                                       +16  }                                                                        +17   +18  // Arrays and array methods +19  const numbers = [12345];                                         +20  const doubledNumbers = numbers.map((num) => num * 2);                    +21  console.log("Doubled numbers:", doubledNumbers);                         +22   +23  // Objects +24  const person = {                                                         +25    firstName: "John",                                                     +26    lastName: "Doe",                                                       +27    getFullName() {                                                        +28  return`${this.firstName}${this.lastName}`;                         +29    },                                                                     +30  };                                                                       +31  console.log("Full name:", person.getFullName());                         +32   +33  // Classes +34  class Rectangle {                                                        +35    constructor(width, height) {                                           +36      this.width = width;                                                  +37      this.height = height;                                                +38    }                                                                      +39   +40    getArea() {                                                            +41  return this.width * this.height;                                     +42    }                                                                      +43  }                                                                        +44  const rectangle = new Rectangle(53);                                   +45  console.log("Rectangle area:", rectangle.getArea());                     +46   +47  // Async/Await and Promises +48  asyncfunctionfetchData() {                                             +49  try {                                                                  +50  const response = awaitfetch("https://api.example.com/data");        +51  const data = await response.json();                                  +52      console.log("Fetched data:", data);                                  +53    } catch (error) {                                                      +54      console.error("Error:", error);                                      +55    }                                                                      +56  }                                                                        +57  fetchData();                                                             +58   +59  // Arrow functions +60  constgreet = (name) => {                                                +61    console.log(`Hello, ${name}!`);                                        +62  };                                                                       +63  greet("Alice");                                                          +64   +65  // Destructuring assignment +66  const [a, b, ...rest] = [12345];                                 +67  console.log(a, b, rest);                                                 +68   +69  // Spread operator +70  const arr1 = [123];                                                  +71  const arr2 = [456];                                                  +72  const combinedArr = [...arr1, ...arr2];                                  +73  console.log("Combined array:", combinedArr);                             +74   +75  // Ternary operator +76  const message = age >= 18 ? "You are an adult." : "You are a minor.";    +77  console.log(message);                                                    +78   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[markdown].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[markdown].svg index 5cf7309fde..95053a32f0 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[markdown].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[markdown].svg @@ -19,329 +19,328 @@ font-weight: 700; } - .terminal-1784849415-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1784849415-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1784849415-r1 { fill: #121212 } -.terminal-1784849415-r2 { fill: #0178d4 } -.terminal-1784849415-r3 { fill: #c5c8c6 } -.terminal-1784849415-r4 { fill: #c2c2bf } -.terminal-1784849415-r5 { fill: #272822;font-weight: bold } -.terminal-1784849415-r6 { fill: #f92672;font-weight: bold } -.terminal-1784849415-r7 { fill: #f8f8f2 } -.terminal-1784849415-r8 { fill: #90908a } -.terminal-1784849415-r9 { fill: #90908a;font-weight: bold } -.terminal-1784849415-r10 { fill: #272822 } -.terminal-1784849415-r11 { fill: #003054 } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #c2c2bf } +.terminal-r5 { fill: #272822;font-weight: bold } +.terminal-r6 { fill: #f92672;font-weight: bold } +.terminal-r7 { fill: #f8f8f2 } +.terminal-r8 { fill: #90908a } +.terminal-r9 { fill: #272822 } +.terminal-r10 { fill: #003054 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - 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. -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. +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/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[python].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[python].svg index 92ffdc54f9..dcc7124808 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[python].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[python].svg @@ -19,375 +19,375 @@ font-weight: 700; } - .terminal-202856356-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-202856356-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-202856356-r1 { fill: #121212 } -.terminal-202856356-r2 { fill: #0178d4 } -.terminal-202856356-r3 { fill: #c5c8c6 } -.terminal-202856356-r4 { fill: #c2c2bf } -.terminal-202856356-r5 { fill: #272822 } -.terminal-202856356-r6 { fill: #f92672 } -.terminal-202856356-r7 { fill: #f8f8f2 } -.terminal-202856356-r8 { fill: #90908a } -.terminal-202856356-r9 { fill: #75715e } -.terminal-202856356-r10 { fill: #e6db74 } -.terminal-202856356-r11 { fill: #ae81ff } -.terminal-202856356-r12 { fill: #a6e22e } -.terminal-202856356-r13 { fill: #003054 } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #c2c2bf } +.terminal-r5 { fill: #272822 } +.terminal-r6 { fill: #f92672 } +.terminal-r7 { fill: #f8f8f2 } +.terminal-r8 { fill: #90908a } +.terminal-r9 { fill: #75715e } +.terminal-r10 { fill: #e6db74 } +.terminal-r11 { fill: #ae81ff } +.terminal-r12 { fill: #a6e22e } +.terminal-r13 { fill: #003054 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - 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   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[xml].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[xml].svg index 7d9ce1aeb3..31f74b433e 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[xml].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[xml].svg @@ -19,130 +19,130 @@ font-weight: 700; } - .terminal-1843935949-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1843935949-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1843935949-r1 { fill: #121212 } -.terminal-1843935949-r2 { fill: #0178d4 } -.terminal-1843935949-r3 { fill: #c5c8c6 } -.terminal-1843935949-r4 { fill: #c2c2bf } -.terminal-1843935949-r5 { fill: #272822 } -.terminal-1843935949-r6 { fill: #f8f8f2 } -.terminal-1843935949-r7 { fill: #f92672 } -.terminal-1843935949-r8 { fill: #ae81ff } -.terminal-1843935949-r9 { fill: #90908a } -.terminal-1843935949-r10 { fill: #75715e } -.terminal-1843935949-r11 { fill: #e6db74 } -.terminal-1843935949-r12 { fill: #003054 } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #c2c2bf } +.terminal-r5 { fill: #272822 } +.terminal-r6 { fill: #f8f8f2 } +.terminal-r7 { fill: #f92672 } +.terminal-r8 { fill: #ae81ff } +.terminal-r9 { fill: #90908a } +.terminal-r10 { fill: #75715e } +.terminal-r11 { fill: #e6db74 } +.terminal-r12 { fill: #003054 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - 1  <?xml version="1.0" encoding="UTF-8"?>                                   - 2  <!-- This is an example XML document --> - 3  <library>                                                                - 4      <book id="1" genre="fiction">                                        - 5          <title>The Great Gatsby</title>                                  - 6          <author>F. Scott Fitzgerald</author>                             - 7          <published>1925</published>                                      - 8          <description><![CDATA[This classic novel explores themes of weal - 9      </book>                                                              -10      <book id="2" genre="non-fiction">                                    -11          <title>Sapiens: A Brief History of Humankind</title>             -12          <author>Yuval Noah Harari</author>                               -13          <published>2011</published>                                      -14          <description><![CDATA[Explores the history and impact of Homo sa -15      </book>                                                              -16  <!-- Another book can be added here --> -17  </library>                                                               -18   - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 1  <?xml version="1.0" encoding="UTF-8"?>                                   + 2  <!-- This is an example XML document --> + 3  <library>                                                                + 4      <book id="1" genre="fiction">                                        + 5          <title>The Great Gatsby</title>                                  + 6          <author>F. Scott Fitzgerald</author>                             + 7          <published>1925</published>                                      + 8          <description><![CDATA[This classic novel explores themes of weal + 9      </book>                                                              +10      <book id="2" genre="non-fiction">                                    +11          <title>Sapiens: A Brief History of Humankind</title>             +12          <author>Yuval Noah Harari</author>                               +13          <published>2011</published>                                      +14          <description><![CDATA[Explores the history and impact of Homo sa +15      </book>                                                              +16  <!-- Another book can be added here --> +17  </library>                                                               +18   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/text_area/test_languages.py b/tests/text_area/test_languages.py index f5f381354f..d1bdc54de6 100644 --- a/tests/text_area/test_languages.py +++ b/tests/text_area/test_languages.py @@ -86,10 +86,10 @@ async def test_update_highlight_query(): text_area = app.query_one(TextArea) # Before registering the language, we have highlights as expected. - assert len(text_area._highlights) > 0 + assert len(text_area._highlights[0]) > 0 # Overwriting the highlight query for Python... text_area.update_highlight_query("python", "") # We've overridden the highlight query with a blank one, so there are no highlights. - assert text_area._highlights == {} + assert len(text_area._highlights[0]) == 0 From 603ba67075988effe26004352f64ee1dc3f7e8b3 Mon Sep 17 00:00:00 2001 From: Paul Ollis Date: Wed, 12 Mar 2025 21:32:37 +0000 Subject: [PATCH 2/9] Update with suggestions by Darren Burns. --- src/textual/widgets/_text_area.py | 84 +++++++++++++++---------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index ecbd14ad59..8d14958335 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -101,48 +101,47 @@ class HighlightMap: BLOCK_SIZE = 50 - def __init__(self, text_area_widget: widgets.TextArea): - self.text_area_widget: widgets.TextArea = text_area_widget - self.uncovered_lines: dict[int, range] = {} + def __init__(self, text_area: TextArea): + self.text_area: TextArea = text_area + """The text area associated with this highlight map.""" - # A mapping from line index to a list of Highlight instances. - self._highlights: LineToHighlightsMap = defaultdict(list) - self.reset() + self._highlighted_blocks: set[int] = set() + """The set of blocks that have been highlighted. Each block covers BLOCK_SIZE + lines. + """ + + self._highlights: dict[int, list[Highlight]] = defaultdict(list) + """A mapping from line index to a list of Highlight instances.""" def reset(self) -> None: """Reset so that future lookups rebuild the highlight map.""" self._highlights.clear() - line_count = self.document.line_count - uncovered_lines = self.uncovered_lines - uncovered_lines.clear() - i = end_range = 0 - for i in range(0, line_count, self.BLOCK_SIZE): - end_range = min(i + self.BLOCK_SIZE, line_count) - line_range = range(i, end_range) - uncovered_lines.update({j: line_range for j in line_range}) - if end_range < line_count: - line_range = range(i, line_count) - uncovered_lines.update({j: line_range for j in line_range}) + self._highlighted_blocks.clear() @property def document(self) -> DocumentBase: """The text document being highlighted.""" - return self.text_area_widget.document + return self.text_area.document + + def __getitem__(self, index: int) -> list[Highlight]: + block_index = index // self.BLOCK_SIZE + if block_index not in self._highlighted_blocks: + self._highlighted_blocks.add(block_index) + self._build_part_of_highlight_map(block_index * self.BLOCK_SIZE) + return self._highlights[index] - def __getitem__(self, idx: int) -> list[text_area.Highlight]: - if idx in self.uncovered_lines: - self._build_part_of_highlight_map(self.uncovered_lines[idx]) - return self._highlights[idx] + def _build_part_of_highlight_map(self, start_index: int) -> None: + """Build part of the highlight map. - def _build_part_of_highlight_map(self, line_range: range) -> None: - """Build part of the highlight map.""" + Args: + start_index: The start of the block of line for which to build the map. + """ highlights = self._highlights - for line_index in line_range: - self.uncovered_lines.pop(line_index) - start_point = (line_range[0], 0) - end_point = (line_range[-1] + 1, 0) + start_point = (start_index, 0) + end_index = min(self.document.line_count, start_index + self.BLOCK_SIZE) + end_point = (end_index, 0) captures = self.document.query_syntax_tree( - self.text_area_widget._highlight_query, + self.text_area._highlight_query, start_point=start_point, end_point=end_point, ) @@ -160,8 +159,9 @@ def _build_part_of_highlight_map(self, line_range: range) -> None: ) # Add the middle lines - entire row of this node is highlighted + middle_highlight = (0, None, highlight_name) for node_row in range(node_start_row + 1, node_end_row): - highlights[node_row].append((0, None, highlight_name)) + highlights[node_row].append(middle_highlight) # Add the last line of the node range highlights[node_end_row].append( @@ -177,16 +177,16 @@ def _build_part_of_highlight_map(self, line_range: range) -> None: # to be sorted in ascending order of ``a``. When two highlights have the same # value of ``a`` then the one with the larger a--b range comes first, with ``None`` # being considered larger than any number. - def sort_key(hl) -> tuple[int, int, int]: - a, b, _ = hl - max_range_ind = 1 + def sort_key(highlight: Highlight) -> tuple[int, int, int]: + a, b, _ = highlight + max_range_index = 1 if b is None: - max_range_ind = 0 + max_range_index = 0 b = a - return a, max_range_ind, a - b + return a, max_range_index, a - b - for line_index in line_range: - line_highlights = highlights.get(line_index, []).sort(key=sort_key) + for line_index in range(start_index, end_index): + highlights.get(line_index, []).sort(key=sort_key) @dataclass @@ -708,7 +708,7 @@ def check_consume_key(self, key: str, character: str | None = None) -> bool: # Otherwise we capture all printable keys return character is not None and character.isprintable() - def _build_highlight_map(self) -> None: + def _reset_highlights(self) -> None: """Reset the lazily evaluated highlight map.""" if self._highlight_query: @@ -1034,7 +1034,7 @@ def _set_document(self, text: str, language: str | None) -> None: self.document = document self.wrapped_document = WrappedDocument(document, tab_width=self.indent_width) self.navigator = DocumentNavigator(self.wrapped_document) - self._build_highlight_map() + self._reset_highlights() self.move_cursor((0, 0)) self._rewrap_and_refresh_virtual_size() @@ -1447,7 +1447,7 @@ def edit(self, edit: Edit) -> EditResult: self._refresh_size() edit.after(self) - self._build_highlight_map() + self._reset_highlights() self.post_message(self.Changed(self)) return result @@ -1510,7 +1510,7 @@ def _undo_batch(self, edits: Sequence[Edit]) -> None: self._refresh_size() for edit in reversed(edits): edit.after(self) - self._build_highlight_map() + self._reset_highlights() self.post_message(self.Changed(self)) def _redo_batch(self, edits: Sequence[Edit]) -> None: @@ -1558,7 +1558,7 @@ def _redo_batch(self, edits: Sequence[Edit]) -> None: self._refresh_size() for edit in edits: edit.after(self) - self._build_highlight_map() + self._reset_highlights() self.post_message(self.Changed(self)) async def _on_key(self, event: events.Key) -> None: From 4097c24933ba8c196f40d6565b7f9bcbea74d802 Mon Sep 17 00:00:00 2001 From: Paul Ollis Date: Wed, 12 Mar 2025 18:55:35 +0000 Subject: [PATCH 3/9] Run tree-sitter parsing as a background task. Tree-sitter's incremental parsing is not always fast enough to be executed for every editing keystroke without providing a very poor user experience. For example: ```python """Docstring with closing quotes not yet added import textual ... ``` Typing into the above docstring can become very slow. For a 25,000 line Python file on my laptop, each change causes a reparse time of about 0.2 - 0.3 seconds: editing is painful. This change decouples incremental parsing from TextArea edits, using tree-sitter's timeout mechanism and a task to effectivley run parsing in the background, on a snapshot of the TextAreas's contents. While parsing is in progress, editing of the TextArea text continues in a responsive manner. Edits to the tree-siiter parse tree are buffered until the background parser is able to process them, while edits to the displayed text are applied as they occur. --- CHANGELOG.md | 4 +- src/textual/document/_document.py | 29 ++- .../document/_syntax_aware_document.py | 198 ++++++++++++++++-- src/textual/widgets/_text_area.py | 38 +++- tests/snapshot_tests/test_snapshots.py | 32 +-- 5 files changed, 261 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d0a81da5..e90a41c92d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,8 +90,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). single colour. Now the 'px' appears in a different colour to the rest of the text. -- Fixed a cause of slow editing for syntax highlighed TextArea widgets with - large documents. +- Fixed some situations where editing for syntax highlighed TextArea widgets with + large documents was very unresponsive. ## [2.1.2] - 2025-02-26 diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 47e87eb09b..97e9bb2568 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 TYPE_CHECKING, NamedTuple, Tuple, overload +from typing import TYPE_CHECKING, Callable, NamedTuple, Tuple, overload from typing_extensions import Literal, get_args @@ -140,6 +140,12 @@ def get_size(self, indent_width: int) -> Size: The Size of the document bounding box. """ + def clean_up(self) -> None: + """Perform any pre-deletion clean up. + + The default implementation does nothing. + """ + def query_syntax_tree( self, query: "Query", @@ -162,6 +168,27 @@ def query_syntax_tree( """ return {} + def set_syntax_tree_update_callback( + callback: Callable[[], None], + ) -> None: + """Set a callback function for signalling a rebuild of the syntax tree. + + The default implementation does nothing. + + Args: + callback: A function that takes no arguments and returns None. + """ + + def trigger_syntax_tree_update(self, force_update: bool = False) -> None: + """Trigger a new syntax tree update to run in the background. + + The default implementation does nothing. + + Args: + force_update: When set, ensure that the syntax tree is regenerated + unconditionally. + """ + def prepare_query(self, query: str) -> "Query | None": return None diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 6602b08ff6..849f22b54d 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -1,7 +1,10 @@ from __future__ import annotations +import weakref +from asyncio import CancelledError, Event, Task, create_task, sleep from contextlib import contextmanager -from typing import ContextManager +from functools import partial +from typing import Callable, ContextManager, NamedTuple try: from tree_sitter import Language, Node, Parser, Query, Tree @@ -43,6 +46,17 @@ def temporary_query_point_range( query.set_point_range(default_point_range) +class SyntaxTreeEdit(NamedTuple): + """Details of a tree-sitter syntax tree edit operation.""" + + start_byte: int + old_end_byte: int + new_end_byte: int + start_point: int + old_end_point: int + new_end_point: int + + class SyntaxAwareDocumentError(Exception): """General error raised when SyntaxAwareDocument is used incorrectly.""" @@ -76,9 +90,37 @@ def __init__( self._parser = Parser(self.language) """The tree-sitter Parser or None if tree-sitter is unavailable.""" - self._syntax_tree: Tree = self._parser.parse(self._read_callable) # type: ignore + self._syntax_tree: Tree = self._parser.parse( + partial(self._read_callable, lines=self.lines) + ) # type: ignore """The tree-sitter Tree (syntax tree) built from the document.""" + self._syntax_tree_update_callback: Callable[[], None] | None = None + self._background_parser = BackgroundSyntaxParser(self) + self._pending_syntax_edits: list[SyntaxTreeEdit] = [] + + def clean_up(self) -> None: + """Perform any pre-deletion clean up.""" + self._background_parser.stop() + + def copy_of_lines(self): + """Provide a copy of the document's lines.""" + return list(self._lines) + + def apply_pending_syntax_edits(self) -> bool: + """Apply any pending edits to the syntax tree. + + Returns: + True if any edits were applied. + """ + if self._pending_syntax_edits: + for edit in self._pending_syntax_edits: + self._syntax_tree.edit(**edit._asdict()) + self._pending_syntax_edits[:] = [] + return True + else: + return False + def prepare_query(self, query: str) -> Query | None: """Prepare a tree-sitter tree query. @@ -117,6 +159,26 @@ def query_syntax_tree( with temporary_query_point_range(query, start_point, end_point): return query.captures(self._syntax_tree.root_node) + def set_syntax_tree_update_callback( + self, + callback: Callable[[], None], + ) -> None: + """Set a callback function for signalling a rebuild of the syntax tree. + + Args: + callback: A function that takes no arguments and returns None. + """ + self._syntax_tree_update_callback = callback + + def trigger_syntax_tree_update(self, force_update: bool = False) -> None: + """Trigger a new syntax tree update to run in the background. + + Args: + force_update: When set, ensure that the syntax tree is regenerated + unconditionally. + """ + self._background_parser.trigger_syntax_tree_update(force_update) + def replace_range(self, start: Location, end: Location, text: str) -> EditResult: """Replace text at the given range. @@ -143,22 +205,47 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult 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._pending_syntax_edits.append( + SyntaxTreeEdit( + 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), + ) ) - return replace_result + def reparse(self, timeout_us: int, lines: list[str], syntax_tree=None) -> bool: + """Reparse the document. + + Args: + timeout_us: The parser timeout in microseconds. + lines: A list of the lines being parsed. + + Returns: + True if parsing succeeded and False if a timeout occurred. + """ + assert timeout_us > 0 + read_source = partial(self._read_callable, lines=lines) + tree = self._syntax_tree + saved_timeout = self._parser.timeout_micros + try: + self._parser.timeout_micros = timeout_us + try: + tree = self._parser.parse(read_source, tree) # type: ignore[arg-type] + except ValueError: + # The only known cause is a timeout. + return False + else: + self._syntax_tree = tree + if self._syntax_tree_update_callback is not None: + self._syntax_tree_update_callback() + return True + finally: + self._parser.timeout_micros = saved_timeout + def get_line(self, index: int) -> str: """Return the string representing the line, not including new line characters. @@ -214,7 +301,12 @@ def _location_to_point(self, location: Location) -> tuple[int, int]: bytes_on_left = 0 return row, bytes_on_left - def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes: + def _read_callable( + self, + byte_offset: int, + point: tuple[int, int], + lines: list[str], + ) -> bytes: """A callable which informs tree-sitter about the document content. This is passed to tree-sitter which will call it frequently to retrieve @@ -224,6 +316,7 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes: 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). + lines: The lines of the document being parsed. Returns: All the utf-8 bytes between the byte_offset/point and the end of the current @@ -231,7 +324,6 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes: offset/point requested by tree-sitter doesn't correspond to a byte. """ row, column = point - lines = self._lines newline = self.newline row_out_of_bounds = row >= len(lines) @@ -252,3 +344,75 @@ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes: return b"\n" return b"" + + +class BackgroundSyntaxParser: + """A provider of incremental background parsing for syntax highlighting. + + This runs tree-sitter parsing as a parallel, background asyncio task. This + prevents occasional, relatively long parsing times from making `TextArea` + editing become unresponsive. + """ + + PARSE_TIME_SLICE = 0.005 + PARSE_TIMEOUT_MICROSECONDS = int(PARSE_TIME_SLICE * 1_000_000) + + def __init__(self, document: SyntaxAwareDocument): + self._document_ref = weakref.ref(document) + self._event = Event() + self._task: Task = create_task(self._execute_reparsing()) + self._force_update = False + + def stop(self): + """Stop running as a background task.""" + self._task.cancel() + + def trigger_syntax_tree_update(self, force_update: bool) -> None: + """Trigger a new syntax tree update to run in the background. + + Args: + force_update: When set, ensure that the syntax tree is regenerated + unconditionally. + """ + if force_update: + self._force_update = True + self._event.set() + + async def _execute_reparsing(self) -> None: + """Run, as a task, tree-sitter reparse operations on demand.""" + while True: + try: + try: + await self._event.wait() + except Exception as e: + return + self._event.clear() + force_update = self._force_update + self._force_update = False + await self._perform_a_single_reparse(force_update) + except CancelledError: + return + + async def _perform_a_single_reparse(self, force_update: bool) -> None: + document = self._document_ref() + if document is None: + return + if not (document.apply_pending_syntax_edits() or force_update): + return + + # In order to allow the user to continue editing without interruption, we reparse + # a snapshot of the TextArea's document. + copy_of_text_for_parsing = document.copy_of_lines() + + # Use tree-sitter's parser timeout mechanism, when necessary, break the + # full reparse into multiple steps. Most of the time, tree-sitter is so + # fast that no looping occurs. + parsed_ok = False + while not parsed_ok: + parsed_ok = document.reparse( + self.PARSE_TIMEOUT_MICROSECONDS, lines=copy_of_text_for_parsing + ) + if not parsed_ok: + # Sleeping for zero seconds allows other tasks, I/O, *etc.* to execute, + # keeping the TextArea and other widgets responsive. + await sleep(0.0) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 8d14958335..c6c344ed5c 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -708,6 +708,30 @@ def check_consume_key(self, key: str, character: str | None = None) -> bool: # Otherwise we capture all printable keys return character is not None and character.isprintable() + def _handle_syntax_tree_update(self) -> None: + """Reflect changes to the syntax tree.""" + if self._highlight_query: + self._highlights.reset() + + # TODO: This feels heavy handed. + _, scroll_offset_y = self.scroll_offset + self.refresh(Region(0, scroll_offset_y, self.size.width, self.size.height)) + + def _handle_change_affecting_highlighting( + self, + force_update: bool = False, + ) -> None: + """Trigger an update of the syntax highlighting tree. + + If the tree is already being updated in the background then that will + complete first. + + Args: + force_update: When set, ensure that the syntax tree is regenerated + unconditionally. + """ + self.document.trigger_syntax_tree_update(force_update=force_update) + def _reset_highlights(self) -> None: """Reset the lazily evaluated highlight map.""" @@ -972,6 +996,7 @@ def update_highlight_query(self, name: str, highlight_query: str) -> None: # it could be a different highlight query for the same language. if name == self.language: self._set_document(self.text, name) + self._reset_highlights() def _set_document(self, text: str, language: str | None) -> None: """Construct and return an appropriate document. @@ -1013,6 +1038,9 @@ def _set_document(self, text: str, language: str | None) -> None: ) else: self._highlight_query = document.prepare_query(highlight_query) + document.set_syntax_tree_update_callback( + self._handle_syntax_tree_update + ) elif language and not TREE_SITTER: # User has supplied a language i.e. `TextArea(language="python")`, but they # don't have tree-sitter available in the environment. We fallback to plain text. @@ -1031,10 +1059,12 @@ def _set_document(self, text: str, language: str | None) -> None: # Use a regular plain-text document. document = Document(text) + if self.document: + self.document.clean_up() self.document = document self.wrapped_document = WrappedDocument(document, tab_width=self.indent_width) self.navigator = DocumentNavigator(self.wrapped_document) - self._reset_highlights() + self._handle_change_affecting_highlighting(force_update=True) self.move_cursor((0, 0)) self._rewrap_and_refresh_virtual_size() @@ -1447,7 +1477,7 @@ def edit(self, edit: Edit) -> EditResult: self._refresh_size() edit.after(self) - self._reset_highlights() + self._handle_change_affecting_highlighting() self.post_message(self.Changed(self)) return result @@ -1510,7 +1540,7 @@ def _undo_batch(self, edits: Sequence[Edit]) -> None: self._refresh_size() for edit in reversed(edits): edit.after(self) - self._reset_highlights() + self._handle_change_affecting_highlighting() self.post_message(self.Changed(self)) def _redo_batch(self, edits: Sequence[Edit]) -> None: @@ -1558,7 +1588,7 @@ def _redo_batch(self, edits: Sequence[Edit]) -> None: self._refresh_size() for edit in edits: edit.after(self) - self._reset_highlights() + self._handle_change_affecting_highlighting() self.post_message(self.Changed(self)) async def _on_key(self, event: events.Key) -> None: diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 2f3f458e43..dd7b5eeb12 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -1340,13 +1340,13 @@ 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") + 4), + press=['right', 'shift+down'] + ['shift+right'] * 8, ) @@ -1878,7 +1878,7 @@ class FormContainer(Vertical): DEFAULT_CSS = """ FormContainer { width: 50%; - border: blue; + border: blue; } """ @@ -2577,7 +2577,7 @@ class ThemeApp(App[None]): Screen { align: center middle; } - + Label { background: $panel; color: $text; @@ -2612,7 +2612,7 @@ class ThemeApp(App[None]): Screen { align: center middle; } - + Label { background: $custom-background; color: $custom-text; @@ -2787,7 +2787,7 @@ def test_position_absolute(snap_compare): class AbsoluteApp(App): CSS = """ - Screen { + Screen { align: center middle; .absolute { @@ -2802,7 +2802,7 @@ class AbsoluteApp(App): offset: 1 1; } .offset2 { - offset: 2 2; + offset: 2 2; } .offset3 { offset: 3 3; @@ -2840,7 +2840,7 @@ class GridOffsetApp(App): border: solid green; } - #six { + #six { offset: 0 10; background: blue; } @@ -3094,7 +3094,7 @@ class Test1(App): layout: horizontal; } - MainContainer { + MainContainer { width: 100%; height: 100%; background: red; @@ -3153,7 +3153,7 @@ class Sidebar(Vertical): height: auto; background: red; border: white; - } + } } """ @@ -3239,7 +3239,7 @@ class MyApp(App): height: auto; background: blue; } - Label { + Label { border: heavy red; text-align: left; } @@ -3292,12 +3292,12 @@ def test_collapsible_datatable(snap_compare): class MyApp(App): CSS = """ DataTable { - max-height: 1fr; - border: red; + max-height: 1fr; + border: red; } Collapsible { max-height: 50%; - + } """ @@ -3417,7 +3417,7 @@ def test_overflow(snap_compare): class OverflowApp(App): CSS = """ Label { - max-width: 100vw; + max-width: 100vw; } #label1 { # Overflow will be cropped @@ -3512,7 +3512,7 @@ def test_option_list_wrapping(snap_compare): class OLApp(App): CSS = """ - OptionList { + OptionList { width: 40; text-wrap: nowrap; text-overflow: ellipsis; @@ -3823,7 +3823,7 @@ def test_select_list_in_collapsible(snap_compare): class CustomWidget(Horizontal): DEFAULT_CSS = """ CustomWidget { - height: auto; + height: auto; } """ From d9398f697026761eaee6b792b4fbf455a9b322c6 Mon Sep 17 00:00:00 2001 From: Paul Ollis Date: Thu, 13 Mar 2025 17:18:37 +0000 Subject: [PATCH 4/9] Use tree-sitter Tree.changed_ranges. A first attempt at using changed_ranges to reduce the amount of redrawing triggered by completion of a tree-sitter incremental parse. --- .../document/_syntax_aware_document.py | 7 ++- src/textual/widgets/_text_area.py | 60 +++++++++++++++++-- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 849f22b54d..a970a8335c 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -239,9 +239,12 @@ def reparse(self, timeout_us: int, lines: list[str], syntax_tree=None) -> bool: # The only known cause is a timeout. return False else: - self._syntax_tree = tree if self._syntax_tree_update_callback is not None: - self._syntax_tree_update_callback() + changed_ranges = self._syntax_tree.changed_ranges(tree) + self._syntax_tree = tree + self._syntax_tree_update_callback(changed_ranges) + else: + self._syntax_tree = tree return True finally: self._parser.timeout_micros = saved_timeout diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index c6c344ed5c..c89e35447c 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -708,14 +708,62 @@ def check_consume_key(self, key: str, character: str | None = None) -> bool: # Otherwise we capture all printable keys return character is not None and character.isprintable() - def _handle_syntax_tree_update(self) -> None: + def _handle_syntax_tree_update(self, tree_ranges) -> None: """Reflect changes to the syntax tree.""" - if self._highlight_query: - self._highlights.reset() + if not self._highlight_query: + return + + self._highlights.reset() + + _, first_line_index = self.scroll_offset + visible_line_range = range( + first_line_index, first_line_index + self.size.height + ) - # TODO: This feels heavy handed. - _, scroll_offset_y = self.scroll_offset - self.refresh(Region(0, scroll_offset_y, self.size.width, self.size.height)) + visible_region = self.window_region + regions = [] + full_width = self.size.width + + for tree_range in tree_ranges: + start_row = tree_range.start_point.row + end_row = tree_range.end_point.row + if not (start_row in visible_line_range or end_row in visible_line_range): + continue + + start_column = tree_range.start_point.column + end_column = tree_range.end_point.column + + if start_row == end_row: + width = end_column = start_column + tree_region = Region(start_column, start_row, width, 1) + region = tree_region.intersection(visible_region) + if region.area > 0: + regions.append(region) + + else: + # Add region for the first changed line. + width = full_width - start_column + tree_region = Region(start_column, start_row, width, 1) + region = tree_region.intersection(visible_region) + if region.area > 0: + regions.append(region) + + # Add region for the last changed line. + tree_region = Region(0, end_row, end_column, 1) + region = tree_region.intersection(visible_region) + if region.area > 0: + regions.append(region) + + # Add region for the other lines. + height = end_row - start_row - 1 + if height > 0: + tree_region = Region(0, start_row + 1, width, height) + region = tree_region.intersection(visible_region) + if region.area > 0: + regions.append(region) + + if regions: + self.refresh(*regions) def _handle_change_affecting_highlighting( self, From 24ee972c69f222bb506a4d68ecbf5a86be79fb70 Mon Sep 17 00:00:00 2001 From: Paul Ollis Date: Mon, 17 Mar 2025 16:06:45 +0000 Subject: [PATCH 5/9] Compositor fix. 1. ChopsUpdate docstring was not correct. 2. ChopsUpdate.render_segments was generating longer sequences than necessary because it always cropped strips with a start of zero. --- src/textual/_compositor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 76103ec47c..79348dbe8a 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -174,7 +174,7 @@ def __init__( Args: chops: A mapping of offsets to list of segments, per line. - crop: Region to restrict update to. + spans: Line spans to restrict update to. chop_ends: A list of the end offsets for each line """ self.chops = chops @@ -263,8 +263,8 @@ def render_segments(self, console: Console) -> str: append(strip.render(console)) continue - strip = strip.crop(0, min(end, x2) - x) - append(move_to(x, y).segment.text) + strip = strip.crop(x1, min(end, x2) - x) + append(move_to(x1, y).segment.text) append(strip.render(console)) if y != last_y: From 3d339eb8327b53d290a09d7ae86babc3c75fc9ce Mon Sep 17 00:00:00 2001 From: Paul Ollis Date: Sun, 23 Mar 2025 20:01:24 +0000 Subject: [PATCH 6/9] Fix slow property in wrapped document. Replaced looped calculation with simple len(...). --- src/textual/document/_wrapped_document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/document/_wrapped_document.py b/src/textual/document/_wrapped_document.py index 68846bedab..5ab67f9778 100644 --- a/src/textual/document/_wrapped_document.py +++ b/src/textual/document/_wrapped_document.py @@ -151,7 +151,7 @@ def lines(self) -> list[list[str]]: @property def height(self) -> int: """The height of the wrapped document.""" - return sum(len(offsets) + 1 for offsets in self._wrap_offsets) + return len(self._offset_to_line_info) def wrap_range( self, From 329f77d39e1016e7117134ab18b61d31d9f53c45 Mon Sep 17 00:00:00 2001 From: Paul Ollis Date: Fri, 14 Mar 2025 16:08:52 +0000 Subject: [PATCH 7/9] Appear to have working render-based diff method. Now capturing a pre-rendered version of the TextArea window and using changes to the pre-rendered version to generate change regions. --- src/textual/_compositor.py | 1 + src/textual/app.py | 22 +- src/textual/document/_document.py | 64 +- src/textual/document/_document_navigator.py | 21 + .../document/_syntax_aware_document.py | 74 +- src/textual/document/_wrapped_document.py | 4 + src/textual/drivers/_writer_thread.py | 4 + src/textual/geometry.py | 85 + src/textual/screen.py | 10 +- src/textual/strip.py | 90 +- src/textual/widget.py | 27 +- src/textual/widgets/_text_area.py | 1365 +++++++++++++---- tests/document/test_document_delete.py | 25 +- ...text_area_horizontal_scrolling[press0].svg | 104 ++ ...text_area_horizontal_scrolling[press1].svg | 105 ++ ...text_area_horizontal_scrolling[press2].svg | 103 ++ ...text_area_horizontal_scrolling[press3].svg | 102 ++ ...text_area_horizontal_scrolling[press4].svg | 103 ++ ...ntal_scrolling_cursor_matching[press0].svg | 105 ++ ...ntal_scrolling_cursor_matching[press1].svg | 105 ++ ...ntal_scrolling_cursor_matching[press2].svg | 105 ++ ...est_text_area_language_rendering[bash].svg | 446 +++--- ...text_area_language_rendering[markdown].svg | 14 +- ...est_text_area_language_rendering[yaml].svg | 200 +-- ...wide_syntax_highlighting[press0-False].svg | 104 ++ ..._wide_syntax_highlighting[press0-True].svg | 120 ++ ...wide_syntax_highlighting[press1-False].svg | 104 ++ ..._wide_syntax_highlighting[press1-True].svg | 120 ++ ...wide_syntax_highlighting[press2-False].svg | 105 ++ ..._wide_syntax_highlighting[press2-True].svg | 121 ++ ...wide_syntax_highlighting[press3-False].svg | 105 ++ ..._wide_syntax_highlighting[press3-True].svg | 121 ++ ...wide_syntax_highlighting[press4-False].svg | 104 ++ ..._wide_syntax_highlighting[press4-True].svg | 120 ++ ...wide_syntax_highlighting[press5-False].svg | 104 ++ ..._wide_syntax_highlighting[press5-True].svg | 120 ++ tests/snapshot_tests/test_snapshots.py | 174 +++ tests/text_area/test_edit_via_api.py | 23 +- 38 files changed, 4083 insertions(+), 746 deletions(-) create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press0].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press1].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press2].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press3].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press4].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press0].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press1].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press2].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-False].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-True].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-False].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-True].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-False].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-True].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-False].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-True].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-False].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-True].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-False].svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-True].svg diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 79348dbe8a..c975aeb3d0 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -1229,6 +1229,7 @@ def update_widgets(self, widgets: set[Widget]) -> None: add_region = regions.append get_widget = self.visible_widgets.__getitem__ for widget in self.visible_widgets.keys() & widgets: + widget._prepare_for_repaint() region, clip = get_widget(widget) offset = region.offset intersection = clip.intersection diff --git a/src/textual/app.py b/src/textual/app.py index 6861012863..49802a335f 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -314,8 +314,8 @@ class App(Generic[ReturnType], DOMNode): scrollbar-background-active: ansi_default; scrollbar-color: ansi_blue; scrollbar-color-active: ansi_bright_blue; - scrollbar-color-hover: ansi_bright_blue; - scrollbar-corner-color: ansi_default; + scrollbar-color-hover: ansi_bright_blue; + scrollbar-corner-color: ansi_default; } .bindings-table--key { @@ -336,18 +336,18 @@ class App(Generic[ReturnType], DOMNode): } /* When a widget is maximized */ - Screen.-maximized-view { + Screen.-maximized-view { layout: vertical !important; hatch: right $panel; overflow-y: auto !important; align: center middle; .-maximized { - dock: initial !important; + dock: initial !important; } } /* Fade the header title when app is blurred */ - &:blur HeaderTitle { - text-opacity: 50%; + &:blur HeaderTitle { + text-opacity: 50%; } } *:disabled:can-focus { @@ -399,7 +399,7 @@ class MyApp(App[None]): ALLOW_SELECT: ClassVar[bool] = True """A switch to toggle arbitrary text selection for the app. - + Note that this doesn't apply to Input and TextArea which have builtin support for selection. """ @@ -445,7 +445,7 @@ class MyApp(App[None]): """The default value of [Screen.ALLOW_IN_MAXIMIZED_VIEW][textual.screen.Screen.ALLOW_IN_MAXIMIZED_VIEW].""" CLICK_CHAIN_TIME_THRESHOLD: ClassVar[float] = 0.5 - """The maximum number of seconds between clicks to upgrade a single click to a double click, + """The maximum number of seconds between clicks to upgrade a single click to a double click, a double click to a triple click, etc.""" BINDINGS: ClassVar[list[BindingType]] = [ @@ -472,7 +472,7 @@ class MyApp(App[None]): ESCAPE_TO_MINIMIZE: ClassVar[bool] = True """Use escape key to minimize widgets (potentially overriding bindings). - + This is the default value, used if the active screen's `ESCAPE_TO_MINIMIZE` is not changed from `None`. """ @@ -544,7 +544,7 @@ def __init__( self._registered_themes: dict[str, Theme] = {} """Themes that have been registered with the App using `App.register_theme`. - + This excludes the built-in themes.""" for theme in BUILTIN_THEMES.values(): @@ -746,7 +746,7 @@ def __init__( self.theme_changed_signal: Signal[Theme] = Signal(self, "theme-changed") """Signal that is published when the App's theme is changed. - + Subscribers will receive the new theme object as an argument to the callback. """ diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 97e9bb2568..2dcd9bee5c 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -3,12 +3,13 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from functools import lru_cache +from itertools import zip_longest from typing import TYPE_CHECKING, Callable, NamedTuple, Tuple, overload from typing_extensions import Literal, get_args if TYPE_CHECKING: - from tree_sitter import Node, Query + from tree_sitter import Query from textual._cells import cell_len from textual.geometry import Size @@ -27,6 +28,10 @@ class EditResult: """The new end Location after the edit is complete.""" replaced_text: str """The text that was replaced.""" + dirty_lines: range | None = None + """The range of lines considered dirty.""" + alt_dirty_line: tuple[int, range] | None = None + """Alternative list of lines considered dirty.""" @lru_cache(maxsize=1024) @@ -146,28 +151,6 @@ def clean_up(self) -> None: The default implementation does nothing. """ - def query_syntax_tree( - self, - query: "Query", - start_point: tuple[int, int] | None = None, - end_point: tuple[int, int] | None = None, - ) -> dict[str, list["Node"]]: - """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 dict mapping captured node names to lists of Nodes with that name. - """ - return {} - def set_syntax_tree_update_callback( callback: Callable[[], None], ) -> None: @@ -262,6 +245,10 @@ def newline(self) -> Newline: """Get the Newline used in this document (e.g. '\r\n', '\n'. etc.)""" return self._newline + def copy_of_lines(self): + """Provide a copy of the document's lines.""" + return list(self._lines) + def get_size(self, tab_width: int) -> Size: """The Size of the document, taking into account the tab rendering width. @@ -321,11 +308,40 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult destination_column = len(before_selection) insert_lines = [before_selection + after_selection] + try: + prev_top_line = lines[top_row] + except IndexError: + prev_top_line = None lines[top_row : bottom_row + 1] = insert_lines destination_row = top_row + len(insert_lines) - 1 end_location = (destination_row, destination_column) - return EditResult(end_location, replaced_text) + + n_previous_lines = bottom_row - top_row + 1 + dirty_range = None + alt_dirty_line = None + if len(insert_lines) != n_previous_lines: + dirty_range = range(top_row, len(lines)) + else: + if len(insert_lines) == 1 and prev_top_line is not None: + rng = self._build_single_line_range(prev_top_line, insert_lines[0]) + if rng is not None: + alt_dirty_line = top_row, rng + else: + dirty_range = range(top_row, bottom_row + 1) + + return EditResult(end_location, replaced_text, dirty_range, alt_dirty_line) + + @staticmethod + def _build_single_line_range(a, b): + rng = [] + for i, (ca, cb) in enumerate(zip_longest(a, b)): + if ca != cb: + rng.append(i) + if rng: + return range(rng[0], rng[-1] + 1) + else: + None def get_text_range(self, start: Location, end: Location) -> str: """Get the text that falls between the start and end locations. diff --git a/src/textual/document/_document_navigator.py b/src/textual/document/_document_navigator.py index 6dbc3c6d59..ee69aca79f 100644 --- a/src/textual/document/_document_navigator.py +++ b/src/textual/document/_document_navigator.py @@ -2,6 +2,8 @@ from bisect import bisect, bisect_left, bisect_right from typing import Any, Sequence +from rich.cells import get_character_cell_size + from textual._cells import cell_len from textual.document._document import Location from textual.document._wrapped_document import WrappedDocument @@ -242,6 +244,16 @@ def get_location_left(self, location: Location) -> Location: length_of_row_above = len(self._document[row - 1]) target_row = row if column != 0 else row - 1 target_column = column - 1 if column != 0 else length_of_row_above + + if target_row < self._document.line_count: + line = self._document[target_row] + if target_column < len(line): + while target_column > 0: + c = line[target_column] + if c == "\t" or get_character_cell_size(c) > 0: + break + target_column -= 1 + return target_row, target_column def get_location_right(self, location: Location) -> Location: @@ -263,6 +275,15 @@ def get_location_right(self, location: Location) -> Location: is_end_of_line = self.is_end_of_document_line(location) target_row = row + 1 if is_end_of_line else row target_column = 0 if is_end_of_line else column + 1 + + if target_row < self._document.line_count: + line = self._document[target_row] + while target_column < len(line): + c = line[target_column] + if c == "\t" or get_character_cell_size(c) > 0: + break + target_column += 1 + return target_row, target_column def get_location_above(self, location: Location) -> Location: diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index a970a8335c..24c4917cc3 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -2,12 +2,11 @@ import weakref from asyncio import CancelledError, Event, Task, create_task, sleep -from contextlib import contextmanager from functools import partial -from typing import Callable, ContextManager, NamedTuple +from typing import Callable, NamedTuple try: - from tree_sitter import Language, Node, Parser, Query, Tree + from tree_sitter import Language, Parser, Query, Tree TREE_SITTER = True except ImportError: @@ -17,35 +16,6 @@ from textual.document._document import Document, EditResult, Location, _utf8_encode -@contextmanager -def temporary_query_point_range( - query: Query, - start_point: tuple[int, int] | None, - end_point: tuple[int, int] | None, -) -> ContextManager[None]: - """Temporarily change the start and/or end point for a tree-sitter Query. - - Args: - query: The tree-sitter Query. - start_point: The (row, column byte) to start the query at. - end_point: The (row, column byte) to end the query at. - """ - # Note: Although not documented for the tree-sitter Python API, an - # end-point of (0, 0) means 'end of document'. - default_point_range = [(0, 0), (0, 0)] - - point_range = list(default_point_range) - if start_point is not None: - point_range[0] = start_point - if end_point is not None: - point_range[1] = end_point - query.set_point_range(point_range) - try: - yield None - finally: - query.set_point_range(default_point_range) - - class SyntaxTreeEdit(NamedTuple): """Details of a tree-sitter syntax tree edit operation.""" @@ -99,14 +69,15 @@ def __init__( self._background_parser = BackgroundSyntaxParser(self) self._pending_syntax_edits: list[SyntaxTreeEdit] = [] + @property + def current_syntax_tree(self) -> Tree: + """The current syntax tree.""" + return self._syntax_tree + def clean_up(self) -> None: """Perform any pre-deletion clean up.""" self._background_parser.stop() - def copy_of_lines(self): - """Provide a copy of the document's lines.""" - return list(self._lines) - def apply_pending_syntax_edits(self) -> bool: """Apply any pending edits to the syntax tree. @@ -136,29 +107,6 @@ def prepare_query(self, query: str) -> Query | None: """ 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, - ) -> dict[str, list["Node"]]: - """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. - """ - with temporary_query_point_range(query, start_point, end_point): - return query.captures(self._syntax_tree.root_node) - def set_syntax_tree_update_callback( self, callback: Callable[[], None], @@ -239,10 +187,14 @@ def reparse(self, timeout_us: int, lines: list[str], syntax_tree=None) -> bool: # The only known cause is a timeout. return False else: + self._syntax_tree = tree if self._syntax_tree_update_callback is not None: + + def set_new_tree(): + self._syntax_tree = tree + changed_ranges = self._syntax_tree.changed_ranges(tree) - self._syntax_tree = tree - self._syntax_tree_update_callback(changed_ranges) + self._syntax_tree_update_callback(self._syntax_tree, len(lines)) else: self._syntax_tree = tree return True diff --git a/src/textual/document/_wrapped_document.py b/src/textual/document/_wrapped_document.py index 5ab67f9778..1cbf77e69e 100644 --- a/src/textual/document/_wrapped_document.py +++ b/src/textual/document/_wrapped_document.py @@ -1,6 +1,7 @@ from __future__ import annotations from bisect import bisect_right +from functools import lru_cache from rich.text import Text @@ -85,6 +86,7 @@ def wrap(self, width: int, tab_width: int | None = None) -> None: tab_width: The maximum width to consider for tab characters. If None, reuse the tab width. """ + self.get_sections.cache_clear() self._width = width if tab_width: self._tab_width = tab_width @@ -168,6 +170,7 @@ def wrap_range( old_end: The old end location of the edit in document-space. new_end: The new end location of the edit in document-space. """ + self.get_sections.cache_clear() start_line_index, _ = start old_end_line_index, _ = old_end new_end_line_index, _ = new_end @@ -403,6 +406,7 @@ def get_target_document_column( return target_column_index + @lru_cache(200) def get_sections(self, line_index: int) -> list[str]: """Return the sections for the given line index. diff --git a/src/textual/drivers/_writer_thread.py b/src/textual/drivers/_writer_thread.py index a26ef46fbb..aa2dbc9f1f 100644 --- a/src/textual/drivers/_writer_thread.py +++ b/src/textual/drivers/_writer_thread.py @@ -8,6 +8,8 @@ MAX_QUEUED_WRITES: Final[int] = 30 +write_count = 0 + class WriterThread(threading.Thread): """A thread / file-like to do writes to stdout in the background.""" @@ -23,6 +25,8 @@ def write(self, text: str) -> None: Args: text: Text to write to the file. """ + global write_count + write_count += len(text) self._queue.put(text) def isatty(self) -> bool: diff --git a/src/textual/geometry.py b/src/textual/geometry.py index 9847052fa8..e7d18c9326 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -1315,6 +1315,91 @@ def grow_maximum(self, other: Spacing) -> Spacing: ) +class LineRegion(NamedTuple): + """Defines a region for a line. + + This is similar to a `Region`, but its height is fixed at 1. + + ``` + (x, y) + ┌────────────────────┐ ▲ + │ │ height = 1 + └────────────────────┘ ▼ + ◀─────── width ──────▶ + ``` + + A region may be empty (w == 0) in which case the 'x' value is considered + irrelevant. + """ + + x: int = 0 + """Offset in the x-axis (horizontal).""" + y: int = 0 + """Offset in the y-axis (vertical).""" + width: int = 0 + """The width of the region.""" + + def __bool__(self) -> bool: + """A Region is considered False when it has zero width.""" + return self.width > 0 + + @lru_cache(maxsize=1024) + def union(self, region: LineRegion) -> LineRegion: + """Get the smallest line region that contains both regions. + + Both regions must have the same 'y' value. + + Args: + region: Another line region. + + Returns: + An optimally sized region to cover both regions. + """ + assert self.y == region.y + + if self.width == 0: + return region + elif region.width == 0: + return self + + x_min = min(self.x, region.x) + x_max = max(self.x + self.width, region.x + region.width) + return LineRegion(x_min, self.y, x_max - x_min) + + @lru_cache(maxsize=1024) + def as_region(self, xoffset: int) -> Region: + x, y, w = self + return Region(x + xoffset, y, w, 1) + + @lru_cache(maxsize=1024) + def disjoint_regions(self, region: LineRegion) -> list[LineRegion]: + assert self.y == region.y + + if self.x == region.x and self.width == region.width: + return [] + + self_x2 = self.x + self.width - 1 + region_x2 = region.x + region.width - 1 + if self_x2 + 1 == region.x or region_x2 + 1 == self.x: + # The regions abut, so return the union.A + return [self.union(region)] + + if self_x2 + 1 < region.x or region_x2 + 1 < self.x: + # The regions do not overlap at all so return boith regions. + return [self, region] + + regions = [] + w = abs(self.x - region.x) + if w != 0: + regions.append(LineRegion(min(self.x, region.x), self.y, w)) + w = abs(self_x2 - region_x2) + if w != 0: + x2 = max(self_x1, region_x2) + x = x2 - w + 1 + regions.append(LineRegion(x, self.y, w)) + return regions + + NULL_OFFSET: Final = Offset(0, 0) """An [offset][textual.geometry.Offset] constant for (0, 0).""" diff --git a/src/textual/screen.py b/src/textual/screen.py index 1268858e22..780d5ae5a1 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -154,8 +154,8 @@ class Screen(Generic[ScreenResultType], Widget): Screen { layout: vertical; overflow-y: auto; - background: $background; - + background: $background; + &:inline { height: auto; min-height: 1; @@ -167,7 +167,7 @@ class Screen(Generic[ScreenResultType], Widget): background: ansi_default; color: ansi_default; - &.-screen-suspended { + &.-screen-suspended { text-style: dim; ScrollBar { text-style: not dim; @@ -175,7 +175,7 @@ class Screen(Generic[ScreenResultType], Widget): } } .screen--selection { - background: $primary 50%; + background: $primary 50%; } } """ @@ -1811,7 +1811,7 @@ class ModalScreen(Screen[ScreenResultType]): overflow-y: auto; background: $background 60%; &:ansi { - background: transparent; + background: transparent; } } """ diff --git a/src/textual/strip.py b/src/textual/strip.py index dc69561a5b..50c06d9682 100644 --- a/src/textual/strip.py +++ b/src/textual/strip.py @@ -7,11 +7,17 @@ from __future__ import annotations +from functools import lru_cache from itertools import chain from typing import Any, Iterable, Iterator, Sequence import rich.repr -from rich.cells import cell_len, set_cell_size +from rich.cells import ( + _is_single_cell_widths, + cell_len, + get_character_cell_size, + set_cell_size, +) from rich.console import Console, ConsoleOptions, RenderResult from rich.measure import Measurement from rich.segment import Segment @@ -525,7 +531,7 @@ def crop(self, start: int, end: int | None = None) -> Strip: for segment in iter_segments: end_pos = pos + _cell_len(segment.text) if end_pos > start: - segment = segment.split_cells(start - pos)[1] + segment = self.split_segment_cells(segment, start - pos)[1] break pos = end_pos @@ -542,7 +548,7 @@ def crop(self, start: int, end: int | None = None) -> Strip: if end_pos < end: add_segment(segment) else: - add_segment(segment.split_cells(end - pos)[0]) + add_segment(self.split_segment_cells(segment, end - pos)[0]) break pos = end_pos segment = next(iter_segments, None) @@ -550,6 +556,84 @@ def crop(self, start: int, end: int | None = None) -> Strip: self._crop_cache[cache_key] = strip return strip + # TODO: + # This and _split_cells are replacements for Rich library methods. + # Arguably, the Rich library should be updated with the same 'fixes'. + @classmethod + def split_segment_cells(cls, segment, cut: int) -> Tuple["Segment", "Segment"]: + """Split segment into two segments at the specified column. + + If the cut point falls in the middle of a 2-cell wide character then it is replaced + by two spaces, to preserve the display width of the parent segment. + + Returns: + Tuple[Segment, Segment]: Two segments. + """ + text, style, control = segment + + if _is_single_cell_widths(text): + # Fast path with all 1 cell characters + if cut >= len(text): + return segment, Segment("", style, control) + return ( + Segment(text[:cut], style, control), + Segment(text[cut:], style, control), + ) + + return cls._split_cells(segment, cut) + + @classmethod + @lru_cache(1024 * 16) + def _split_cells(cls, segment: "Segment", cut: int) -> Tuple["Segment", "Segment"]: + + def split_text_at_cell_boundary(text, index): + text_end = len(text) - 1 + while index < text_end: + if cell_size(text[index]) > 0: + break + index += 1 + return text[:index], text[index:] + + text, style, control = segment + _Segment = Segment + + cell_length = segment.cell_length + if cut >= cell_length: + # Cut beyond end of segment, return segment and empty segment. + return segment, _Segment("", style, control) + + # If some simple maths lands us on the correct cell then 'take the + # win'. + cell_size = get_character_cell_size + pos = int((cut / cell_length) * (len(text) - 1)) + before, after = split_text_at_cell_boundary(text, pos) + cell_pos = cell_len(before) + if cell_pos == cut: + return ( + _Segment(before, style, control), + _Segment(text[pos:], style, control), + ) + + # Fall back to the slowish iteration over characters. + while pos < len(text): + char = text[pos] + pos += 1 + cell_pos += cell_size(char) + if cell_pos == cut: + before, after = split_text_at_cell_boundary(text, pos) + return ( + _Segment(before, style, control), + _Segment(after, style, control), + ) + if cell_pos > cut: + before = text[:pos] + return ( + _Segment(before[: pos - 1] + " ", style, control), + _Segment(" " + text[pos:], style, control), + ) + + raise AssertionError("Will never reach here") + def divide(self, cuts: Iterable[int]) -> Sequence[Strip]: """Divide the strip into multiple smaller strips by cutting at given (cell) indices. diff --git a/src/textual/widget.py b/src/textual/widget.py index 04557156df..243d573110 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -309,11 +309,11 @@ class Widget(DOMNode): ALLOW_MAXIMIZE: ClassVar[bool | None] = None """Defines default logic to allow the widget to be maximized. - + - `None` Use default behavior (Focusable widgets may be maximized) - `False` Do not allow widget to be maximized - `True` Allow widget to be maximized - + """ ALLOW_SELECT: ClassVar[bool] = True @@ -2437,6 +2437,18 @@ def _set_dirty(self, *regions: Region) -> None: if outer_size: self._repaint_regions.add(outer_size.region) + def _prepare_for_repaint(self) -> None: + """A hook to allow a widget to prepare for a repaint. + + Currently only used by `TextArea`. This hook allow a widget a chance to + perform extra preparation required in order to be ready to repaint. + + The hook is for specialised use and should almost certainly not be + used for most widgets. In the case of the `TextArea`, it is used to + calculate the regions that have changed since the last repaint - + something that is automatic for other widgets. + """ + def _exchange_repaint_regions(self) -> Collection[Region]: """Get a copy of the regions which need a repaint, and clear internal cache. @@ -4074,6 +4086,17 @@ def refresh( self.check_idle() return self + def _trigger_repaint(self) -> None: + """Indicate that parts of this widget need repainting. + + !!!warning + + This is provided for very specialised use - currently only the + `TextArea` widget uses it. + """ + self._repaint_required = True + self.check_idle() + def remove(self) -> AwaitRemove: """Remove the Widget from the DOM (effectively deleting it). diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index c89e35447c..288297b717 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -2,12 +2,25 @@ import dataclasses import re +from array import array from collections import defaultdict +from contextlib import contextmanager from dataclasses import dataclass from functools import lru_cache +from itertools import chain from pathlib import Path -from typing import TYPE_CHECKING, ClassVar, Iterable, Optional, Sequence, Tuple +from typing import ( + TYPE_CHECKING, + ClassVar, + ContextManager, + Iterable, + MutableSequence, + Optional, + Sequence, + Tuple, +) +from rich.cells import get_character_cell_size from rich.console import RenderableType from rich.style import Style from rich.text import Text @@ -22,7 +35,6 @@ EditResult, Location, Selection, - _utf8_encode, ) from textual.document._document_navigator import DocumentNavigator from textual.document._edit import Edit @@ -32,7 +44,7 @@ SyntaxAwareDocumentError, ) from textual.document._wrapped_document import WrappedDocument -from textual.expand_tabs import expand_tabs_inline, expand_text_tabs_from_widths +from textual.expand_tabs import expand_tabs_inline from textual.screen import Screen if TYPE_CHECKING: @@ -42,7 +54,7 @@ from textual._cells import cell_len, cell_width_to_column_index from textual.binding import Binding from textual.events import Message, MouseEvent -from textual.geometry import Offset, Region, Size, Spacing, clamp +from textual.geometry import LineRegion, Offset, Region, Size, Spacing, clamp from textual.reactive import Reactive, reactive from textual.scroll_view import ScrollView from textual.strip import Strip @@ -51,6 +63,11 @@ _CLOSING_BRACKETS = {v: k for k, v in _OPENING_BRACKETS.items()} _TREE_SITTER_PATH = Path(__file__).parent / "../tree-sitter/" _HIGHLIGHTS_PATH = _TREE_SITTER_PATH / "highlights/" +DISCARDABLE_CONTROL_CHARS: set[str] = set( + chr(n) for n in chain(range(0x09), range(0x10, 0x20), range(0x7F, 0xA0)) +) +TAB = "\t" +EMPTY_RANGE = range(0) StartColumn = int EndColumn = Optional[int] @@ -77,6 +94,15 @@ ] """Languages that are included in the `syntax` extras.""" +# This is a pragmatic limitation, which allows some code to be simplified and +# other code to use less memory. This is basically 2**32 so lines of 2**31 less +# a few bytes are technically supported, but I (Paul Ollis) have no intention +# of writing the tests. +# +# This constant can be elimiated at the expense of handling extra corner cases +# in the code and not using array('L') in place of lists. +MAX_LINE_LENGTH_X2 = 0x1_0000_0000 + class ThemeDoesNotExist(Exception): """Raised when the user tries to use a theme which does not exist. @@ -90,10 +116,242 @@ class LanguageDoesNotExist(Exception): """ +class TextReprString(str): + """A string 'optimised' for text representation. + + This extends the standard Python 'str' providing support for easily working + with character cells. + + Use the `create` class method to instantiate instances. + """ + + __slots__ = ["_cell_offsets", "_cells", "_tab_offsets"] + + def __new__(cls, value: str): + # Create the basic instance. the `create` class method is responsible + # for assigning the additional attributes. + inst = super().__new__(cls, value) + return inst + + @classmethod + @lru_cache(maxsize=256) + def create(cls, text: str, tab_size: int = 4, tab_offset: int = 0): + """Create a TextReprString from the given text. + + Any TAB characters in `text` are expanded before the instance is + created. Expansion is performed with respect to character cells, taking + into account wide characters and zero width combining characters. + Leading zero width characters, all non-TAB control characters and + combining characters immediately after TAB characters are discarded. + + Args: + text: The text of the string. + tab_size: The tab-stop size for the string. + offset: The offset to the first tabstop position. Zero implies + tab_size. + """ + text, cells, cell_offsets, tab_offsets = cls._expand_and_map_string( + text, tab_size, tab_offset + ) + inst = cls(text) + inst._cell_offsets = cell_offsets + inst._cells = cells + inst._tab_offsets = tab_offsets + return inst + + @property + def cells(self) -> list[tuple[str, ...]]: + """A list of character tuples for this string's terminal representation. + + Most cells will consist of a 1-tuple holding a single unicode + character. For double width characters, the first cell is a 1-tuple of + the character and the following cell is an empty tuple. For TAB + characters, a 1-tuple of (`\t`,) is followed by zero or more empty + tuples. When a character is followed by one or more combining (zero + width) characters, the cell is a tuple of the character and those + combining characters. + """ + return self._cells + + def cell_width(self, index) -> int: + """The number of cells covered by the character at a given index. + + When the index lies on an expanded TAB character, the result is + number of spaces used in the expansion. + """ + try: + cell_index = self._cell_offsets[index] + n = len(self._cells[cell_index]) + except IndexError: + return 1 + else: + if n == 0: + return 0 + cell_index += 1 + while cell_index < len(self._cells): + chars = self._cells[cell_index] + if chars: + break + n += 1 + cell_index += 1 + return n + + def logical_character_width(self, index) -> int: + """The logical width of the character at a given index. + + For TAB characters this is the number of spaces that the TAB has been + expannded to. For all other characters this returns 1. + """ + try: + cell_index = self._cell_offsets[index] + cell = self._cells[cell_index] + except IndexError: + return 1 + if cell == (TAB,): + return self.cell_width(index) + else: + return 1 + + def render_count(self, index) -> int: + """How many characters combine into the cell for the character index. + + Typically this is 1, but if a character is followed by one or more + combining (zero width) characters the value is increased accordingly. + """ + try: + cell_index = self._cell_offsets[index] + return len(self._cells[cell_index]) + except IndexError: + return 1 + + def cell_offset(self, index) -> int: + """Calculate the cell offset for the indexed character.""" + try: + return self._cell_offsets[index] + except IndexError: + return self._cell_offsets[-1] + + def adjust_index_for_tabs(self, index) -> int: + """Adjust index to allow for TAB character expansion.""" + try: + return self._tab_offsets[index] + except IndexError: + return len(self) + + @staticmethod + def _expand_and_map_string( + text: str, + tab_size: int, + tab_offset: int, + ) -> tuple[str, bytearray, bytearray]: + """Expand TAB characters in text and generate useful mappings. + + Args: + text: The text of the string. + tab_size: The tab-stop size for the string. + offset: An offset to the first tabstop position. + Returns: + A tuple of: + + 1. The expanded string. + 2. A character cell representation of the string. + 3. Offsets that allow mapping string indices to cell indices. + 4. Offsets that map from the original string to the expanded string. + """ + expanded_text: list[str] = [] + cell_offsets = array("L") + tab_offsets = array("L") + cells: list[tuple[str, ...]] = [] + + next_tab_offset = tab_size if tab_offset == 0 else tab_offset + char_index = 0 + cell_index = 0 + discarding_zero_width = True + empty_tuple = () + + for c in text: + if discarding_zero_width and character_cell_size(c) == 0: + # Discard invalid zero width/control characters. + continue + + if c in DISCARDABLE_CONTROL_CHARS: + continue + + discarding_zero_width = False + tab_offsets.append(char_index) + if c == TAB: + # Replace with a space and add additional spaces so that the + # next character's cell is tab-aligned. + expanded_text.append(" ") + cells.append((TAB,)) + cell_offsets.append(cell_index) + char_index += 1 + cell_index += 1 + while cell_index < next_tab_offset: + expanded_text.append(" ") + cells.append(empty_tuple) + cell_offsets.append(cell_index) + cell_index += 1 + char_index += 1 + discarding_zero_width = False + + else: + width = character_cell_size(c) + if width == 0: + cells[-1] = cells[-1] + (c,) + else: + expanded_text.append(c) + cells.append((c,)) + assert width == 1 or width == 2 + if width == 2: + cells.append(()) + + cell_offsets.append(cell_index) + char_index += 1 + cell_index += character_cell_size(c) + + if cell_index >= next_tab_offset: + next_tab_offset += tab_size + + # Add a cell offset for the 'cursor-at-end-of-line' position. + cell_offsets.append(char_index) + return "".join(expanded_text), cells, cell_offsets, tab_offsets + + +class DocSelection: + + def __init__(self, selection: Selection, lines: list[str]): + start, end = selection.start, selection.end + self.selection = Selection(*sorted((start, end))) + self.cursor = end + self.lines = lines + self.line_range = range(selection.start[0], selection.end[0] + 1) + + def char_range(self, index) -> range: + """The range of characters covered on a given line.""" + lines = self.lines + if index in self.line_range and index < len(lines): + selection = self.selection + line = lines[index] + if len(self.line_range) == 1: + return range(selection.start[1], selection.end[1] + 1) + elif index == selection.start[0]: + return range(selection.start[1], len(line)) + elif index == selection.end[0]: + return range(0, min(selection.end[1] + 1, len(line))) + else: + return range(0, len(line)) + else: + return range(0) + + def __len__(self) -> int: + return len(self.line_range) + + class HighlightMap: """Lazy evaluated pseudo dictionary mapping lines to highlight information. - This allows TextArea syntax highlighting to scale. + This represents a snapshot of the TextArea's underlying document. Args: text_area_widget: The associated `TextArea` widget. @@ -101,29 +359,104 @@ class HighlightMap: BLOCK_SIZE = 50 - def __init__(self, text_area: TextArea): - self.text_area: TextArea = text_area - """The text area associated with this highlight map.""" + def __init__(self): + self._lines: list[str] = [] + """The lines from which the syntax tree was generated.""" + + self._tree: Tree | None = None + """The tree-sitter tree from which to genrate highlights.""" + + self._query: Query | None = None + """The tree-sitter query used to generate highlights from the tree.""" self._highlighted_blocks: set[int] = set() - """The set of blocks that have been highlighted. Each block covers BLOCK_SIZE - lines. - """ + """The set of already highlighted blocks, BLOCK_SIZE lines per block.""" self._highlights: dict[int, list[Highlight]] = defaultdict(list) - """A mapping from line index to a list of Highlight instances.""" + """A cache mapping line index to a list of Highlight instances.""" + + self.tab_size: int = 4 + """The tab size setting in effect.""" - def reset(self) -> None: - """Reset so that future lookups rebuild the highlight map.""" + @property + def line_count(self) -> int: + """The number of lines in the map.""" + return len(self._lines) + + def copy(self) -> HighlightMap: + """Create a copy of this highlight map.""" + inst = HighlightMap() + inst._lines = self._lines + inst._tree = self._tree + inst._query = self._query + inst._highlighted_blocks = self._highlighted_blocks.copy() + inst._highlights = self._highlights.copy() + return inst + + def set_snapshot( + self, lines: list[str], query: Query, tree: Tree, tab_size: int + ) -> None: + """Set the snapshot information for this mapping + + Args: + lines: The lines from which the syntax tree was generated. + query: The current Query structure used to generate highlights. + tree: The tree-sitter syntax tree. + tab_size: The tab_size for the text area. + .""" + if self._tree is not tree: + self._highlights.clear() + self._highlighted_blocks.clear() + self._query = query + self._tree = tree + self.tab_size = tab_size + self._lines = [] if None in (query, tree) else lines + + def set_query(self, query: Query) -> None: + self._query = query self._highlights.clear() self._highlighted_blocks.clear() - @property - def document(self) -> DocumentBase: - """The text document being highlighted.""" - return self.text_area.document + def difference_region( + self, index: int, other_map: HighlightMap + ) -> LineRegion | None: + """Create a region by comparing highlights for a line with another map. + + Args: + index: The index of the line to compare. + other_map: The other HighlightMap to use for comparison. + + Returns: + None if the highlights are the same. Otherwise a Region + covering the full extent of the changes. + """ + highlights = self[index] + other_highlighs = other_map[index] + if highlights != other_highlighs: + min_start = min( + highlight[0] for highlight in chain(highlights, other_highlighs) + ) + max_end = max( + highlight[1] for highlight in chain(highlights, other_highlighs) + ) + return LineRegion(min_start, index, max_end - min_start + 1) + else: + return None + + @staticmethod + def _highlight_change_range(highlight_list_a, highlight_list_b) -> range: + min_start = min( + highlight[0] for highlight in chain(highlight_list_a, highlight_list_b) + ) + max_end = max( + highlight[1] for highlight in chain(highlight_list_a, highlight_list_b) + ) + return range(min_start, max_end + 1) def __getitem__(self, index: int) -> list[Highlight]: + if index >= self.line_count: + return [] + block_index = index // self.BLOCK_SIZE if block_index not in self._highlighted_blocks: self._highlighted_blocks.add(block_index) @@ -133,40 +466,64 @@ def __getitem__(self, index: int) -> list[Highlight]: def _build_part_of_highlight_map(self, start_index: int) -> None: """Build part of the highlight map. + This is invoped by __getitem__, when an uncached highlight list is + accessed. It generates the highlights for the block of lines containing + the index and adds them to the cache. + Args: - start_index: The start of the block of line for which to build the map. + start_index: The start of the block of lines for which to build the + map. """ - highlights = self._highlights start_point = (start_index, 0) - end_index = min(self.document.line_count, start_index + self.BLOCK_SIZE) + end_index = min(self.line_count, start_index + self.BLOCK_SIZE) end_point = (end_index, 0) - captures = self.document.query_syntax_tree( - self.text_area._highlight_query, - start_point=start_point, - end_point=end_point, - ) + with temporary_query_point_range(self._query, start_point, end_point): + captures = self._query.captures(self._tree.root_node) + + highlights = self._highlights + line_count = len(self._lines) for highlight_name, nodes in captures.items(): for node in nodes: 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) + if node_start_row < line_count: + 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) - ) + if node_start_row < line_count: + highlights[node_start_row].append( + (node_start_column, None, highlight_name) + ) # Add the middle lines - entire row of this node is highlighted middle_highlight = (0, None, highlight_name) for node_row in range(node_start_row + 1, node_end_row): - highlights[node_row].append(middle_highlight) + if node_row < line_count: + highlights[node_row].append(middle_highlight) # Add the last line of the node range - highlights[node_end_row].append( - (0, node_end_column, highlight_name) - ) + if node_end_row < line_count: + highlights[node_end_row].append( + (0, node_end_column, highlight_name) + ) + + def realign_highlight(highlight): + start, end, name = highlight + return mapper[start], mapper[end], name + + # Tree-sitter uses byte offsets, but we want to use characters so we + # adjust each highlight's offset here to match character positions in + # the (TAB expanded) line text. + block_end = min(line_count, start_index + self.BLOCK_SIZE) + for index in range(start_index, block_end): + # text, offsets = expand_tabs(self._lines[index]) + text = self._lines[index] + mapper = build_byte_to_tab_expanded_string_table(text, self.tab_size) + highlights[index][:] = [ + realign_highlight(highlight) for highlight in highlights[index] + ] # The highlights for each line need to be sorted. Each highlight is of # the form: @@ -189,6 +546,142 @@ def sort_key(highlight: Highlight) -> tuple[int, int, int]: highlights.get(line_index, []).sort(key=sort_key) +@dataclass +class PreRenderLine: + """Pre-render information about a physical line in a TextArea. + + This stores enough information to compute difference regions. + """ + + text: TextReprString + """The plain form of the text on the line.""" + gutter_text: str + """The text shown in the gutter.""" + select_range: range + """The range of characters highlighted by the current selection.""" + syntax: list[Highlight] + """The syntax highlights for the line.""" + cursor_highlighted: bool + """Set if cursor highlighting is shown for this physical line.""" + + def make_diff_regions( + self, + text_area: TextArea, + y: int, + line: PreRenderLine | None, + full_width: int, + gutter_width: int, + prev_gutter_width: int, + ) -> list[LineRegion]: + text_width = full_width - gutter_width + if line is None: + return [LineRegion(0, y, text_width)] + + def text_region(x, width): + return LineRegion(x + gutter_width, y, width) + + regions = [] + if self.cursor_highlighted != line.cursor_highlighted: + regions.append(LineRegion(0, y, text_width + gutter_width)) + return regions + + if self.text != line.text: + regions.append( + build_difference_region( + y, + self.text.cells, + line.text.cells, + x_offset=gutter_width, + ) + ) + + if self.gutter_text != line.gutter_text: + text_a = text_b = "" + if gutter_width > 1: + gutter_width_no_margin = gutter_width - 2 + text_a = f"{self.gutter_text:>{gutter_width_no_margin}}" + if prev_gutter_width > 1: + gutter_width_no_margin = prev_gutter_width - 2 + text_b = f"{line.gutter_text:>{gutter_width_no_margin}}" + regions.append(build_difference_region(y, text_a, text_b)) + + if self.select_range != line.select_range: + before, common, after = intersect_ranges( + self.select_range, line.select_range + ) + if before: + regions.append(text_region(before.start, len(before))) + if after: + regions.append(text_region(after.start, len(after))) + + if self.syntax != line.syntax: + min_start = min(hl[0] for hl in chain(self.syntax, line.syntax)) + max_end = max(hl[1] for hl in chain(self.syntax, line.syntax)) + regions.append(text_region(min_start, max_end - min_start + 1)) + + return regions + + +@dataclass +class PreRenderState: + """Information about the TextArea state at the just before being rendered. + + Attributes: + lines: A snapshot of the document's lines at the time of rendering. + cursor: A tuple of (visible, cell_x, cell_y, width, char_x). + size: The (width, height) of the full text area, including the gutter. + gutter_width: The width of the gutter. + bracket: A tuple of (cell_x, cell_y, char_x). + """ + + lines: list[PreRenderLine] + cursor: tuple[bool, int, int, int] = (False, -1, -1, 1) + size: tuple[int, int] = 0, 0 + gutter_width: int = 0 + bracket: tuple[int, int, int] = -1, -1, -1 + + def make_diff_regions( + self, + text_area: TextArea, + state: PreRenderState, + ) -> list[LineRegion]: + + def text_region(x, y, width): + return LineRegion(x + gutter_width, y, width) + + gutter_width = self.gutter_width + prev_gutter_width = state.gutter_width + regions = [] + for y, line in enumerate(self.lines): + try: + old_line = state.lines[y] + except IndexError: + old_line = None + if not (old_line == line): + regions.extend( + line.make_diff_regions( + text_area, + y, + old_line, + self.size.width, + gutter_width, + prev_gutter_width, + ) + ) + + if self.cursor != state.cursor: + if self.cursor[1] >= 0: + regions.append(text_region(*self.cursor[1:-1])) + if state.cursor[1] >= 0: + regions.append(text_region(*state.cursor[1:-1])) + if self.bracket != state.bracket: + if self.bracket[0] >= 0: + regions.append(text_region(*self.bracket[:-1], 1)) + if state.bracket[0] >= 0: + regions.append(text_region(*state.bracket[:-1], 1)) + return regions + + @dataclass class TextAreaLanguage: """A container for a language which has been registered with the TextArea.""" @@ -422,7 +915,10 @@ class TextArea(ScrollView): """ selection: Reactive[Selection] = reactive( - Selection(), init=False, always_update=True + Selection(), + init=False, + always_update=True, + repaint=False, ) """The selection start and end locations (zero-based line_index, offset). @@ -578,8 +1074,8 @@ def __init__( self.document: DocumentBase = Document(text) """The document this widget is currently editing.""" - self._highlights: HighlightMap = HighlightMap(self) - """Mapping line numbers to the set of highlights for that line.""" + self._highlights: HighlightMap = HighlightMap() + """Mapping of line number to the lists of highlights.""" self.wrapped_document: WrappedDocument = WrappedDocument(self.document) """The wrapped view of the document.""" @@ -591,6 +1087,10 @@ def __init__( self._cursor_offset = (0, 0) """The virtual offset of the cursor (not screen-space offset).""" + self._old_render_state: PreRenderState = PreRenderState(lines=[]) + self._new_render_state: PreRenderState = PreRenderState(lines=[]) + """Saved state for the last time this widget was rendered.""" + self._set_document(text, language) self.language = language @@ -708,62 +1208,9 @@ def check_consume_key(self, key: str, character: str | None = None) -> bool: # Otherwise we capture all printable keys return character is not None and character.isprintable() - def _handle_syntax_tree_update(self, tree_ranges) -> None: + def _handle_syntax_tree_update(self, tree: Tree, line_count: int) -> None: """Reflect changes to the syntax tree.""" - if not self._highlight_query: - return - - self._highlights.reset() - - _, first_line_index = self.scroll_offset - visible_line_range = range( - first_line_index, first_line_index + self.size.height - ) - - visible_region = self.window_region - regions = [] - full_width = self.size.width - - for tree_range in tree_ranges: - start_row = tree_range.start_point.row - end_row = tree_range.end_point.row - if not (start_row in visible_line_range or end_row in visible_line_range): - continue - - start_column = tree_range.start_point.column - end_column = tree_range.end_point.column - - if start_row == end_row: - width = end_column = start_column - tree_region = Region(start_column, start_row, width, 1) - region = tree_region.intersection(visible_region) - if region.area > 0: - regions.append(region) - - else: - # Add region for the first changed line. - width = full_width - start_column - tree_region = Region(start_column, start_row, width, 1) - region = tree_region.intersection(visible_region) - if region.area > 0: - regions.append(region) - - # Add region for the last changed line. - tree_region = Region(0, end_row, end_column, 1) - region = tree_region.intersection(visible_region) - if region.area > 0: - regions.append(region) - - # Add region for the other lines. - height = end_row - start_row - 1 - if height > 0: - tree_region = Region(0, start_row + 1, width, height) - region = tree_region.intersection(visible_region) - if region.area > 0: - regions.append(region) - - if regions: - self.refresh(*regions) + self._trigger_repaint() def _handle_change_affecting_highlighting( self, @@ -810,6 +1257,9 @@ def _watch_selection( self.scroll_cursor_visible() + if previous_selection != selection: + self.post_message(self.SelectionChanged(selection, self)) + cursor_row, cursor_column = cursor_location try: @@ -820,13 +1270,8 @@ def _watch_selection( # 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: - _, offset_y = self._cursor_offset - self.refresh_lines(offset_y) - self.app.cursor_position = self.cursor_screen_offset - if previous_selection != selection: - self.post_message(self.SelectionChanged(selection, self)) + self._trigger_repaint() def _watch_cursor_blink(self, blink: bool) -> None: if not self.is_mounted: @@ -1044,7 +1489,8 @@ def update_highlight_query(self, name: str, highlight_query: str) -> None: # it could be a different highlight query for the same language. if name == self.language: self._set_document(self.text, name) - self._reset_highlights() + if self._highlight_query: + self._highlights.set_query(self._highlight_query) def _set_document(self, text: str, language: str | None) -> None: """Construct and return an appropriate document. @@ -1215,12 +1661,17 @@ def _yield_character_locations_reverse( def _refresh_size(self) -> None: """Update the virtual size of the TextArea.""" - if self.soft_wrap: - self.virtual_size = Size(0, self.wrapped_document.height) - else: - # +1 width to make space for the cursor resting at the end of the line - width, height = self.document.get_size(self.indent_width) - self.virtual_size = Size(width + self.gutter_width + 1, height) + dirty = self._dirty_regions.copy(), self._repaint_regions.copy() + try: + if self.soft_wrap: + self.virtual_size = Size(0, self.wrapped_document.height) + else: + # +1 width to make space for the cursor resting at the end of the line + width, height = self.document.get_size(self.indent_width) + self.virtual_size = Size(width + self.gutter_width + 1, height) + finally: + self._dirty_regions, self._repaint_regions = dirty + self._trigger_repaint() def get_line(self, line_index: int) -> Text: """Retrieve the line at the given line index. @@ -1237,222 +1688,410 @@ def get_line(self, line_index: int) -> Text: line_string = self.document.get_line(line_index) return Text(line_string, end="") - def render_line(self, y: int) -> Strip: - """Render a single line of the TextArea. Called by Textual. + def _prepare_for_repaint(self) -> Collection[Region]: + with self._preserved_refresh_state(): + return self._do_prepare_for_repaint() + + def _do_prepare_for_repaint(self) -> Collection[Region]: + # TODO: + # This is being used as a hook to prepare for an imminent screen + # update, which is not the intended use of this method. A proper + # 'prepare for screen update' hook. + + is_syntax_aware = self.is_syntax_aware + if is_syntax_aware: + highlights = self._highlights + highlights.set_snapshot( + lines=self.document.copy_of_lines(), + query=self._highlight_query, + tree=self.document.current_syntax_tree, + tab_size=self.indent_width, + ) - Args: - y: Y Coordinate of line relative to the widget region. + prev_render_state = self._new_render_state + render_state = self._pre_render_lines() + regions = render_state.make_diff_regions(self, prev_render_state) + self._old_render_state = render_state - Returns: - A rendered line. - """ - theme = self._theme - if theme: - theme.apply_css(self) + self._repaint_regions.clear() + self._dirty_regions.clear() - wrapped_document = self.wrapped_document - scroll_x, scroll_y = self.scroll_offset + regions = [r.as_region(0) for r in regions if r.width > 0] + if regions: + self.refresh(*regions) - # Account for how much the TextArea is scrolled. - y_offset = y + scroll_y + def render_lines(self, crop: Region) -> list[Strip]: + if len(self._dirty_regions) == 0: + self._new_render_state = self._old_render_state + return super().render_lines(crop) - # If we're beyond the height of the document, render blank lines - out_of_bounds = y_offset >= wrapped_document.height + ret = super().render_lines(crop) + self._dirty_regions.clear() - if out_of_bounds: - return Strip.blank(self.size.width) + self._new_render_state = self._old_render_state + + return ret - # Get the line corresponding to this offset + @contextmanager + def _preserved_refresh_state(self) -> ContextManager: + repaint_required = self._repaint_required try: - line_info = wrapped_document._offset_to_line_info[y_offset] - except IndexError: - line_info = None + yield + finally: + self._repaint_required = repaint_required + + def _pre_render_lines(self) -> PreRenderState: + + def build_doc_y_to_render_y_table() -> dict[int, int]: + """Build a dict mapping document y (row) to text area y.""" + if not visible_line_range: + return [] + + line_index, section_offset = offset_to_line_info[scroll_y] + line_count = self.document.line_count + table: dicxt[int, int] = {} + y = -section_offset + while line_index < line_count: + table[line_index] = y + y += len(wrap_offsets[line_index]) + 1 + if y not in visible_line_range: + break + line_index += 1 + + return table + + def doc_pos_to_xy(row, column) -> tuple[int, int]: + try: + y = doc_y_to_render_y_table[row] + except KeyError: + return -1, -1 + + offsets = wrap_offsets[row] + offset = 0 + physical_column = column + for offset in offsets: + if column >= offset: + y += 1 + physical_column = column - offset + else: + break + return physical_column, y + + def snip_syntax(line_highlights, start, end): + snipped = [] + for hl_start, hl_end, hl_name in line_highlights: + if hl_end is None: + hl_end = end + hl_start = max(start, hl_start) - start + hl_end = min(end, hl_end) - start + if hl_start <= hl_end: + snipped.append((hl_start, hl_end, hl_name)) + return snipped + + def add_pre_renders(y: int) -> None: + + nonlocal cursor_line_text + + def get_line_sections(section_offset): + sections = get_sections(line_index) + if not soft_wrap: + text = sections[0] + if scroll_x > 0: + # If horizontally scrolled, 'pretend' this line is the + # second of a wrapped line. + section_offset = 1 + sections = [ + text[:scroll_x], + text[scroll_x : text_width + scroll_x], + ] + else: + sections = [text[:text_width]] + return sections, section_offset + + def calculate_selection_limits() -> tuple[int | None, int | None]: + """Calculate the selection limits for the current document line.""" + sel_start = sel_end = None + if line_index in selection_line_range: + if len(selection_line_range) == 1: + sel_start = selection_top_column + sel_len = selection_bottom_column - selection_top_column + 1 + elif line_index == selection_top_row: + sel_start = selection_top_column + sel_len = MAX_LINE_LENGTH_X2 + elif line_index == selection_bottom_row: + sel_start = 0 + sel_len = selection_bottom_column + else: + sel_start = 0 + sel_len = MAX_LINE_LENGTH_X2 + sel_end = sel_start + sel_len + return sel_start, sel_end + + def create_gutter_text(): + if gutter_width > 0: + if section_offset == 0 or not soft_wrap: + gutter_content = str(line_index + line_number_start) + gutter_text = f"{gutter_content:>{gutter_width_no_margin}} " + else: + gutter_text = blank_gutter_text + else: + gutter_text = blank_gutter_text + return gutter_text + + def create_select_range() -> range: + """Calculate the selection range for the current line section.""" + + nonlocal sel_start, sel_end, line_text + + select_range = EMPTY_RANGE + if sel_start is not None: + if sel_start <= sel_end and 0 <= sel_start <= text_length: + range_end = min(sel_end, text_length) + if not line_text and yy != cursor_y: + # Make sure that empty line show up as selected. + line_text = TextReprString("▌") + select_range = range(1, 0, -1) + elif sel_start < sel_end: + # The selection covers part of this line section. + select_range = range(sel_start, range_end) + + # Adjust start/end ready for the next line section. + sel_start = max(0, sel_start - text_length) + sel_end = max(0, sel_end - text_length) + + return select_range + + y_offset = y + scroll_y + if y_offset >= len(offset_to_line_info): + repr_line = TextReprString.create("") + text_repr_strings[y] = repr_line + rendered_lines.append(PreRenderLine(repr_line, "", range(0), [], False)) + return - if line_info is None: - return Strip.blank(self.size.width) + line_index, section_offset = offset_to_line_info[y_offset] + sections, section_offset = get_line_sections(section_offset) + sel_start, sel_end = calculate_selection_limits() + gutter_text = create_gutter_text() + doc_line_highlights = highlights[line_index] if highlights else [] + + syn_start = 0 + line_highlights = [] + tab_offset = 0 + for yy, doc_text in enumerate(sections, y - section_offset): + line_text = TextReprString.create( + doc_text, self.indent_width, tab_offset=tab_offset + ) + tab_offset = self.indent_width - len(line_text) % self.indent_width + text_length = len(line_text) + syn_end = syn_start + len(line_text) + if yy in visible_line_range and yy not in text_repr_strings: + select_range = create_select_range() + if highlights: + line_highlights = snip_syntax( + doc_line_highlights, syn_start, syn_end + ) + if yy == cursor_y: + cursor_line_text = line_text + rendered_lines.append( + PreRenderLine( + line_text, + gutter_text, + select_range, + line_highlights, + cursor_row == line_index, + ) + ) + text_repr_strings[yy] = line_text + gutter_text = blank_gutter_text + syn_start = syn_end - line_index, section_offset = line_info + # Create local references to oft-used attributes and methods. + full_width = self.size.width + gutter_width = self.gutter_width + text_width = full_width - gutter_width + highlights = self._highlights if self._highlights and self.theme else None + line_count = self.size.height + if line_count < 1: + # We have no visible lines. + return PreRenderState([]) + + line_number_start = self.line_number_start + scroll_x, scroll_y = self.scroll_offset + selection = self.selection + soft_wrap = self.soft_wrap + wrapped_document = self.wrapped_document - line = self.get_line(line_index) - line_character_count = len(line) - line.tab_size = self.indent_width - line.set_length(line_character_count + 1) # space at end for cursor - virtual_width, _virtual_height = self.virtual_size + # Local references to the wrapped document attribute and methods + get_sections = wrapped_document.get_sections + line_index_to_offsets = wrapped_document._line_index_to_offsets + offset_to_line_info = wrapped_document._offset_to_line_info + wrap_offsets = wrapped_document._wrap_offsets + wrapped_height = wrapped_document.height # Note: slowish property. - selection = self.selection - start, end = selection - cursor_row, cursor_column = end + visible_line_range = range(line_count) + if self.show_line_numbers: + gutter_width_no_margin = gutter_width - 2 + blank_gutter_text = " " * (gutter_width_no_margin + 2) + else: + gutter_width = 0 + blank_gutter_text = "" selection_top, selection_bottom = sorted(selection) selection_top_row, selection_top_column = selection_top selection_bottom_row, selection_bottom_column = selection_bottom + if selection.is_empty: + selection_line_range = EMPTY_RANGE + else: + selection_line_range = range(selection_top_row, selection_bottom_row + 1) + + doc_y_to_render_y_table = build_doc_y_to_render_y_table() + cursor_row, cursor_column = selection.end + cursor_char_x, cursor_y = doc_pos_to_xy(*selection.end) + cursor_x = cursor_char_x + if self._matching_bracket_location is not None: + bracket_char_x, bracket_y = doc_pos_to_xy(*self._matching_bracket_location) + bracket_x = bracket_char_x + else: + bracket_char_x = bracket_x = bracket_y = -1 + if not soft_wrap: + cursor_char_x -= scroll_x + bracket_x -= scroll_x + bracket_char_x -= scroll_x + + cursor_line_text: str | None = None + text_repr_strings: dict[int, TextReprString] = {} + rendered_lines: list[PreRenderLine] = [] + for y in visible_line_range: + if y not in text_repr_strings: + add_pre_renders(y) + + if bracket_char_x >= 0: + try: + bracket_x = text_repr_strings[bracket_y].cell_offset(bracket_char_x) + except IndexError: + # This can occur when the cursor is beyond RHS. + bracket_x = bracket_char_x = -1 + else: + bracket_x = -1 + cursor_width = 1 + if cursor_line_text is not None: # Should always be true. + cursor_char_x = cursor_line_text.adjust_index_for_tabs(cursor_char_x) + cursor_x = cursor_line_text.cell_offset(cursor_char_x) + if cursor_char_x < len(cursor_line_text): + cursor_width = cursor_line_text.cell_width(cursor_char_x) + + return PreRenderState( + lines=rendered_lines, + cursor=(self.cursor_is_on, cursor_x, cursor_y, cursor_width, cursor_char_x), + size=self.size, + gutter_width=gutter_width, + bracket=(bracket_x, bracket_y, bracket_char_x), + ) - 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) + def render_line(self, y: int) -> Strip: + """Render a single line of the TextArea. Called by Textual. + + Args: + y: Y Coordinate of line relative to the widget region. + + Returns: + A rendered line. + """ + state = self._old_render_state + if y >= len(state.lines): + return Strip.blank(self.size.width) + if y + self.scroll_offset.y >= self.wrapped_document.height: + return Strip.blank(self.size.width) + + # Get the text for this physical line. + pre_render = state.lines[y] + text = pre_render.text + rich_line = Text(text + " ", end="") + if theme := self._theme: + theme.apply_css(self) + if cursor_highlighted := pre_render.cursor_highlighted: + cursor_line_style = theme.cursor_line_style if theme else None + rich_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 + # If this line is part of the selection, add selection styling. + if sel_range := pre_render.select_range: selection_style = theme.selection_style if theme else None - cursor_row, _ = end - 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)) + if selection_style is not None: + if sel_range.step < 0: + rich_line = Text( + "▌", end="", style=Style(color=selection_style.bgcolor) + ) 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, - ) - else: - # 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) + rich_line.stylize( + selection_style, start=sel_range.start, end=sel_range.stop + ) - highlights = self._highlights - if highlights and theme: - line_bytes = _utf8_encode(line.plain) - byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) + # Add syntax highlighting. + if (line_highlights := pre_render.syntax) and theme: 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) if node_style is not None: - line.stylize( - node_style, - byte_to_codepoint.get(highlight_start, 0), - byte_to_codepoint.get(highlight_end) if highlight_end else None, - ) + rich_line.stylize(node_style, highlight_start, highlight_end) - # 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 and start == end - ) + # Highlight the cursor, taking care when it is on a bracket. + draw_matched_brackets = False + if self._matching_bracket_location is not None: + if self.match_cursor_bracket: + draw_matched_brackets = self.selection.is_empty - if cursor_row == line_index: - draw_cursor = ( - self.has_focus - and not self.cursor_blink - or (self.cursor_blink and self._cursor_visible) - ) + cursor_is_on, cell_x, cell_y, _, char_x = state.cursor + if cell_y == y: if draw_matched_brackets: 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: + rich_line.stylize(matching_bracket_style, char_x, char_x + 1) + if cursor_is_on: 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 theme and bracket_match_row == line_index: + width = text.logical_character_width(char_x) + rich_line.stylize(cursor_style, char_x, char_x + width) + + # Add styling for a matching bracket. + _, bracket_cell_y, bracket_char_x = state.bracket + if theme and draw_matched_brackets: + if bracket_cell_y == y and bracket_char_x >= 0: matching_bracket_style = theme.bracket_matching_style if matching_bracket_style: - line.stylize( - matching_bracket_style, - bracket_match_column, - bracket_match_column + 1, + rich_line.stylize( + matching_bracket_style, bracket_char_x, bracket_char_x + 1 ) - # Build the gutter text for this line + # Add gutter text. gutter_width = self.gutter_width + gutter = Text("", end="") + gutter_style = theme.gutter_style if self.show_line_numbers: - if cursor_row == line_index: + if cursor_highlighted: gutter_style = theme.cursor_line_gutter_style - else: - gutter_style = theme.gutter_style - - gutter_width_no_margin = gutter_width - 2 - gutter_content = ( - str(line_index + self.line_number_start) if section_offset == 0 else "" - ) - gutter = Text( - f"{gutter_content:>{gutter_width_no_margin}} ", - style=gutter_style or "", - end="", - ) - else: - gutter = Text("", end="") - - # TODO: Lets not apply the division each time through render_line. - # We should cache sections with the edit counts. - wrap_offsets = wrapped_document.get_offsets(line_index) - if wrap_offsets: - sections = line.divide(wrap_offsets) # TODO cache result with edit count - line = sections[section_offset] - line_tab_widths = wrapped_document.get_tab_widths(line_index) - line.end = "" - - # Get the widths of the tabs corresponding only to the section of the - # line that is currently being rendered. We don't care about tabs in - # other sections of the same line. - - # Count the tabs before this section. - tabs_before = 0 - for section_index in range(section_offset): - tabs_before += sections[section_index].plain.count("\t") - - # Count the tabs in this section. - tabs_within = line.plain.count("\t") - section_tab_widths = line_tab_widths[ - tabs_before : tabs_before + tabs_within - ] - line = expand_text_tabs_from_widths(line, section_tab_widths) - else: - line.expand_tabs(self.indent_width) + gutter = Text(pre_render.gutter_text, style=gutter_style or "", end="") - base_width = ( - self.scrollable_content_region.size.width - if self.soft_wrap - else max(virtual_width, self.region.size.width) - ) - target_width = base_width - self.gutter_width + # Create strips for the gutter and line text. + base_width = self.scrollable_content_region.size.width + if not self.soft_wrap: + base_width = max(self.virtual_size.width, self.region.size.width) + target_width = base_width - gutter_width console = self.app.console gutter_segments = console.render(gutter) - text_segments = list( - console.render(line, console.options.update_width(target_width)) + console.render(rich_line, console.options.update_width(target_width)) ) - gutter_strip = Strip(gutter_segments, cell_length=gutter_width) text_strip = Strip(text_segments) - # Crop the line to show only the visible part (some may be scrolled out of view) - if not self.soft_wrap: - text_strip = text_strip.crop(scroll_x, scroll_x + virtual_width) - - # Stylize the line the cursor is currently on. - if cursor_row == line_index: + # Pad the line using the cursor line or base style. + if cursor_highlighted: line_style = cursor_line_style else: line_style = theme.base_style if theme else None - text_strip = text_strip.extend_cell_length(target_width, line_style) - strip = Strip.join([gutter_strip, text_strip]).simplify() + strip = Strip.join([gutter_strip, text_strip]).simplify() return strip.apply_style( theme.base_style if theme and theme.base_style is not None @@ -1656,7 +2295,7 @@ async def _on_key(self, event: events.Key) -> None: self.screen.focus_next() return if self.indent_type == "tabs": - insert_values["tab"] = "\t" + insert_values["tab"] = TAB else: insert_values["tab"] = " " * self._find_columns_to_next_tab_stop() @@ -1745,12 +2384,12 @@ def _toggle_cursor_blink_visible(self) -> None: """Toggle visibility of the cursor for the purposes of 'cursor blink'.""" self._cursor_visible = not self._cursor_visible _, cursor_y = self._cursor_offset - self.refresh_lines(cursor_y) + self._trigger_repaint() def _watch__cursor_visible(self) -> None: """When the cursor visibility is toggled, ensure the row is refreshed.""" _, cursor_y = self._cursor_offset - self.refresh_lines(cursor_y) + self._trigger_repaint() def _restart_blink(self) -> None: """Reset the cursor blink timer.""" @@ -1763,6 +2402,11 @@ def _pause_blink(self, visible: bool = True) -> None: self._cursor_visible = visible self.blink_timer.pause() + @property + def cursor_is_on(self) -> bool: + """True if the cursor currently visible.""" + return self.has_focus and (self._cursor_visible or not self.cursor_blink) + 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) @@ -1894,6 +2538,7 @@ def move_cursor( self.scroll_cursor_visible(center) self.history.checkpoint() + self._trigger_repaint() def move_cursor_relative( self, @@ -2520,41 +3165,157 @@ def action_delete_word_right(self) -> None: self._delete_via_keyboard(end, to_location) +class IndexMapper: + """An infinite list-like mapping from one index to another.""" + + def __init__(self, base_map: MutableSequence[int]): + self._base_map = base_map or [0] + + def __getitem__(self, index: int | None) -> int | None: + if index is None: + return None + try: + return self._base_map[index] + except IndexError: + return self._base_map[-1] + index - len(self._base_map) + 1 + + +class _IdentityIndexMapper(IndexMapper): + """A `Mapper` that maps 0->0, 1->1, ...""" + + def __init__(self): + pass + + def __getitem__(self, index: int | None) -> int | None: + return index + + @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. +def build_byte_to_tab_expanded_string_table(text: str, tab_size: int) -> Mapper: + """Build a mapping of utf-8 byte offsets to TAB-expanded character positions. Args: - data: utf-8 bytes. + text: The string to map. + tab_size: The size setting to use for TAB expansion. Returns: - A `dict[int, int]` mapping byte indices to codepoint indices within `data`. + A list-like object mapping byte index to character index. """ - byte_to_codepoint: dict[int, int] = {} - 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 + if not text: + return identity_index_mapper + + data = text.encode("utf-8") + if len(data) == len(text) and TAB not in text: + return identity_index_mapper + + offsets: MutableSequence[int] = array("L") + char_offset = 0 + next_tabstop = 0 + for c in text: + offsets.append(char_offset) + if c == TAB: + char_offset = next_tabstop else: - raise ValueError(f"Invalid UTF-8 byte: {first_byte}") + ord_c = ord(c) + if ord_c >= 0x80: + offsets.append(char_offset) + if ord_c >= 0x800: + offsets.append(char_offset) + if ord_c >= 0x10000: + offsets.append(char_offset) + char_offset += 1 + + if char_offset >= next_tabstop: + next_tabstop += tab_size + + return IndexMapper(offsets) + + +@contextmanager +def temporary_query_point_range( + query: Query, + start_point: tuple[int, int] | None, + end_point: tuple[int, int] | None, +) -> ContextManager[None]: + """Temporarily change the start and/or end point for a tree-sitter Query. + + Args: + query: The tree-sitter Query. + start_point: The (row, column byte) to start the query at. + end_point: The (row, column byte) to end the query at. + """ + # Note: Although not documented for the tree-sitter Python API, an + # end-point of (0, 0) means 'end of document'. + default_point_range = [(0, 0), (0, 0)] + + point_range = list(default_point_range) + if start_point is not None: + point_range[0] = start_point + if end_point is not None: + point_range[1] = end_point + query.set_point_range(point_range) + try: + yield None + finally: + query.set_point_range(default_point_range) + + +def build_difference_region( + line_index: int, + a: Sequence, + b: Sequence, + x_offset: int = 0, +) -> LineRegion: + """Compare 2 sequences to create a line region covering the differences. + + The caller should only use this if a nd b are known to differ. + """ + if a is None: + return LineRegion(x_offset, line_index, len(b)) + elif b is None: + return LineRegion(x_offset, line_index, len(a)) + + start = 0 + for start, (ca, cb) in enumerate(zip(a, b)): + if ca != cb: + break + end = max(len(a), len(b)) + return LineRegion(start + x_offset, line_index, end - start + 1) + + +def intersect_ranges(range_a, range_b) -> list[range]: + if range_a.step < 0: + range_b = range(0) + if range_b.step < 0: + range_b = range(0) + if range_a.start > range_b.start: + range_a, range_b = range_b, range_a + elif range_a.start == range_b.start: + if range_a.stop > range_b.stop: + range_a, range_b = range_b, range_a + overlap = range_a.stop - range_b.start + if overlap <= 0: + # Ranges do not overlap + before = range_a + common = [] + after = range_b + else: + before = range(range_a.start, range_a.stop - overlap) + common = range(range_a.stop - overlap, range_a.stop) + after = range(range_b.start + overlap, range_b.stop) + return before, common, after + + +def character_cell_size(char: str) -> int: + """Calculate the cell size for a character. + + This is athing wrapper around get_character_cell_size, which treats the TAB + character as width == 1. + """ + if char == TAB: + return 1 + else: + return get_character_cell_size(char) - code_point_offset += 1 - # Mapping for the end of the string - byte_to_codepoint[current_byte_offset] = code_point_offset - return byte_to_codepoint +identity_index_mapper = _IdentityIndexMapper() diff --git a/tests/document/test_document_delete.py b/tests/document/test_document_delete.py index d00fa686c9..0707062f7f 100644 --- a/tests/document/test_document_delete.py +++ b/tests/document/test_document_delete.py @@ -16,7 +16,11 @@ def document(): def test_delete_single_character(document): replace_result = document.replace_range((0, 0), (0, 1), "") - assert replace_result == EditResult(end_location=(0, 0), replaced_text="I") + assert replace_result == EditResult( + end_location=(0, 0), + replaced_text="I", + alt_dirty_line=(0, range(0, 16)), + ) assert document.lines == [ " must not fear.", "Fear is the mind-killer.", @@ -28,7 +32,11 @@ def test_delete_single_character(document): def test_delete_single_newline(document): """Testing deleting newline from right to left""" replace_result = document.replace_range((1, 0), (0, 16), "") - assert replace_result == EditResult(end_location=(0, 16), replaced_text="\n") + assert replace_result == EditResult( + end_location=(0, 16), + replaced_text="\n", + dirty_lines=range(0, 3), + ) assert document.lines == [ "I must not fear.Fear is the mind-killer.", "I forgot the rest of the quote.", @@ -44,6 +52,7 @@ def test_delete_near_end_of_document(document): replaced_text="Fear is the mind-killer.\n" "I forgot the rest of the quote.\n" "Sorry Will.", + dirty_lines=range(1, 2), ) assert document.lines == [ "I must not fear.", @@ -56,6 +65,7 @@ def test_delete_clearing_the_document(document): assert replace_result == EditResult( end_location=(0, 0), replaced_text=TEXT, + dirty_lines=range(0, 1), ) assert document.lines == [""] @@ -65,6 +75,7 @@ def test_delete_multiple_characters_on_one_line(document): assert replace_result == EditResult( end_location=(0, 2), replaced_text="must ", + alt_dirty_line=(0, range(2, 16)), ) assert document.lines == [ "I not fear.", @@ -80,6 +91,7 @@ def test_delete_multiple_lines_partially_spanned(document): assert replace_result == EditResult( end_location=(0, 2), replaced_text="must not fear.\nFear is the mind-killer.\nI ", + dirty_lines=range(0, 2), ) assert document.lines == [ "I forgot the rest of the quote.", @@ -93,6 +105,7 @@ def test_delete_end_of_line(document): assert replace_result == EditResult( end_location=(0, 16), replaced_text="\n", + dirty_lines=range(0, 3), ) assert document.lines == [ "I must not fear.Fear is the mind-killer.", @@ -107,6 +120,7 @@ def test_delete_single_line_excluding_newline(document): assert replace_result == EditResult( end_location=(2, 0), replaced_text="I forgot the rest of the quote.", + alt_dirty_line=(2, range(0, 31)), ) assert document.lines == [ "I must not fear.", @@ -122,6 +136,7 @@ def test_delete_single_line_including_newline(document): assert replace_result == EditResult( end_location=(2, 0), replaced_text="I forgot the rest of the quote.\n", + dirty_lines=range(2, 3), ) assert document.lines == [ "I must not fear.", @@ -139,7 +154,11 @@ def test_delete_single_line_including_newline(document): def test_delete_end_of_file_newline(): document = Document(TEXT_NEWLINE_EOF) replace_result = document.replace_range((2, 0), (1, 24), "") - assert replace_result == EditResult(end_location=(1, 24), replaced_text="\n") + assert replace_result == EditResult( + end_location=(1, 24), + replaced_text="\n", + dirty_lines=range(1, 2), + ) assert document.lines == [ "I must not fear.", "Fear is the mind-killer.", diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press0].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press0].svg new file mode 100644 index 0000000000..54027c15fb --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press0].svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def a_function_with_a_long_name(lon +2  if long_argument_name == 42:    +3  print('I think you may have +4  else:                           +5  print('You need the one who + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press1].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press1].svg new file mode 100644 index 0000000000..3a968d62e1 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press1].svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  ef a_function_with_a_long_name(long +2  if long_argument_name == 42:     +3  print('I think you may have  +4  else:                            +5  print('You need the one who  + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press2].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press2].svg new file mode 100644 index 0000000000..69e4848909 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press2].svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  a_long_name(long_argument_name): +2  _name == 42:                        +3  nk you may have figured it out!')   +4   +5  eed the one who is to come after me + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press3].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press3].svg new file mode 100644 index 0000000000..dccf5641e1 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press3].svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  ng_name(long_argument_name):        +2  e == 42:                            +3  ou may have figured it out!')       +4   +5  the one who is to come after me!') + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press4].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press4].svg new file mode 100644 index 0000000000..f851a46578 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press4].svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  ng_name(long_argument_name):        +2  e == 42:                            +3  ou may have figured it out!')       +4   +5  the one who is to come after me!') + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press0].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press0].svg new file mode 100644 index 0000000000..3d06b84838 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press0].svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  n_with_a_long_name(long_argument_na +2  rgument_name == 42:                 +3  ( +4  'You have figured it out!') +5   +6  (                                   +7  You need the one who is to come aft + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press1].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press1].svg new file mode 100644 index 0000000000..b04b284e67 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press1].svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  _with_a_long_name(long_argument_nam +2  gument_name == 42:                  +3   +4  'You have figured it out!')# +5   +6   +7  ou need the one who is to come afte + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press2].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press2].svg new file mode 100644 index 0000000000..34dd88c014 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press2].svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def a_function_with_a_long_name(lon +2  if long_argument_name == 42:    +3  print(                      +4  'You have figure +5  else:                           +6  print( +7  'You need the one who i + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[bash].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[bash].svg index fac1d695c7..13a54e6f87 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[bash].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[bash].svg @@ -19,457 +19,457 @@ font-weight: 700; } - .terminal-1868575305-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1868575305-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1868575305-r1 { fill: #121212 } -.terminal-1868575305-r2 { fill: #0178d4 } -.terminal-1868575305-r3 { fill: #c5c8c6 } -.terminal-1868575305-r4 { fill: #c2c2bf } -.terminal-1868575305-r5 { fill: #272822 } -.terminal-1868575305-r6 { fill: #75715e } -.terminal-1868575305-r7 { fill: #f8f8f2 } -.terminal-1868575305-r8 { fill: #90908a } -.terminal-1868575305-r9 { fill: #e6db74 } -.terminal-1868575305-r10 { fill: #a6e22e } -.terminal-1868575305-r11 { fill: #f92672 } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #c2c2bf } +.terminal-r5 { fill: #272822 } +.terminal-r6 { fill: #75715e } +.terminal-r7 { fill: #f8f8f2 } +.terminal-r8 { fill: #90908a } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #a6e22e } +.terminal-r11 { fill: #f92672 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  1  #!/bin/bash -  2   -  3  # Variables -  4  name="John" -  5  age=30                                                                  -  6  is_student=true                                                         -  7   -  8  # Printing variables -  9  echo"Hello, $name! You are $age years old." - 10   - 11  # Conditional statements - 12  if [[ $age -ge 18 && $is_student == true ]]; then - 13  echo"You are an adult student." - 14  elif [[ $age -ge 18 ]]; then - 15  echo"You are an adult." - 16  else - 17  echo"You are a minor." - 18  fi - 19   - 20  # Arrays - 21  numbers=(1 2 3 4 5)                                                     - 22  echo"Numbers: ${numbers[@]}" - 23   - 24  # Loops - 25  for num in"${numbers[@]}"do - 26  echo"Number: $num" - 27  done - 28   - 29  # Functions - 30  greet() {                                                               - 31    local name=$1                                                         - 32  echo"Hello, $name!" - 33  }                                                                       - 34  greet"Alice" - 35   - 36  # Command substitution - 37  current_date=$(date +%Y-%m-%d)                                          - 38  echo"Current date: $current_date" - 39   - 40  # File operations - 41  touch file.txt                                                          - 42  echo"Some content" > file.txt                                          - 43  cat file.txt                                                            - 44   - 45  # Conditionals with file checks - 46  if [[ -f file.txt ]]; then - 47  echo"file.txt exists." - 48  else - 49  echo"file.txt does not exist." - 50  fi - 51   - 52  # Case statement - 53  case $age in - 54    18)                                                                   - 55  echo"You are 18 years old." - 56      ;;                                                                  - 57    30)                                                                   - 58  echo"You are 30 years old." - 59      ;;                                                                  - 60    *)                                                                    - 61  echo"You are neither 18 nor 30 years old." - 62      ;;                                                                  - 63  esac - 64   - 65  # While loop - 66  counter=0                                                               - 67  while [[ $counter -lt 5 ]]; do - 68  echo"Counter: $counter" - 69    ((counter++))                                                         - 70  done - 71   - 72  # Until loop - 73  until [[ $counter -eq 0 ]]; do - 74  echo"Counter: $counter" - 75    ((counter--))                                                         - 76  done - 77   - 78  # Heredoc - 79  cat << EOF - 80  This is a heredoc.  - 81  It allows you to write multiple lines of text.  - 82  EOF  - 83   - 84  # Redirection - 85  ls > file_list.txt                                                      - 86  grep"file" file_list.txt > filtered_list.txt                           - 87   - 88  # Pipes - 89  cat file_list.txt | wc -l                                               - 90   - 91  # Arithmetic operations - 92  result=$((10 + 5))                                                      - 93  echo"Result: $result" - 94   - 95  # Exporting variables - 96  export DB_PASSWORD="secret" - 97   - 98  # Sourcing external files - 99  source config.sh                                                        -100   - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  1  #!/bin/bash +  2   +  3  # Variables +  4  name="John" +  5  age=30                                                                  +  6  is_student=true                                                         +  7   +  8  # Printing variables +  9  echo"Hello, $name! You are $age years old." + 10   + 11  # Conditional statements + 12  if [[ $age -ge 18 && $is_student == true ]]; then + 13  echo"You are an adult student." + 14  elif [[ $age -ge 18 ]]; then + 15  echo"You are an adult." + 16  else + 17  echo"You are a minor." + 18  fi + 19   + 20  # Arrays + 21  numbers=(1 2 3 4 5)                                                     + 22  echo"Numbers: ${numbers[@]}" + 23   + 24  # Loops + 25  for num in"${numbers[@]}"do + 26  echo"Number: $num" + 27  done + 28   + 29  # Functions + 30  greet() {                                                               + 31    local name=$1                                                         + 32  echo"Hello, $name!" + 33  }                                                                       + 34  greet"Alice" + 35   + 36  # Command substitution + 37  current_date=$(date +%Y-%m-%d)                                          + 38  echo"Current date: $current_date" + 39   + 40  # File operations + 41  touch file.txt                                                          + 42  echo"Some content" > file.txt                                          + 43  cat file.txt                                                            + 44   + 45  # Conditionals with file checks + 46  if [[ -f file.txt ]]; then + 47  echo"file.txt exists." + 48  else + 49  echo"file.txt does not exist." + 50  fi + 51   + 52  # Case statement + 53  case $age in + 54    18)                                                                   + 55  echo"You are 18 years old." + 56      ;;                                                                  + 57    30)                                                                   + 58  echo"You are 30 years old." + 59      ;;                                                                  + 60    *)                                                                    + 61  echo"You are neither 18 nor 30 years old." + 62      ;;                                                                  + 63  esac + 64   + 65  # While loop + 66  counter=0                                                               + 67  while [[ $counter -lt 5 ]]; do + 68  echo"Counter: $counter" + 69    ((counter++))                                                         + 70  done + 71   + 72  # Until loop + 73  until [[ $counter -eq 0 ]]; do + 74  echo"Counter: $counter" + 75    ((counter--))                                                         + 76  done + 77   + 78  # Heredoc + 79  cat << EOF + 80  This is a heredoc. + 81  It allows you to write multiple lines of text. + 82  EOF                                                                     + 83   + 84  # Redirection + 85  ls > file_list.txt                                                      + 86  grep"file" file_list.txt > filtered_list.txt                           + 87   + 88  # Pipes + 89  cat file_list.txt | wc -l                                               + 90   + 91  # Arithmetic operations + 92  result=$((10 + 5))                                                      + 93  echo"Result: $result" + 94   + 95  # Exporting variables + 96  export DB_PASSWORD="secret" + 97   + 98  # Sourcing external files + 99  source config.sh                                                        +100   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[markdown].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[markdown].svg index 95053a32f0..4106c5cc8d 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[markdown].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[markdown].svg @@ -268,14 +268,14 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - 1  Heading  - 2  =======  + 1  Heading + 2  =======  3   - 4  Sub-heading  - 5  -----------  + 4  Sub-heading + 5  -----------  6    7  ###Heading  8   @@ -297,8 +297,8 @@ 24   25  Horizontal rule:                                                         26   -27  ---  -28    +27  --- +28   29  Bullet list:                                                             30   31    * apples                                                               diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[yaml].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[yaml].svg index f12b6d629f..e4de99559e 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[yaml].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[yaml].svg @@ -19,210 +19,210 @@ font-weight: 700; } - .terminal-2714046411-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2714046411-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2714046411-r1 { fill: #121212 } -.terminal-2714046411-r2 { fill: #0178d4 } -.terminal-2714046411-r3 { fill: #c5c8c6 } -.terminal-2714046411-r4 { fill: #c2c2bf } -.terminal-2714046411-r5 { fill: #272822 } -.terminal-2714046411-r6 { fill: #75715e } -.terminal-2714046411-r7 { fill: #f8f8f2 } -.terminal-2714046411-r8 { fill: #90908a } -.terminal-2714046411-r9 { fill: #f92672;font-weight: bold } -.terminal-2714046411-r10 { fill: #e6db74 } -.terminal-2714046411-r11 { fill: #ae81ff } -.terminal-2714046411-r12 { fill: #66d9ef;font-style: italic; } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #c2c2bf } +.terminal-r5 { fill: #272822 } +.terminal-r6 { fill: #75715e } +.terminal-r7 { fill: #f8f8f2 } +.terminal-r8 { fill: #90908a } +.terminal-r9 { fill: #f92672;font-weight: bold } +.terminal-r10 { fill: #e6db74 } +.terminal-r11 { fill: #ae81ff } +.terminal-r12 { 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/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-False].svg new file mode 100644 index 0000000000..db53ec82cc --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-False].svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  defūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     +4  else:                                                                 +5  print('You need the one who is to come after me!')                + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-True].svg new file mode 100644 index 0000000000..5aacac14d8 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-True].svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  defūnicode_ち(arg_āāち="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-False].svg new file mode 100644 index 0000000000..7ec31a9a52 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-False].svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def ūnicode_(arg_āāち="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     +4  else:                                                                 +5  print('You need the one who is to come after me!')                + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-True].svg new file mode 100644 index 0000000000..24cefe9c7d --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-True].svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def ūnicode_(arg_āāち="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-False].svg new file mode 100644 index 0000000000..e05b098615 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-False].svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def ūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     +4  else:                                                                 +5  print('You need the one who is to come after me!')                + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-True].svg new file mode 100644 index 0000000000..2a0f017268 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-True].svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def ūnicode_ち(arg_āāち="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-False].svg new file mode 100644 index 0000000000..73929fc504 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-False].svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def ūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     +4  else:                                                                 +5  print('You need the one who is to come after me!')                + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-True].svg new file mode 100644 index 0000000000..c8a7fe6dea --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-True].svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def ūnicode_ち(arg_āāち="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-False].svg new file mode 100644 index 0000000000..42723d1078 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-False].svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def ūnicode_ち(arg_āā="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     +4  else:                                                                 +5  print('You need the one who is to come after me!')                + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-True].svg new file mode 100644 index 0000000000..bb371e634d --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-True].svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def ūnicode_ち(arg_āā="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-False].svg new file mode 100644 index 0000000000..3d5d2872cb --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-False].svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def ūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     +4  else:                                                                 +5  print('You need the one who is to come after me!')                + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-True].svg new file mode 100644 index 0000000000..f15a8676ff --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-True].svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1  def ūnicode_ち(arg_āāち="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index dd7b5eeb12..2ab5410e2e 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -1320,6 +1320,22 @@ def setup_selection(pilot): ) +@pytest.mark.skip("Paul Ollis: I think pilot.click does not do what is needed.") +def test_text_area_mouse_cursor_placement(snap_compare): + def setup_selection(pilot): + text_area = pilot.app.query_one(TextArea) + text_area.theme = "css" + text_area.text = "Hello, world!\nNice to see you." + text_area.read_only = True + pilot.click(text_area, offset=(1, 4)) + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area.py", + run_before=setup_selection, + terminal_size=(30, 5), + ) + + @pytest.mark.syntax @pytest.mark.parametrize( "theme_name", [theme.name for theme in TextAreaTheme.builtin_themes()] @@ -1369,6 +1385,164 @@ def test_text_area_line_number_start(snap_compare): ) +@pytest.mark.parametrize( + "press", + [ + # Just before horizontal scrolling should occur. + ['ctrl+right'] * 2 + ['right'] * 1, + + # Scroll right by a single character. + ['ctrl+right'] * 2 + ['right'] * 2, + + # Maximum first line scroll; i.e. cursor at end of first line. + ['ctrl+right'] * 5, + + # Maximum scroll; i.e. cursor at end of fifth line. + ['down'] * 5 + ['left'] + ['right'], + + # Cursor on the closing parenthesis on the fifth line. The opening + # parenthesis is scrolled off the LHSdt. of the TextArea. + ['down'] * 4 + ['ctrl+right'] * 12 + ['right'] * 2, + ], +) +def test_text_area_horizontal_scrolling(snap_compare, press): + text = """def a_function_with_a_long_name(long_argument_name): + if long_argument_name == 42: + print('I think you may have figured it out!') + else: + print('You need the one who is to come after me!')""" + + def setup(pilot): + text_area = pilot.app.query_one(TextArea) + text_area.load_text(text) + text_area.language = "python" + text_area.show_line_numbers = True + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area.py", + run_before=setup, + press=press, + terminal_size=(42, 10), + ) + + +@pytest.mark.parametrize( + "press", + [ + # Cursor on the closing parenthesis on the fourth line. Matching + # parenthesis just about to scroll off LHS. + ['down'] * 3 + ['ctrl+right'] * 6 + ['right'] * 2, + + # Cursor on the closing parenthesis on the fourth line. Matching + # parenthesis just scrolled off LHS. + ['down'] * 3 + ['ctrl+right'] * 6 + ['right'] * 3 + ["left"], + + # Cursor on the opening parenthesis on the 6th line. The closing + # parenthesis is scrolled off the RHS. + ['down'] * 5 + ['ctrl+right'] * 1, + ], +) +def test_text_area_horizontal_scrolling_cursor_matching(snap_compare, press): + text = """def a_function_with_a_long_name(long_argument_name): + if long_argument_name == 42: + print( + 'You have figured it out!') # 42 + else: + print( + 'You need the one who is to come after me!')""" + + def setup(pilot): + text_area = pilot.app.query_one(TextArea) + text_area.load_text(text) + text_area.language = "python" + text_area.show_line_numbers = True + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area.py", + run_before=setup, + press=press, + terminal_size=(42, 10), + ) + + +@pytest.mark.parametrize("soft_wrap", [False, True]) +@pytest.mark.parametrize( + "press", [ + # Move the cursor on to the 'ū' character. + ["ctrl+right"] * 1 + ["right"], + + # Move past the 'ち' character then before it and the on to it. + ["ctrl+right"] * 2 + ["left"] * 2 + ["right"], + + # Move the cursor on to the opening parenthesis. + ["ctrl+right"] * 2, + + # Move the cursor on to the closing parenthesis. + ["ctrl+right"] * 10 + ["right"], + + # Move the cursor on to the second 'ち' character. + ["ctrl+right"] * 4 + ["left"], + + # Move the cursor on to the character below the second 'ち' character. + ["ctrl+right"] * 4 + ["left"] + ["down"], + ], +) +def test_text_area_unicode_wide_syntax_highlighting( + snap_compare, soft_wrap, press, +): + text = """def ūnicode_ち(arg_āāち="hi", arg_b="bye"): # Trailing comment + if unicode_ārg_name == 42: # Trailing comment + print('I think you māy have figured it out!') + else: + print('You need the one who is to come after me!')""" + + def setup(pilot): + text_area = pilot.app.query_one(TextArea) + text_area.load_text(text) + text_area.language = "python" + text_area.show_line_numbers = True + text_area.soft_wrap = soft_wrap + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area.py", + run_before=setup, + press=press, + terminal_size=(42, 14) if soft_wrap else (80, 10), + ) + + +@pytest.mark.skip("SVG rendering does not match terminal rendering") +@pytest.mark.parametrize( + "press", [ + ["ctrl+right"] * 1 + ["right"], + ["ctrl+right"] * 1 + ["right"] * 2, + ["ctrl+right"] * 1 + ["right"] * 3, + ["ctrl+right"] * 1 + ["right"] + ["left"], + ], +) +def test_text_area_unicode_zero_width_codepoint_syntax_highlighting( + snap_compare, press): + # The 'ū' in the function name below is actually formed by a 'u' followed + # by 2 u0304 characters, which are zero width, modifying codepoints. + text = """def ū̄nicode_func(arg_āāā="hi", arg_b="bye"): # Trailing comment + if unicode_ārg_name == 42: # Trailing comment + print('I think you māy have figured it out!') + else: + print('You need the one who is to come after me!')""" + + def setup(pilot): + text_area = pilot.app.query_one(TextArea) + text_area.load_text(text) + text_area.language = "python" + text_area.show_line_numbers = True + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area.py", + run_before=setup, + press=press, + ) + + def test_digits(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "digits.py") diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index e732680f0f..1b0ccf4cd0 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -115,9 +115,7 @@ async def test_insert_character_near_cursor_maintain_selection_offset( ], ) async def test_insert_newline_around_cursor_maintain_selection_offset( - cursor_location, - insert_location, - cursor_destination + cursor_location, insert_location, cursor_destination ): app = TextAreaApp() async with app.run_test(): @@ -187,6 +185,7 @@ async def test_insert_text_non_cursor_location_dont_maintain_offset(): assert result == EditResult( end_location=(4, 5), replaced_text="", + alt_dirty_line=(4, range(0, 5)), ) assert text_area.text == TEXT + "Hello" @@ -222,6 +221,7 @@ async def test_insert_multiline_text_maintain_offset(): assert result == EditResult( end_location=(3, 6), replaced_text="", + dirty_lines=range(2, 6), ) # The insert happens at the cursor (default location) @@ -252,6 +252,7 @@ async def test_replace_multiline_text(): assert result == EditResult( end_location=(3, 0), replaced_text=expected_replaced_text, + dirty_lines=range(1, 4), ) expected_content = """\ @@ -316,6 +317,7 @@ async def test_delete_within_line(): assert result == EditResult( end_location=(0, 6), replaced_text=" not", + alt_dirty_line=(0, range(7, 16)), ) expected_text = """\ @@ -368,6 +370,7 @@ async def test_delete_multiple_lines_selection_above(): assert result == EditResult( end_location=(1, 0), replaced_text=expected_replaced_text, + dirty_lines=range(1, 3), ) assert ( text_area.text @@ -447,7 +450,11 @@ async def test_insert_text_multiline_selection_top(select_from, select_to): end=(0, 2), ) - assert result == EditResult(end_location=(0, 5), replaced_text="AB") + assert result == EditResult( + end_location=(0, 5), + replaced_text="AB", + alt_dirty_line=(0, range(0, 8)), + ) # 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 @@ -494,7 +501,11 @@ async def test_insert_text_multiline_selection_bottom(select_from, select_to): start=(2, 0), end=(2, 3), ) - assert result == EditResult(end_location=(2, 1), replaced_text="KLM") + assert result == EditResult( + end_location=(2, 1), + replaced_text="KLM", + alt_dirty_line=(2, range(0, 5)), + ) # The 'NO' from the selection is still available on the # bottom selection line, however the 'KLM' is replaced @@ -521,6 +532,7 @@ async def test_delete_fully_within_selection(): assert result == EditResult( replaced_text="45", end_location=(0, 4), + alt_dirty_line=(0, range(4, 10)), ) # We deleted 45, but the other characters are still available assert text_area.selected_text == "236" @@ -540,6 +552,7 @@ async def test_replace_fully_within_selection(): assert result == EditResult( replaced_text="234", end_location=(0, 4), + alt_dirty_line=(0, range(2, 10)), ) assert text_area.selected_text == "XX56" From eb07f869134e1684d85350150b1ba7e1a155e21a Mon Sep 17 00:00:00 2001 From: Paul Ollis Date: Tue, 8 Apr 2025 09:59:25 +0100 Subject: [PATCH 8/9] Figured out I need to mark my tests as 'syntax'. --- tests/snapshot_tests/test_snapshots.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 2ab5410e2e..1075651da8 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -1385,6 +1385,7 @@ def test_text_area_line_number_start(snap_compare): ) +@pytest.mark.syntax @pytest.mark.parametrize( "press", [ @@ -1426,6 +1427,7 @@ def setup(pilot): ) +@pytest.mark.syntax @pytest.mark.parametrize( "press", [ @@ -1465,6 +1467,7 @@ def setup(pilot): ) +@pytest.mark.syntax @pytest.mark.parametrize("soft_wrap", [False, True]) @pytest.mark.parametrize( "press", [ @@ -1511,6 +1514,7 @@ def setup(pilot): ) +@pytest.mark.syntax @pytest.mark.skip("SVG rendering does not match terminal rendering") @pytest.mark.parametrize( "press", [ From 154483a53ea7487996b66da0bb66827024a95868 Mon Sep 17 00:00:00 2001 From: Paul Ollis Date: Sun, 20 Apr 2025 17:04:05 +0100 Subject: [PATCH 9/9] Run tree_sitter.parse with whole text. Drop 3.9 from supported tree-sitter Python versions. This allows predicates in SCM files to work. --- .github/workflows/pythonpackage.yml | 10 +- poetry.lock | 258 +++++++--- pyproject.toml | 36 +- .../document/_syntax_aware_document.py | 64 +-- src/textual/tree-sitter/highlights/python.scm | 8 +- src/textual/widgets/_text_area.py | 9 +- ...text_area_horizontal_scrolling[press0].svg | 20 +- ...text_area_horizontal_scrolling[press1].svg | 20 +- ...text_area_horizontal_scrolling[press2].svg | 29 +- ...text_area_horizontal_scrolling[press3].svg | 25 +- ...text_area_horizontal_scrolling[press4].svg | 27 +- ...ntal_scrolling_cursor_matching[press0].svg | 35 +- ...ntal_scrolling_cursor_matching[press1].svg | 31 +- ...ntal_scrolling_cursor_matching[press2].svg | 14 +- ...est_text_area_language_rendering[java].svg | 468 ++++++++--------- ...xt_area_language_rendering[javascript].svg | 6 +- ...t_text_area_language_rendering[python].svg | 14 +- ...est_text_area_language_rendering[rust].svg | 470 +++++++++--------- .../test_text_area_themes[css].svg | 74 ++- .../test_text_area_themes[dracula].svg | 70 +-- .../test_text_area_themes[github_light].svg | 73 +-- .../test_text_area_themes[monokai].svg | 72 +-- .../test_text_area_themes[vscode_dark].svg | 72 ++- ...wide_syntax_highlighting[press0-False].svg | 22 +- ..._wide_syntax_highlighting[press0-True].svg | 32 +- ...wide_syntax_highlighting[press1-False].svg | 24 +- ..._wide_syntax_highlighting[press1-True].svg | 34 +- ...wide_syntax_highlighting[press2-False].svg | 26 +- ..._wide_syntax_highlighting[press2-True].svg | 36 +- ...wide_syntax_highlighting[press3-False].svg | 26 +- ..._wide_syntax_highlighting[press3-True].svg | 36 +- ...wide_syntax_highlighting[press4-False].svg | 24 +- ..._wide_syntax_highlighting[press4-True].svg | 34 +- ...wide_syntax_highlighting[press5-False].svg | 22 +- ..._wide_syntax_highlighting[press5-True].svg | 34 +- 35 files changed, 1163 insertions(+), 1092 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 32f2e0a2b5..61697d3c3a 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -35,18 +35,18 @@ jobs: cache: "poetry" - name: Install dependencies run: poetry install --no-interaction --extras syntax - if: ${{ matrix.python-version != '3.8' }} - - name: Install dependencies for 3.8 + if: ${{ matrix.python-version != '3.8' && matrix.python-version != '3.9' }} + - name: Install dependencies for 3.8 and 3.9 run: poetry install --no-interaction - if: ${{ matrix.python-version == '3.8' }} + if: ${{ matrix.python-version == '3.8' || matrix.python-version == '3.9' }} - name: Test with pytest (Py39+ - with syntax highlighting) run: | poetry run pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing - if: ${{ matrix.python-version != '3.8' }} + if: ${{ matrix.python-version != '3.8' && matrix.python-version != '3.9' }} - name: Test with pytest (Py38 - without syntax highlighting) run: | poetry run pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing -m 'not syntax' - if: ${{ matrix.python-version == '3.8' }} + if: ${{ matrix.python-version == '3.8' || matrix.python-version == '3.9' }} - name: Upload snapshot report if: always() uses: actions/upload-artifact@v4 diff --git a/poetry.lock b/poetry.lock index f2200d7a03..513494286a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -6,6 +6,7 @@ version = "2.4.4" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, @@ -17,6 +18,7 @@ version = "3.10.11" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5077b1a5f40ffa3ba1f40d537d3bec4383988ee51fbba6b74aa8fb1bc466599e"}, {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d6a14a4d93b5b3c2891fca94fa9d41b2322a68194422bef0dd5ec1e57d7d298"}, @@ -121,7 +123,7 @@ multidict = ">=4.5,<7.0" yarl = ">=1.12.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aiohttp-jinja2" @@ -129,6 +131,7 @@ version = "1.6" description = "jinja2 template renderer for aiohttp.web (http server for asyncio)" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "aiohttp-jinja2-1.6.tar.gz", hash = "sha256:a3a7ff5264e5bca52e8ae547bbfd0761b72495230d438d05b6c0915be619b0e2"}, {file = "aiohttp_jinja2-1.6-py3-none-any.whl", hash = "sha256:0df405ee6ad1b58e5a068a105407dc7dcc1704544c559f1938babde954f945c7"}, @@ -144,6 +147,7 @@ version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, @@ -158,6 +162,7 @@ version = "4.5.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, @@ -171,7 +176,7 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -180,6 +185,8 @@ version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -191,18 +198,19 @@ version = "25.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "babel" @@ -210,6 +218,7 @@ version = "2.17.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, @@ -219,7 +228,7 @@ files = [ pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} [package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "black" @@ -227,6 +236,7 @@ version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, @@ -263,7 +273,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -273,6 +283,7 @@ version = "0.14.2" description = "httplib2 caching for requests" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cachecontrol-0.14.2-py3-none-any.whl", hash = "sha256:ebad2091bf12d0d200dfc2464330db638c5deb41d546f6d7aca079e87290f3b0"}, {file = "cachecontrol-0.14.2.tar.gz", hash = "sha256:7d47d19f866409b98ff6025b6a0fca8e4c791fb31abbd95f622093894ce903a2"}, @@ -294,6 +305,7 @@ version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, @@ -305,6 +317,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -316,6 +329,7 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -417,6 +431,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -431,6 +446,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -442,6 +458,7 @@ version = "7.6.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, @@ -521,7 +538,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "distlib" @@ -529,6 +546,7 @@ version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -540,6 +558,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -554,6 +574,7 @@ version = "2.1.1" description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, @@ -568,6 +589,7 @@ version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -576,7 +598,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "frozenlist" @@ -584,6 +606,7 @@ version = "1.5.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, @@ -685,6 +708,7 @@ version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." optional = false python-versions = "*" +groups = ["dev"] 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"}, @@ -702,6 +726,7 @@ version = "4.0.12" description = "Git Object Database" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, @@ -716,6 +741,7 @@ version = "3.1.44" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110"}, {file = "gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269"}, @@ -726,7 +752,7 @@ gitdb = ">=4.0.1,<5" [package.extras] doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] -test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] [[package]] name = "griffe" @@ -734,6 +760,7 @@ version = "0.32.3" 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." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "griffe-0.32.3-py3-none-any.whl", hash = "sha256:d9471934225818bf8f309822f70451cc6abb4b24e59e0bb27402a45f9412510f"}, {file = "griffe-0.32.3.tar.gz", hash = "sha256:14983896ad581f59d5ad7b6c9261ff12bdaa905acccc1129341d13e545da8521"}, @@ -748,6 +775,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -759,6 +787,7 @@ version = "0.16.3" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, @@ -780,6 +809,7 @@ version = "0.23.3" description = "The next generation HTTP client." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, @@ -792,7 +822,7 @@ rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -803,6 +833,7 @@ version = "2.6.1" description = "File identification library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, @@ -817,6 +848,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -831,6 +863,8 @@ version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.10\"" files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, @@ -840,12 +874,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -854,6 +888,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -865,6 +900,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -879,6 +915,7 @@ version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, @@ -896,6 +933,7 @@ version = "2.0.3" description = "Links recognition library with FULL unicode support." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"}, {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, @@ -916,6 +954,7 @@ version = "3.7" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, @@ -934,6 +973,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -960,6 +1000,7 @@ version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, @@ -1029,6 +1070,7 @@ version = "0.4.2" description = "Collection of plugins for markdown-it-py" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, @@ -1048,6 +1090,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -1059,6 +1102,7 @@ version = "1.3.4" description = "A deep merge function for 🐍." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, @@ -1070,6 +1114,7 @@ version = "1.6.1" description = "Project documentation with Markdown." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, @@ -1093,7 +1138,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.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" @@ -1101,6 +1146,7 @@ version = "1.2.0" description = "Automatically link across pages in MkDocs." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f"}, {file = "mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f"}, @@ -1117,6 +1163,7 @@ version = "1.0.2" description = "A mkdocs plugin that lets you exclude files or trees." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"}, ] @@ -1130,6 +1177,7 @@ version = "0.2.0" description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, @@ -1147,6 +1195,7 @@ version = "1.3.0" description = "Mkdocs plugin that enables displaying the localized date of the last git modification of a markdown file." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mkdocs_git_revision_date_localized_plugin-1.3.0-py3-none-any.whl", hash = "sha256:c99377ee119372d57a9e47cff4e68f04cce634a74831c06bc89b33e456e840a1"}, {file = "mkdocs_git_revision_date_localized_plugin-1.3.0.tar.gz", hash = "sha256:439e2f14582204050a664c258861c325064d97cdc848c541e48bb034a6c4d0cb"}, @@ -1169,6 +1218,7 @@ version = "9.6.4" description = "Documentation that simply works" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mkdocs_material-9.6.4-py3-none-any.whl", hash = "sha256:414e8376551def6d644b8e6f77226022868532a792eb2c9accf52199009f568f"}, {file = "mkdocs_material-9.6.4.tar.gz", hash = "sha256:4d1d35e1c1d3e15294cb7fa5d02e0abaee70d408f75027dc7be6e30fb32e6867"}, @@ -1198,6 +1248,7 @@ version = "1.3.1" description = "Extension pack for Python Markdown and MkDocs Material." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, @@ -1209,6 +1260,7 @@ version = "1.15.0" description = "MkDocs plugin which generates a static RSS feed using git log and page.meta." optional = false python-versions = "<4,>=3.8" +groups = ["dev"] files = [ {file = "mkdocs_rss_plugin-1.15.0-py2.py3-none-any.whl", hash = "sha256:7308ac13f0976c0479db5a62cb7ef9b10fdd74b6521e459bb66a13e2cfe69d4b"}, {file = "mkdocs_rss_plugin-1.15.0.tar.gz", hash = "sha256:92995ed6c77b2ae1f5f2913e62282c27e50c35d618c4291b5b939e50badd7504"}, @@ -1233,6 +1285,7 @@ version = "0.20.0" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"}, {file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"}, @@ -1258,6 +1311,7 @@ version = "1.3.0" description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mkdocstrings_python-1.3.0-py3-none-any.whl", hash = "sha256:36c224c86ab77e90e0edfc9fea3307f7d0d245dd7c28f48bbb2203cf6e125530"}, {file = "mkdocstrings_python-1.3.0.tar.gz", hash = "sha256:f967f84bab530fcc13cc9c02eccf0c18bdb2c3bab5c55fa2045938681eec4fc4"}, @@ -1273,6 +1327,7 @@ version = "1.1.0" description = "MessagePack serializer" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, @@ -1346,6 +1401,7 @@ version = "6.1.0" description = "multidict implementation" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, @@ -1450,6 +1506,7 @@ version = "1.14.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, @@ -1509,6 +1566,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] 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"}, @@ -1520,6 +1578,7 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -1531,6 +1590,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -1542,6 +1602,7 @@ version = "0.5.7" description = "Divides large result sets into pages for easier browsing" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, @@ -1557,6 +1618,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -1568,6 +1630,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -1584,6 +1647,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1599,6 +1663,7 @@ version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.7" +groups = ["dev"] 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"}, @@ -1617,6 +1682,7 @@ version = "0.2.0" description = "Accelerated property cache" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58"}, {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b"}, @@ -1724,6 +1790,7 @@ version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -1738,6 +1805,7 @@ version = "10.14.3" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9"}, {file = "pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"}, @@ -1756,6 +1824,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -1778,6 +1847,7 @@ version = "0.24.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, @@ -1796,6 +1866,7 @@ version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, @@ -1814,6 +1885,7 @@ version = "1.1.0" description = "Snapshot testing for Textual apps" optional = false python-versions = "<4.0.0,>=3.8.1" +groups = ["dev"] files = [ {file = "pytest_textual_snapshot-1.1.0-py3-none-any.whl", hash = "sha256:fdf7727d2bc444f947554308da1b08df7a45215fe49d0621cbbc24c33e8f7b8d"}, {file = "pytest_textual_snapshot-1.1.0.tar.gz", hash = "sha256:96d48ab01306852a3b4ae165f008d5fdd7fda777e91e9d2c3ea0f7d7458544eb"}, @@ -1832,6 +1904,7 @@ version = "3.6.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, @@ -1852,6 +1925,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1866,6 +1940,7 @@ version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, @@ -1877,6 +1952,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1939,6 +2015,7 @@ version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " optional = false python-versions = ">=3.6" +groups = ["dev"] 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"}, @@ -1953,6 +2030,7 @@ version = "2024.11.6" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, @@ -2056,6 +2134,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -2077,6 +2156,7 @@ version = "1.5.0" description = "Validating URI References per RFC 3986" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, @@ -2094,6 +2174,7 @@ version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" +groups = ["main", "dev"] files = [ {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, @@ -2113,6 +2194,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -2124,6 +2206,7 @@ version = "5.0.2" description = "A pure Python implementation of a sliding window memory map manager" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, @@ -2135,6 +2218,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -2146,6 +2230,7 @@ version = "4.8.0" description = "Pytest Snapshot Test Utility" optional = false python-versions = ">=3.8.1" +groups = ["dev"] files = [ {file = "syrupy-4.8.0-py3-none-any.whl", hash = "sha256:544f4ec6306f4b1c460fdab48fd60b2c7fe54a6c0a8243aeea15f9ad9c638c3f"}, {file = "syrupy-4.8.0.tar.gz", hash = "sha256:648f0e9303aaa8387c8365d7314784c09a6bab0a407455c6a01d6a4f5c6a8ede"}, @@ -2160,6 +2245,7 @@ version = "1.7.0" description = "Development tools for working with Textual" optional = false python-versions = "<4.0.0,>=3.8.1" +groups = ["dev"] files = [ {file = "textual_dev-1.7.0-py3-none-any.whl", hash = "sha256:a93a846aeb6a06edb7808504d9c301565f7f4bf2e7046d56583ed755af356c8d"}, {file = "textual_dev-1.7.0.tar.gz", hash = "sha256:bf1a50eaaff4cd6a863535dd53f06dbbd62617c371604f66f56de3908220ccd5"}, @@ -2179,6 +2265,7 @@ version = "1.1.1" description = "Turn your Textual TUIs in to web applications" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "textual_serve-1.1.1-py3-none-any.whl", hash = "sha256:568782f1c0e60e3f7039d9121e1cb5c2f4ca1aaf6d6bd7aeb833d5763a534cb2"}, {file = "textual_serve-1.1.1.tar.gz", hash = "sha256:71c662472c462e5e368defc660ee6e8eae3bfda88ca40c050c55474686eb0c54"}, @@ -2197,6 +2284,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2234,57 +2323,47 @@ files = [ [[package]] name = "tree-sitter" -version = "0.23.2" +version = "0.24.0" description = "Python bindings to the Tree-sitter parsing library" optional = true -python-versions = ">=3.9" -files = [ - {file = "tree-sitter-0.23.2.tar.gz", hash = "sha256:66bae8dd47f1fed7bdef816115146d3a41c39b5c482d7bad36d9ba1def088450"}, - {file = "tree_sitter-0.23.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3a937f5d8727bc1c74c4bf2a9d1c25ace049e8628273016ad0d45914ae904e10"}, - {file = "tree_sitter-0.23.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2c7eae7fe2af215645a38660d2d57d257a4c461fe3ec827cca99a79478284e80"}, - {file = "tree_sitter-0.23.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a71d607595270b6870eaf778a1032d146b2aa79bfcfa60f57a82a7b7584a4c7"}, - {file = "tree_sitter-0.23.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe9b9ea7a0aa23b52fd97354da95d1b2580065bc12a4ac868f9164a127211d6"}, - {file = "tree_sitter-0.23.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d74d00a8021719eae14d10d1b1e28649e15d8b958c01c2b2c3dad7a2ebc4dbae"}, - {file = "tree_sitter-0.23.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6de18d8d8a7f67ab71f472d1fcb01cc506e080cbb5e13d52929e4b6fdce6bbee"}, - {file = "tree_sitter-0.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:12b60dca70d2282af942b650a6d781be487485454668c7c956338a367b98cdee"}, - {file = "tree_sitter-0.23.2-cp310-cp310-win_arm64.whl", hash = "sha256:3346a4dd0447a42aabb863443b0fd8c92b909baf40ed2344fae4b94b625d5955"}, - {file = "tree_sitter-0.23.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91fda41d4f8824335cc43c64e2c37d8089c8c563bd3900a512d2852d075af719"}, - {file = "tree_sitter-0.23.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92b2b489d5ce54b41f94c6f23fbaf592bd6e84dc2877048fd1cb060480fa53f7"}, - {file = "tree_sitter-0.23.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64859bd4aa1567d0d6016a811b2b49c59d4a4427d096e3d8c84b2521455f62b7"}, - {file = "tree_sitter-0.23.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:614590611636044e071d3a0b748046d52676dbda3bc9fa431216231e11dd98f7"}, - {file = "tree_sitter-0.23.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08466953c78ae57be61057188fb88c89791b0a562856010228e0ccf60e2ac453"}, - {file = "tree_sitter-0.23.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a33f03a562de91f7fd05eefcedd8994a06cd44c62f7aabace811ad82bc11cbd"}, - {file = "tree_sitter-0.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:03b70296b569ef64f7b92b42ca5da9bf86d81bee2afd480bea35092687f51dae"}, - {file = "tree_sitter-0.23.2-cp311-cp311-win_arm64.whl", hash = "sha256:7cb4bb953ea7c0b50eeafc4454783e030357179d2a93c3dd5ebed2da5588ddd0"}, - {file = "tree_sitter-0.23.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a014498b6a9e6003fae8c6eb72f5927d62da9dcb72b28b3ce8cd15c6ff6a6572"}, - {file = "tree_sitter-0.23.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f8699b131d4bcbe3805c37e4ef3d159ee9a82a0e700587625623999ba0ea53"}, - {file = "tree_sitter-0.23.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4471577df285059c71686ecb208bc50fb472099b38dcc8e849b0e86652891e87"}, - {file = "tree_sitter-0.23.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f342c925290dd4e20ecd5787ef7ae8749981597ab364783a1eb73173efe65226"}, - {file = "tree_sitter-0.23.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4e9e53d07dd076bede72e4f7d3a0173d7b9ad6576572dd86da008a740a9bb22"}, - {file = "tree_sitter-0.23.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8caebe65bc358759dac2500d8f8feed3aed939c4ade9a684a1783fe07bc7d5db"}, - {file = "tree_sitter-0.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:fc5a72eb50d43485000dbbb309acb350467b7467e66dc747c6bb82ce63041582"}, - {file = "tree_sitter-0.23.2-cp312-cp312-win_arm64.whl", hash = "sha256:a0320eb6c7993359c5f7b371d22719ccd273f440d41cf1bd65dac5e9587f2046"}, - {file = "tree_sitter-0.23.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eff630dddee7ba05accb439b17e559e15ce13f057297007c246237ceb6306332"}, - {file = "tree_sitter-0.23.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4780ba8f3894f2dea869fad2995c2aceab3fd5ab9e6a27c45475d2acd7f7e84e"}, - {file = "tree_sitter-0.23.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0b609460b8e3e256361fb12e94fae5b728cb835b16f0f9d590b5aadbf9d109b"}, - {file = "tree_sitter-0.23.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d070d8eaeaeb36cf535f55e5578fddbfc3bf53c1980f58bf1a99d57466b3b5"}, - {file = "tree_sitter-0.23.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878580b2ad5054c410ba3418edca4d34c81cc26706114d8f5b5541688bc2d785"}, - {file = "tree_sitter-0.23.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:29224bdc2a3b9af535b7725e249d3ee291b2e90708e82832e73acc175e40dc48"}, - {file = "tree_sitter-0.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:c58d89348162fbc3aea1fe6511a66ee189fc0e4e4bbe937026f29e4ecef17763"}, - {file = "tree_sitter-0.23.2-cp313-cp313-win_arm64.whl", hash = "sha256:0ff2037be5edab7801de3f6a721b9cf010853f612e2008ee454e0e0badb225a6"}, - {file = "tree_sitter-0.23.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a5db8e585205faef8bf219da77d8993e2ef04d08eda2e3c8ad7e4df8297ee344"}, - {file = "tree_sitter-0.23.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9dbd110a30cf28be5da734ae4cd0e9031768228dbf6a79f2973962aa51de4ec7"}, - {file = "tree_sitter-0.23.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569514b9a996a0fd458b3a891c46ca125298be0c03cf82f2b6f0c13d5d8f25dc"}, - {file = "tree_sitter-0.23.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a357ed98a74e47787b812df99a74a2c35c0fe11e55c2095cc01d1cad144ef552"}, - {file = "tree_sitter-0.23.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c2dfb8e8f760f4cc67888d03ef9e2dbd3353245f67f5efba375c2a14d944ac0e"}, - {file = "tree_sitter-0.23.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3ead958df87a21d706903987e665e9e0e5df7b2c5021ff69ea349826840adc6a"}, - {file = "tree_sitter-0.23.2-cp39-cp39-win_amd64.whl", hash = "sha256:611cae16be332213c0e6ece72c0bfca202e30ff320a8b309b1526c6cb79ee4ba"}, - {file = "tree_sitter-0.23.2-cp39-cp39-win_arm64.whl", hash = "sha256:b848e0fdd522fbb8888cdb4f4d93f8fad97ae10d70c122fb922e51363c7febcd"}, +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" +files = [ + {file = "tree-sitter-0.24.0.tar.gz", hash = "sha256:abd95af65ca2f4f7eca356343391ed669e764f37748b5352946f00f7fc78e734"}, + {file = "tree_sitter-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f3f00feff1fc47a8e4863561b8da8f5e023d382dd31ed3e43cd11d4cae445445"}, + {file = "tree_sitter-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f9691be48d98c49ef8f498460278884c666b44129222ed6217477dffad5d4831"}, + {file = "tree_sitter-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:098a81df9f89cf254d92c1cd0660a838593f85d7505b28249216661d87adde4a"}, + {file = "tree_sitter-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b26bf9e958da6eb7e74a081aab9d9c7d05f9baeaa830dbb67481898fd16f1f5"}, + {file = "tree_sitter-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2a84ff87a2f2a008867a1064aba510ab3bd608e3e0cd6e8fef0379efee266c73"}, + {file = "tree_sitter-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c012e4c345c57a95d92ab5a890c637aaa51ab3b7ff25ed7069834b1087361c95"}, + {file = "tree_sitter-0.24.0-cp310-cp310-win_arm64.whl", hash = "sha256:033506c1bc2ba7bd559b23a6bdbeaf1127cee3c68a094b82396718596dfe98bc"}, + {file = "tree_sitter-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de0fb7c18c6068cacff46250c0a0473e8fc74d673e3e86555f131c2c1346fb13"}, + {file = "tree_sitter-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7c9c89666dea2ce2b2bf98e75f429d2876c569fab966afefdcd71974c6d8538"}, + {file = "tree_sitter-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddb113e6b8b3e3b199695b1492a47d87d06c538e63050823d90ef13cac585fd"}, + {file = "tree_sitter-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ea01a7003b88b92f7f875da6ba9d5d741e0c84bb1bd92c503c0eecd0ee6409"}, + {file = "tree_sitter-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:464fa5b2cac63608915a9de8a6efd67a4da1929e603ea86abaeae2cb1fe89921"}, + {file = "tree_sitter-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:3b1f3cbd9700e1fba0be2e7d801527e37c49fc02dc140714669144ef6ab58dce"}, + {file = "tree_sitter-0.24.0-cp311-cp311-win_arm64.whl", hash = "sha256:f3f08a2ca9f600b3758792ba2406971665ffbad810847398d180c48cee174ee2"}, + {file = "tree_sitter-0.24.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14beeff5f11e223c37be7d5d119819880601a80d0399abe8c738ae2288804afc"}, + {file = "tree_sitter-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26a5b130f70d5925d67b47db314da209063664585a2fd36fa69e0717738efaf4"}, + {file = "tree_sitter-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fc5c3c26d83c9d0ecb4fc4304fba35f034b7761d35286b936c1db1217558b4e"}, + {file = "tree_sitter-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:772e1bd8c0931c866b848d0369b32218ac97c24b04790ec4b0e409901945dd8e"}, + {file = "tree_sitter-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:24a8dd03b0d6b8812425f3b84d2f4763322684e38baf74e5bb766128b5633dc7"}, + {file = "tree_sitter-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9e8b1605ab60ed43803100f067eed71b0b0e6c1fb9860a262727dbfbbb74751"}, + {file = "tree_sitter-0.24.0-cp312-cp312-win_arm64.whl", hash = "sha256:f733a83d8355fc95561582b66bbea92ffd365c5d7a665bc9ebd25e049c2b2abb"}, + {file = "tree_sitter-0.24.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d4a6416ed421c4210f0ca405a4834d5ccfbb8ad6692d4d74f7773ef68f92071"}, + {file = "tree_sitter-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0992d483677e71d5c5d37f30dfb2e3afec2f932a9c53eec4fca13869b788c6c"}, + {file = "tree_sitter-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57277a12fbcefb1c8b206186068d456c600dbfbc3fd6c76968ee22614c5cd5ad"}, + {file = "tree_sitter-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25fa22766d63f73716c6fec1a31ee5cf904aa429484256bd5fdf5259051ed74"}, + {file = "tree_sitter-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d5d9537507e1c8c5fa9935b34f320bfec4114d675e028f3ad94f11cf9db37b9"}, + {file = "tree_sitter-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:f58bb4956917715ec4d5a28681829a8dad5c342cafd4aea269f9132a83ca9b34"}, + {file = "tree_sitter-0.24.0-cp313-cp313-win_arm64.whl", hash = "sha256:23641bd25dcd4bb0b6fa91b8fb3f46cc9f1c9f475efe4d536d3f1f688d1b84c8"}, ] [package.extras] -docs = ["sphinx (>=7.3,<8.0)", "sphinx-book-theme"] -tests = ["tree-sitter-html (>=0.23.0)", "tree-sitter-javascript (>=0.23.0)", "tree-sitter-json (>=0.23.0)", "tree-sitter-python (>=0.23.0)", "tree-sitter-rust (>=0.23.0)"] +docs = ["sphinx (>=8.1,<9.0)", "sphinx-book-theme"] +tests = ["tree-sitter-html (>=0.23.2)", "tree-sitter-javascript (>=0.23.1)", "tree-sitter-json (>=0.24.8)", "tree-sitter-python (>=0.23.6)", "tree-sitter-rust (>=0.23.2)"] [[package]] name = "tree-sitter-bash" @@ -2292,6 +2371,8 @@ version = "0.23.3" description = "Bash grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_bash-0.23.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c1ee7a46fcbfca9937d01056be756631762f53c5afdb8c4ab64eb9fed060896b"}, {file = "tree_sitter_bash-0.23.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5a090118e887bf667d82ae445794906186216f5500e0d2cd58eb499f7502dc57"}, @@ -2312,6 +2393,8 @@ version = "0.23.2" description = "CSS grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_css-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:62b9eadb8f47c666a36a2ead96d17c2a01d7599e1f13f69c617f08e4acf62bf0"}, {file = "tree_sitter_css-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0be54e07f90679173bb06a8ecf483a7d79eaa6d236419b5baa6ce02401ea31a9"}, @@ -2332,6 +2415,8 @@ version = "0.23.4" description = "Go grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_go-0.23.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c9320f87a05cd47fa0f627b9329bbc09b7ed90de8fe4f5882aed318d6e19962d"}, {file = "tree_sitter_go-0.23.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:914e63d16b36ab0e4f52b031e574b82d17d0bbfecca138ae83e887a1cf5b71ac"}, @@ -2352,6 +2437,8 @@ version = "0.23.2" description = "HTML grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_html-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e1641d5edf5568a246c6c47b947ed524b5bf944664e6473b21d4ae568e28ee9"}, {file = "tree_sitter_html-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3d0a83dd6cd1c7d4bcf6287b5145c92140f0194f8516f329ae8b9e952fbfa8ff"}, @@ -2372,6 +2459,8 @@ version = "0.23.5" description = "Java grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_java-0.23.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:355ce0308672d6f7013ec913dee4a0613666f4cda9044a7824240d17f38209df"}, {file = "tree_sitter_java-0.23.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:24acd59c4720dedad80d548fe4237e43ef2b7a4e94c8549b0ca6e4c4d7bf6e69"}, @@ -2392,6 +2481,8 @@ version = "0.23.1" description = "JavaScript grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_javascript-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6ca583dad4bd79d3053c310b9f7208cd597fd85f9947e4ab2294658bb5c11e35"}, {file = "tree_sitter_javascript-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:94100e491a6a247aa4d14caf61230c171b6376c863039b6d9cd71255c2d815ec"}, @@ -2412,6 +2503,8 @@ version = "0.24.8" description = "JSON grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_json-0.24.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:59ac06c6db1877d0e2076bce54a5fddcdd2fc38ca778905662e80fa9ffcea2ab"}, {file = "tree_sitter_json-0.24.8-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:62b4c45b561db31436a81a3f037f71ec29049f4fc9bf5269b6ec3ebaaa35a1cd"}, @@ -2432,6 +2525,8 @@ version = "0.3.2" description = "Markdown grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_markdown-0.3.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2a0d60ee5185fbc20c6f3e7744348956a62f8bc9ae85b574251632e3c2220c77"}, {file = "tree_sitter_markdown-0.3.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0a72f199966380e18f668abb3e9d0a75569c8a292967deefc432282e253f9f84"}, @@ -2453,6 +2548,8 @@ version = "0.23.6" description = "Python grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_python-0.23.6-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:28fbec8f74eeb2b30292d97715e60fac9ccf8a8091ce19b9d93e9b580ed280fb"}, {file = "tree_sitter_python-0.23.6-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:680b710051b144fedf61c95197db0094f2245e82551bf7f0c501356333571f7a"}, @@ -2473,6 +2570,8 @@ version = "0.24.3" description = "Regex grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_regex-0.24.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:16ded552d0f43dda608cec078b4a63f1dfa53c793775ba1a1bb06b2539b94fff"}, {file = "tree_sitter_regex-0.24.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0a26bf77f7a8aa070299246eb3a29276030481ff380346c4085a97e448c34570"}, @@ -2493,6 +2592,8 @@ version = "0.23.2" description = "Rust grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_rust-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b6b26a4c07ddc243f3701450ff34093b8e3b08f14d269db2d049c625d151677c"}, {file = "tree_sitter_rust-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c6224f608df559d75425e5ef428f635b9fb87d7aa8716444915ee67ec6955085"}, @@ -2513,6 +2614,8 @@ version = "0.3.7" description = "Tree-sitter Grammar for SQL" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_sql-0.3.7-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:f3f8427328bd8b4ee02ab50d71bfc515937c037b8a03dcf54b8c98403d804269"}, {file = "tree_sitter_sql-0.3.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:97ad55611f7d777b08a30d60150e1c44100ac2759a341b1cf1ffa2f97f20259e"}, @@ -2533,6 +2636,8 @@ version = "0.7.0" description = "TOML grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_toml-0.7.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b9ae5c3e7c5b6bb05299dd73452ceafa7fa0687d5af3012332afa7757653b676"}, {file = "tree_sitter_toml-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:18be09538e9775cddc0290392c4e2739de2201260af361473ca60b5c21f7bd22"}, @@ -2553,6 +2658,8 @@ version = "0.7.0" description = "XML & DTD grammars for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_xml-0.7.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cc3e516d4c1e0860fb22172c172148debb825ba638971bc48bad15b22e5b0bae"}, {file = "tree_sitter_xml-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0674fdf4cc386e4d323cb287d3b072663de0f20a9e9af5d5e09821aae56a9e5c"}, @@ -2573,6 +2680,8 @@ version = "0.7.0" description = "YAML grammar for tree-sitter" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"syntax\"" files = [ {file = "tree_sitter_yaml-0.7.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e21553ac190ae05bf82796df8beb4d9158ba195b5846018cb36fbc3a35bd0679"}, {file = "tree_sitter_yaml-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c022054f1f9b54201082ea83073a6c24c42d0436ad8ee99ff2574cba8f928c28"}, @@ -2593,6 +2702,7 @@ version = "67.8.0.0" description = "Typing stubs for setuptools" optional = false python-versions = "*" +groups = ["dev"] 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"}, @@ -2604,6 +2714,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -2615,6 +2726,8 @@ version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["dev"] +markers = "python_version >= \"3.9\" and sys_platform == \"win32\"" files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, @@ -2626,6 +2739,7 @@ version = "1.0.3" description = "Micro subset of unicode data files for linkify-it-py projects." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"}, {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"}, @@ -2640,13 +2754,14 @@ version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2657,6 +2772,7 @@ version = "20.29.2" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, @@ -2669,7 +2785,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "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)"] +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) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "watchdog" @@ -2677,6 +2793,7 @@ version = "4.0.2" description = "Filesystem events monitoring" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22"}, {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1"}, @@ -2724,6 +2841,7 @@ version = "1.15.2" description = "Yet another URL library" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "yarl-1.15.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e4ee8b8639070ff246ad3649294336b06db37a94bdea0d09ea491603e0be73b8"}, {file = "yarl-1.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a7cf963a357c5f00cb55b1955df8bbe68d2f2f65de065160a1c26b85a1e44172"}, @@ -2836,23 +2954,25 @@ version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.10\"" files = [ {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [extras] syntax = ["tree-sitter", "tree-sitter-bash", "tree-sitter-css", "tree-sitter-go", "tree-sitter-html", "tree-sitter-java", "tree-sitter-javascript", "tree-sitter-json", "tree-sitter-markdown", "tree-sitter-python", "tree-sitter-regex", "tree-sitter-rust", "tree-sitter-sql", "tree-sitter-toml", "tree-sitter-xml", "tree-sitter-yaml"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.8.1" -content-hash = "db29b377e8fcedd9730b54f573ba175c8743e06fa57da2dee8a4e62bc2a6faa7" +content-hash = "64a9f677e78ed910aa7679123dff96604f006ddc4f5c5fbba87ec8ebb642ec3d" diff --git a/pyproject.toml b/pyproject.toml index c01b22558b..bf053b703d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Modern Text User Interface framework" authors = ["Will McGugan "] license = "MIT" readme = "README.md" -classifiers = [ +classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", @@ -52,24 +52,24 @@ typing-extensions = "^4.4.0" platformdirs = ">=3.6.0,<5" # start of [syntax] extras -# Require tree-sitter >= 0.23.0 and python >= 3.9 +# Require tree-sitter >= 0.24.0 and python >= 3.10 # Windows, MacOS and Linux binary wheels are available for all of the languages below. -tree-sitter = { version = ">=0.23.0", optional = true, python = ">=3.9" } -tree-sitter-python = { version = ">=0.23.0", optional = true, python = ">=3.9" } -tree-sitter-markdown = { version = ">=0.3.0", optional = true, python = ">=3.9"} -tree-sitter-json = { version = ">=0.24.0", optional = true, python = ">=3.9" } -tree-sitter-toml = { version = ">=0.6.0", optional = true, python = ">=3.9" } -tree-sitter-yaml = { version = ">=0.6.0", optional = true, python = ">=3.9" } -tree-sitter-html = { version = ">=0.23.0", optional = true, python = ">=3.9" } -tree-sitter-css = { version = ">=0.23.0", optional = true, python = ">=3.9" } -tree-sitter-javascript = { version = ">=0.23.0", optional = true, python = ">=3.9" } -tree-sitter-rust = { version = ">=0.23.0", optional = true, python = ">=3.9" } -tree-sitter-go = { version = ">=0.23.0", optional = true, python = ">=3.9" } -tree-sitter-regex = { version = ">=0.24.0", optional = true, python = ">=3.9" } -tree-sitter-xml = { version = ">=0.7.0", optional = true, python = ">=3.9" } -tree-sitter-sql = { version = ">=0.3.0,<0.3.8", optional = true, python = ">=3.9" } -tree-sitter-java = { version = ">=0.23.0", optional = true, python = ">=3.9" } -tree-sitter-bash = { version = ">=0.23.0", optional = true, python = ">=3.9" } +tree-sitter = { version = ">=0.24.0", optional = true, python = ">=3.10" } +tree-sitter-python = { version = ">=0.23.0", optional = true, python = ">=3.10" } +tree-sitter-markdown = { version = ">=0.3.0", optional = true, python = ">=3.10"} +tree-sitter-json = { version = ">=0.24.0", optional = true, python = ">=3.10" } +tree-sitter-toml = { version = ">=0.6.0", optional = true, python = ">=3.10" } +tree-sitter-yaml = { version = ">=0.6.0", optional = true, python = ">=3.10" } +tree-sitter-html = { version = ">=0.23.0", optional = true, python = ">=3.10" } +tree-sitter-css = { version = ">=0.23.0", optional = true, python = ">=3.10" } +tree-sitter-javascript = { version = ">=0.23.0", optional = true, python = ">=3.10" } +tree-sitter-rust = { version = ">=0.23.0", optional = true, python = ">=3.10" } +tree-sitter-go = { version = ">=0.23.0", optional = true, python = ">=3.10" } +tree-sitter-regex = { version = ">=0.24.0", optional = true, python = ">=3.10" } +tree-sitter-xml = { version = ">=0.7.0", optional = true, python = ">=3.10" } +tree-sitter-sql = { version = ">=0.3.0,<0.3.8", optional = true, python = ">=3.10" } +tree-sitter-java = { version = ">=0.23.0", optional = true, python = ">=3.10" } +tree-sitter-bash = { version = ">=0.23.0", optional = true, python = ">=3.10" } # end of [syntax] extras [tool.poetry.extras] diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py index 24c4917cc3..9734c176d0 100644 --- a/src/textual/document/_syntax_aware_document.py +++ b/src/textual/document/_syntax_aware_document.py @@ -2,7 +2,6 @@ import weakref from asyncio import CancelledError, Event, Task, create_task, sleep -from functools import partial from typing import Callable, NamedTuple try: @@ -61,7 +60,7 @@ def __init__( """The tree-sitter Parser or None if tree-sitter is unavailable.""" self._syntax_tree: Tree = self._parser.parse( - partial(self._read_callable, lines=self.lines) + self.text.encode("utf-8") ) # type: ignore """The tree-sitter Tree (syntax tree) built from the document.""" @@ -165,7 +164,7 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult ) return replace_result - def reparse(self, timeout_us: int, lines: list[str], syntax_tree=None) -> bool: + def reparse(self, timeout_us: int, text: bytes) -> bool: """Reparse the document. Args: @@ -176,13 +175,12 @@ def reparse(self, timeout_us: int, lines: list[str], syntax_tree=None) -> bool: True if parsing succeeded and False if a timeout occurred. """ assert timeout_us > 0 - read_source = partial(self._read_callable, lines=lines) tree = self._syntax_tree saved_timeout = self._parser.timeout_micros try: self._parser.timeout_micros = timeout_us try: - tree = self._parser.parse(read_source, tree) # type: ignore[arg-type] + tree = self._parser.parse(text, tree) # type: ignore[arg-type] except ValueError: # The only known cause is a timeout. return False @@ -194,7 +192,7 @@ def set_new_tree(): self._syntax_tree = tree changed_ranges = self._syntax_tree.changed_ranges(tree) - self._syntax_tree_update_callback(self._syntax_tree, len(lines)) + self._syntax_tree_update_callback(self._syntax_tree) else: self._syntax_tree = tree return True @@ -256,50 +254,6 @@ def _location_to_point(self, location: Location) -> tuple[int, int]: bytes_on_left = 0 return row, bytes_on_left - def _read_callable( - self, - byte_offset: int, - point: tuple[int, int], - lines: list[str], - ) -> bytes: - """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). - lines: The lines of the document being parsed. - - 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 - newline = self.newline - - row_out_of_bounds = row >= len(lines) - if row_out_of_bounds: - return b"" - else: - row_text = lines[row] - - 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" - - return b"" - class BackgroundSyntaxParser: """A provider of incremental background parsing for syntax highlighting. @@ -357,15 +311,15 @@ async def _perform_a_single_reparse(self, force_update: bool) -> None: # In order to allow the user to continue editing without interruption, we reparse # a snapshot of the TextArea's document. - copy_of_text_for_parsing = document.copy_of_lines() + copy_of_text_for_parsing = document.text.encode("utf-8") - # Use tree-sitter's parser timeout mechanism, when necessary, break the - # full reparse into multiple steps. Most of the time, tree-sitter is so - # fast that no looping occurs. + # Use tree-sitter's parser timeout mechanism to break the full reparse + # into multiple steps. Most of the time, tree-sitter is so fast that no + # looping occurs. parsed_ok = False while not parsed_ok: parsed_ok = document.reparse( - self.PARSE_TIMEOUT_MICROSECONDS, lines=copy_of_text_for_parsing + self.PARSE_TIMEOUT_MICROSECONDS, text=copy_of_text_for_parsing ) if not parsed_ok: # Sleeping for zero seconds allows other tasks, I/O, *etc.* to execute, diff --git a/src/textual/tree-sitter/highlights/python.scm b/src/textual/tree-sitter/highlights/python.scm index 6f844b6135..3e7381b813 100644 --- a/src/textual/tree-sitter/highlights/python.scm +++ b/src/textual/tree-sitter/highlights/python.scm @@ -11,9 +11,9 @@ ;; Identifier naming conventions ((identifier) @type - (#lua-match? @type "^[A-Z].*[a-z]")) + (#match? @type "^[A-Z].*[a-z]")) ((identifier) @constant - (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) + (#match? @constant "^[A-Z][A-Z_0-9]*$")) ((attribute attribute: (identifier) @field) @@ -59,12 +59,12 @@ ((call function: (identifier) @constructor) - (#lua-match? @constructor "^[A-Z]")) + (#match? @constructor "^[A-Z]")) ((call function: (attribute attribute: (identifier) @constructor)) - (#lua-match? @constructor "^[A-Z]")) + (#match? @constructor "^[A-Z]")) ;; Decorators diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 288297b717..646d9e9e9f 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1208,7 +1208,7 @@ def check_consume_key(self, key: str, character: str | None = None) -> bool: # Otherwise we capture all printable keys return character is not None and character.isprintable() - def _handle_syntax_tree_update(self, tree: Tree, line_count: int) -> None: + def _handle_syntax_tree_update(self, tree: Tree) -> None: """Reflect changes to the syntax tree.""" self._trigger_repaint() @@ -1693,11 +1693,6 @@ def _prepare_for_repaint(self) -> Collection[Region]: return self._do_prepare_for_repaint() def _do_prepare_for_repaint(self) -> Collection[Region]: - # TODO: - # This is being used as a hook to prepare for an imminent screen - # update, which is not the intended use of this method. A proper - # 'prepare for screen update' hook. - is_syntax_aware = self.is_syntax_aware if is_syntax_aware: highlights = self._highlights @@ -1849,7 +1844,7 @@ def create_select_range() -> range: range_end = min(sel_end, text_length) if not line_text and yy != cursor_y: # Make sure that empty line show up as selected. - line_text = TextReprString("▌") + line_text = TextReprString.create("▌") select_range = range(1, 0, -1) elif sel_start < sel_end: # The selection covers part of this line section. diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press0].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press0].svg index 54027c15fb..b2e4ffdaa9 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press0].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press0].svg @@ -38,10 +38,10 @@ .terminal-r4 { fill: #c2c2bf } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #272822 } -.terminal-r8 { fill: #90908a } -.terminal-r9 { fill: #ae81ff } -.terminal-r10 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #272822 } +.terminal-r9 { fill: #90908a } +.terminal-r10 { fill: #ae81ff } .terminal-r11 { fill: #e6db74 } .terminal-r12 { fill: #003054 } @@ -87,14 +87,14 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def a_function_with_a_long_name(lon -2  if long_argument_name == 42:    -3  print('I think you may have -4  else:                           -5  print('You need the one who +1  defa_function_with_a_long_name(lon +2  if long_argument_name == 42:    +3  print('I think you may have +4  else:                           +5  print('You need the one who diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press1].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press1].svg index 3a968d62e1..37a31849fd 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press1].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press1].svg @@ -38,10 +38,10 @@ .terminal-r4 { fill: #c2c2bf } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #272822 } -.terminal-r8 { fill: #90908a } -.terminal-r9 { fill: #ae81ff } -.terminal-r10 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #272822 } +.terminal-r9 { fill: #90908a } +.terminal-r10 { fill: #ae81ff } .terminal-r11 { fill: #e6db74 } .terminal-r12 { fill: #000000 } .terminal-r13 { fill: #003054 } @@ -88,14 +88,14 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  ef a_function_with_a_long_name(long -2  if long_argument_name == 42:     -3  print('I think you may have  -4  else:                            -5  print('You need the one who  +1  efa_function_with_a_long_name(long +2  if long_argument_name == 42:     +3  print('I think you may have  +4  else:                            +5  print('You need the one who  diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press2].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press2].svg index 69e4848909..a0888d998f 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press2].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press2].svg @@ -36,13 +36,14 @@ .terminal-r2 { fill: #0178d4 } .terminal-r3 { fill: #c5c8c6 } .terminal-r4 { fill: #c2c2bf } -.terminal-r5 { fill: #f8f8f2 } -.terminal-r6 { fill: #272822 } -.terminal-r7 { fill: #90908a } -.terminal-r8 { fill: #ae81ff } -.terminal-r9 { fill: #e6db74 } -.terminal-r10 { fill: #000000 } -.terminal-r11 { fill: #003054 } +.terminal-r5 { fill: #a6e22e } +.terminal-r6 { fill: #f8f8f2 } +.terminal-r7 { fill: #272822 } +.terminal-r8 { fill: #90908a } +.terminal-r9 { fill: #ae81ff } +.terminal-r10 { fill: #e6db74 } +.terminal-r11 { fill: #000000 } +.terminal-r12 { fill: #003054 } @@ -86,17 +87,17 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  a_long_name(long_argument_name): -2  _name == 42:                        -3  nk you may have figured it out!')   -4   -5  eed the one who is to come after me +1  a_long_name(long_argument_name): +2  _name == 42:                        +3  nk you may have figured it out!')   +4   +5  eed the one who is to come after me - + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press3].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press3].svg index dccf5641e1..4e07cc22c6 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press3].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press3].svg @@ -36,12 +36,13 @@ .terminal-r2 { fill: #0178d4 } .terminal-r3 { fill: #c5c8c6 } .terminal-r4 { fill: #90908a } -.terminal-r5 { fill: #f8f8f2 } -.terminal-r6 { fill: #ae81ff } -.terminal-r7 { fill: #e6db74 } -.terminal-r8 { fill: #c2c2bf } -.terminal-r9 { fill: #272822 } -.terminal-r10 { fill: #000000 } +.terminal-r5 { fill: #a6e22e } +.terminal-r6 { fill: #f8f8f2 } +.terminal-r7 { fill: #ae81ff } +.terminal-r8 { fill: #e6db74 } +.terminal-r9 { fill: #c2c2bf } +.terminal-r10 { fill: #272822 } +.terminal-r11 { fill: #000000 } @@ -85,17 +86,17 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  ng_name(long_argument_name):        -2  e == 42:                            -3  ou may have figured it out!')       +1  ng_name(long_argument_name):        +2  e == 42:                            +3  ou may have figured it out!')       4   -5  the one who is to come after me!') +5  the one who is to come after me!') - + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press4].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press4].svg index f851a46578..aa6c936a26 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press4].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling[press4].svg @@ -36,13 +36,14 @@ .terminal-r2 { fill: #0178d4 } .terminal-r3 { fill: #c5c8c6 } .terminal-r4 { fill: #90908a } -.terminal-r5 { fill: #f8f8f2 } -.terminal-r6 { fill: #ae81ff } -.terminal-r7 { fill: #e6db74 } -.terminal-r8 { fill: #c2c2bf } -.terminal-r9 { fill: #272822;font-weight: bold } -.terminal-r10 { fill: #000000 } -.terminal-r11 { fill: #272822 } +.terminal-r5 { fill: #a6e22e } +.terminal-r6 { fill: #f8f8f2 } +.terminal-r7 { fill: #ae81ff } +.terminal-r8 { fill: #e6db74 } +.terminal-r9 { fill: #c2c2bf } +.terminal-r10 { fill: #272822;font-weight: bold } +.terminal-r11 { fill: #000000 } +.terminal-r12 { fill: #272822 } @@ -86,17 +87,17 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  ng_name(long_argument_name):        -2  e == 42:                            -3  ou may have figured it out!')       +1  ng_name(long_argument_name):        +2  e == 42:                            +3  ou may have figured it out!')       4   -5  the one who is to come after me!') +5  the one who is to come after me!') - + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press0].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press0].svg index 3d06b84838..1d9f07d9cb 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press0].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press0].svg @@ -36,15 +36,16 @@ .terminal-r2 { fill: #0178d4 } .terminal-r3 { fill: #c5c8c6 } .terminal-r4 { fill: #90908a } -.terminal-r5 { fill: #f8f8f2 } -.terminal-r6 { fill: #ae81ff } -.terminal-r7 { fill: #f8f8f2;font-weight: bold } -.terminal-r8 { fill: #c2c2bf } -.terminal-r9 { fill: #e6db74 } -.terminal-r10 { fill: #272822;font-weight: bold } -.terminal-r11 { fill: #000000 } -.terminal-r12 { fill: #272822 } -.terminal-r13 { fill: #003054 } +.terminal-r5 { fill: #a6e22e } +.terminal-r6 { fill: #f8f8f2 } +.terminal-r7 { fill: #ae81ff } +.terminal-r8 { fill: #f8f8f2;font-weight: bold } +.terminal-r9 { fill: #c2c2bf } +.terminal-r10 { fill: #e6db74 } +.terminal-r11 { fill: #272822;font-weight: bold } +.terminal-r12 { fill: #000000 } +.terminal-r13 { fill: #272822 } +.terminal-r14 { fill: #003054 } @@ -88,17 +89,17 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  n_with_a_long_name(long_argument_na -2  rgument_name == 42:                 -3  ( -4  'You have figured it out!') +1  n_with_a_long_name(long_argument_na +2  rgument_name == 42:                 +3  ( +4  'You have figured it out!') 5   -6  (                                   -7  You need the one who is to come aft - +6  (                                   +7  You need the one who is to come aft + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press1].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press1].svg index b04b284e67..233ea07482 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press1].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press1].svg @@ -36,15 +36,16 @@ .terminal-r2 { fill: #0178d4 } .terminal-r3 { fill: #c5c8c6 } .terminal-r4 { fill: #90908a } -.terminal-r5 { fill: #f8f8f2 } -.terminal-r6 { fill: #ae81ff } -.terminal-r7 { fill: #c2c2bf } -.terminal-r8 { fill: #e6db74 } -.terminal-r9 { fill: #272822;font-weight: bold } -.terminal-r10 { fill: #75715e } -.terminal-r11 { fill: #000000 } -.terminal-r12 { fill: #272822 } -.terminal-r13 { fill: #003054 } +.terminal-r5 { fill: #a6e22e } +.terminal-r6 { fill: #f8f8f2 } +.terminal-r7 { fill: #ae81ff } +.terminal-r8 { fill: #c2c2bf } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #272822;font-weight: bold } +.terminal-r11 { fill: #75715e } +.terminal-r12 { fill: #000000 } +.terminal-r13 { fill: #272822 } +.terminal-r14 { fill: #003054 } @@ -88,17 +89,17 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  _with_a_long_name(long_argument_nam -2  gument_name == 42:                  +1  _with_a_long_name(long_argument_nam +2  gument_name == 42:                  3   -4  'You have figured it out!')# +4  'You have figured it out!')# 5   6   -7  ou need the one who is to come afte - +7  ou need the one who is to come afte + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press2].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press2].svg index 34dd88c014..a77ea1205f 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press2].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_horizontal_scrolling_cursor_matching[press2].svg @@ -38,8 +38,8 @@ .terminal-r4 { fill: #90908a } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #ae81ff } -.terminal-r8 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #ae81ff } .terminal-r9 { fill: #e6db74 } .terminal-r10 { fill: #c2c2bf } .terminal-r11 { fill: #272822;font-weight: bold } @@ -88,15 +88,15 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def a_function_with_a_long_name(lon -2  if long_argument_name == 42:    -3  print(                      +1  defa_function_with_a_long_name(lon +2  if long_argument_name == 42:    +3  print(                      4  'You have figure 5  else:                           -6  print( +6  print( 7  'You need the one who i ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[java].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[java].svg index 02656c7fec..bf808befa2 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[java].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[java].svg @@ -19,476 +19,476 @@ font-weight: 700; } - .terminal-1303054993-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1303054993-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1303054993-r1 { fill: #121212 } -.terminal-1303054993-r2 { fill: #0178d4 } -.terminal-1303054993-r3 { fill: #c5c8c6 } -.terminal-1303054993-r4 { fill: #c2c2bf } -.terminal-1303054993-r5 { fill: #272822 } -.terminal-1303054993-r6 { fill: #f92672 } -.terminal-1303054993-r7 { fill: #f8f8f2 } -.terminal-1303054993-r8 { fill: #a6e22e } -.terminal-1303054993-r9 { fill: #90908a } -.terminal-1303054993-r10 { fill: #75715e } -.terminal-1303054993-r11 { fill: #ae81ff } -.terminal-1303054993-r12 { fill: #e6db74 } -.terminal-1303054993-r13 { fill: #66d9ef;font-style: italic; } -.terminal-1303054993-r14 { fill: #003054 } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #c2c2bf } +.terminal-r5 { fill: #272822 } +.terminal-r6 { fill: #f92672 } +.terminal-r7 { fill: #f8f8f2 } +.terminal-r8 { fill: #90908a } +.terminal-r9 { fill: #75715e } +.terminal-r10 { fill: #a6e22e } +.terminal-r11 { fill: #ae81ff } +.terminal-r12 { fill: #e6db74 } +.terminal-r13 { fill: #66d9ef;font-style: italic; } +.terminal-r14 { fill: #003054 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  1  importjava.util.ArrayList;                                             -  2  importjava.util.HashMap;                                               -  3  importjava.util.List;                                                  -  4  importjava.util.Map;                                                   -  5   -  6  // Classes and interfaces -  7  interfaceShape {                                                       -  8  double getArea();                                                   -  9  }                                                                       - 10   - 11  classRectangleimplementsShape {                                      - 12  privatedouble width;                                               - 13  privatedouble height;                                              - 14   - 15  publicRectangle(double width, double height) {                     - 16          this.width = width;                                             - 17          this.height = height;                                           - 18      }                                                                   - 19   - 20      @Override                                                           - 21  publicdouble getArea() {                                           - 22  return width * height;                                          - 23      }                                                                   - 24  }                                                                       - 25   - 26  // Enums - 27  enumDaysOfWeek {                                                       - 28      MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY      - 29  }                                                                       - 30   - 31  publicclassMain {                                                     - 32  // Constants - 33  privatestaticfinaldouble PI = 3.14159;                           - 34   - 35  // Methods - 36  publicstaticint sum(int a, int b) {                               - 37  return a + b;                                                   - 38      }                                                                   - 39   - 40  publicstaticvoid main(String[] args) {                            - 41  // Variables - 42  String name = "John";                                           - 43  int age = 30;                                                   - 44  boolean isStudent = true;                                       - 45   - 46  // Printing variables - 47  System.out.println("Hello, " + name + "! You are " + age + " ye - 48   - 49  // Conditional statements - 50  if (age >= 18 && isStudent) {                                   - 51  System.out.println("You are an adult student.");            - 52          } elseif (age >= 18) {                                         - 53  System.out.println("You are an adult.");                    - 54          } else {                                                        - 55  System.out.println("You are a minor.");                     - 56          }                                                               - 57   - 58  // Arrays - 59  int[] numbers = {12345};                                - 60  System.out.println("Numbers: " + Arrays.toString(numbers));     - 61   - 62  // Lists - 63  List<String> fruits = newArrayList<>();                        - 64  fruits.add("apple");                                            - 65  fruits.add("banana");                                           - 66  fruits.add("orange");                                           - 67  System.out.println("Fruits: " + fruits);                        - 68   - 69  // Loops - 70  for (int num : numbers) {                                       - 71  System.out.println("Number: " + num);                       - 72          }                                                               - 73   - 74  // Hash maps - 75  Map<StringInteger> scores = newHashMap<>();                  - 76  scores.put("Alice"100);                                       - 77  scores.put("Bob"80);                                          - 78  System.out.println("Alice's score: " + scores.get("Alice"));    - 79   - 80  // Exception handling - 81  try {                                                           - 82  int result = 10 / 0;                                        - 83          } catch (ArithmeticException e) {                               - 84  System.out.println("Error: " + e.getMessage());             - 85          }                                                               - 86   - 87  // Instantiating objects - 88  Rectangle rect = newRectangle(1020);                         - 89  System.out.println("Rectangle area: " + rect.getArea());        - 90   - 91  // Enums - 92  DaysOfWeek today = DaysOfWeek.MONDAY;                           - 93  System.out.println("Today is " + today);                        - 94   - 95  // Calling methods - 96  int sum = sum(510);                                           - 97  System.out.println("Sum: " + sum);                              - 98   - 99  // Ternary operator -100  String message = age >= 18 ? "You are an adult." : "You are a m -101  System.out.println(message);                                    -102      }                                                                   -103  }                                                                       -104   - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  1  import java.util.ArrayList;                                             +  2  import java.util.HashMap;                                               +  3  import java.util.List;                                                  +  4  import java.util.Map;                                                   +  5   +  6  // Classes and interfaces +  7  interfaceShape {                                                       +  8  double getArea();                                                   +  9  }                                                                       + 10   + 11  classRectangleimplementsShape {                                      + 12  privatedouble width;                                               + 13  privatedouble height;                                              + 14   + 15  publicRectangle(double width, double height) {                     + 16          this.width = width;                                             + 17          this.height = height;                                           + 18      }                                                                   + 19   + 20      @Override                                                           + 21  publicdouble getArea() {                                           + 22  return width * height;                                          + 23      }                                                                   + 24  }                                                                       + 25   + 26  // Enums + 27  enumDaysOfWeek {                                                       + 28      MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY      + 29  }                                                                       + 30   + 31  publicclassMain {                                                     + 32  // Constants + 33  privatestaticfinaldouble PI = 3.14159;                           + 34   + 35  // Methods + 36  publicstaticint sum(int a, int b) {                               + 37  return a + b;                                                   + 38      }                                                                   + 39   + 40  publicstaticvoid main(String[] args) {                            + 41  // Variables + 42  String name = "John";                                           + 43  int age = 30;                                                   + 44  boolean isStudent = true;                                       + 45   + 46  // Printing variables + 47  System.out.println("Hello, " + name + "! You are " + age + " ye + 48   + 49  // Conditional statements + 50  if (age >= 18 && isStudent) {                                   + 51  System.out.println("You are an adult student.");            + 52          } elseif (age >= 18) {                                         + 53  System.out.println("You are an adult.");                    + 54          } else {                                                        + 55  System.out.println("You are a minor.");                     + 56          }                                                               + 57   + 58  // Arrays + 59  int[] numbers = {12345};                                + 60  System.out.println("Numbers: " + Arrays.toString(numbers));     + 61   + 62  // Lists + 63  List<String> fruits = newArrayList<>();                        + 64          fruits.add("apple");                                            + 65          fruits.add("banana");                                           + 66          fruits.add("orange");                                           + 67  System.out.println("Fruits: " + fruits);                        + 68   + 69  // Loops + 70  for (int num : numbers) {                                       + 71  System.out.println("Number: " + num);                       + 72          }                                                               + 73   + 74  // Hash maps + 75  Map<StringInteger> scores = newHashMap<>();                  + 76          scores.put("Alice"100);                                       + 77          scores.put("Bob"80);                                          + 78  System.out.println("Alice's score: " + scores.get("Alice"));    + 79   + 80  // Exception handling + 81  try {                                                           + 82  int result = 10 / 0;                                        + 83          } catch (ArithmeticException e) {                               + 84  System.out.println("Error: " + e.getMessage());             + 85          }                                                               + 86   + 87  // Instantiating objects + 88  Rectangle rect = newRectangle(1020);                         + 89  System.out.println("Rectangle area: " + rect.getArea());        + 90   + 91  // Enums + 92  DaysOfWeek today = DaysOfWeek.MONDAY;                           + 93  System.out.println("Today is " + today);                        + 94   + 95  // Calling methods + 96  int sum = sum(510);                                           + 97  System.out.println("Sum: " + sum);                              + 98   + 99  // Ternary operator +100  String message = age >= 18 ? "You are an adult." : "You are a m +101  System.out.println(message);                                    +102      }                                                                   +103  }                                                                       +104   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[javascript].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[javascript].svg index 77ae9609f9..210ce01926 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[javascript].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[javascript].svg @@ -301,7 +301,7 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔  1  // Variable declarations @@ -310,7 +310,7 @@  4  var isStudent = true;                                                     5    6  // Template literals - 7  console.log(`Hello, ${name}! You are ${age} years old.`);                + 7  console.log(`Hello, ${name}! You are ${age} years old.`);                 8    9  // Conditional statements 10  if (age >= 18 && isStudent) {                                            @@ -364,7 +364,7 @@ 58   59  // Arrow functions 60  constgreet = (name) => {                                                -61    console.log(`Hello, ${name}!`);                                        +61    console.log(`Hello, ${name}!`);                                        62  };                                                                       63  greet("Alice");                                                          64   diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[python].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[python].svg index dcc7124808..7fdee992fd 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[python].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[python].svg @@ -304,7 +304,7 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔  1  import math                                                              @@ -341,18 +341,18 @@ 32  print("Greater than the answer.")                                    33   34  for index, value inenumerate(list_var):                                 -35  print(f"Index: {index}, Value: {value}")                             +35  print(f"Index: {index}, Value: {value}")                             36   37  counter = 0 38  while counter < 5:                                                       -39  print(f"Counter value: {counter}")                                   +39  print(f"Counter value: {counter}")                                   40      counter += 1 41   -42  squared_numbers = [x**2for x inrange(10if x % 2 == 0]                +42  squared_numbers = [x**2for x inrange(10if x % 2 == 0]                43   44  try:                                                                     45      result = 10 / 0 -46  except ZeroDivisionError:                                                +46  exceptZeroDivisionError:                                                47  print("Cannot divide by zero!")                                      48  finally:                                                                 49  print("End of try-except block.")                                    @@ -364,9 +364,9 @@ 55  defspeak(self):                                                     56  raiseNotImplementedError("Subclasses must implement this method 57   -58  classDog(Animal):                                                       +58  classDog(Animal):                                                       59  defspeak(self):                                                     -60  returnf"{self.name} says Woof!" +60  returnf"{self.name} says Woof!" 61   62  deffibonacci(n):                                                        63      a, b = 01 diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[rust].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[rust].svg index 5552dc5849..da8e8479f7 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[rust].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_language_rendering[rust].svg @@ -19,479 +19,479 @@ font-weight: 700; } - .terminal-1133178079-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1133178079-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1133178079-r1 { fill: #121212 } -.terminal-1133178079-r2 { fill: #0178d4 } -.terminal-1133178079-r3 { fill: #c5c8c6 } -.terminal-1133178079-r4 { fill: #c2c2bf } -.terminal-1133178079-r5 { fill: #272822 } -.terminal-1133178079-r6 { fill: #f92672 } -.terminal-1133178079-r7 { fill: #f8f8f2 } -.terminal-1133178079-r8 { fill: #a6e22e } -.terminal-1133178079-r9 { fill: #90908a } -.terminal-1133178079-r10 { fill: #75715e } -.terminal-1133178079-r11 { fill: #66d9ef;font-style: italic; } -.terminal-1133178079-r12 { fill: #e6db74 } -.terminal-1133178079-r13 { fill: #003054 } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #c2c2bf } +.terminal-r5 { fill: #272822 } +.terminal-r6 { fill: #f92672 } +.terminal-r7 { fill: #f8f8f2 } +.terminal-r8 { fill: #90908a } +.terminal-r9 { fill: #75715e } +.terminal-r10 { fill: #66d9ef;font-style: italic; } +.terminal-r11 { fill: #a6e22e } +.terminal-r12 { fill: #e6db74 } +.terminal-r13 { fill: #003054 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  1  usestd::collections::HashMap;                                          -  2   -  3  // Constants -  4  const PI: f64 = 3.14159;                                                -  5   -  6  // Structs -  7  structRectangle {                                                      -  8      width: u32,                                                         -  9      height: u32,                                                        - 10  }                                                                       - 11   - 12  implRectangle {                                                        - 13  fnarea(&self) -> u32 {                                             - 14          self.width * self.height                                        - 15      }                                                                   - 16  }                                                                       - 17   - 18  // Enums - 19  enumResult<TE> {                                                     - 20      Ok(T),                                                              - 21      Err(E),                                                             - 22  }                                                                       - 23   - 24  // Functions - 25  fngreet(name: &str) {                                                  - 26      println!("Hello, {}!", name);                                       - 27  }                                                                       - 28   - 29  fnmain() {                                                             - 30  // Variables - 31  let name = "John";                                                  - 32  letmut age = 30;                                                   - 33  let is_student = true;                                              - 34   - 35  // Printing variables - 36      println!("Hello, {}! You are {} years old.", name, age);            - 37   - 38  // Conditional statements - 39  if age >= 18 && is_student {                                        - 40          println!("You are an adult student.");                          - 41      } elseif age >= 18 {                                               - 42          println!("You are an adult.");                                  - 43      } else {                                                            - 44          println!("You are a minor.");                                   - 45      }                                                                   - 46   - 47  // Arrays - 48  let numbers = [12345];                                      - 49      println!("Numbers: {:?}", numbers);                                 - 50   - 51  // Vectors - 52  letmut fruits = vec!["apple""banana""orange"];                 - 53      fruits.push("grape");                                               - 54      println!("Fruits: {:?}", fruits);                                   - 55   - 56  // Loops - 57  for num in &numbers {                                               - 58          println!("Number: {}", num);                                    - 59      }                                                                   - 60   - 61  // Pattern matching - 62  let result = Result::Ok(42);                                        - 63  match result {                                                      - 64  Result::Ok(value) => println!("Value: {}", value),              - 65  Result::Err(error) => println!("Error: {:?}", error),           - 66      }                                                                   - 67   - 68  // Ownership and borrowing - 69  let s1 = String::from("hello");                                     - 70  let s2 = s1.clone();                                                - 71      println!("s1: {}, s2: {}", s1, s2);                                 - 72   - 73  // References - 74  let rect = Rectangle {                                              - 75          width: 10,                                                      - 76          height: 20,                                                     - 77      };                                                                  - 78      println!("Rectangle area: {}", rect.area());                        - 79   - 80  // Hash maps - 81  letmut scores = HashMap::new();                                    - 82      scores.insert("Alice"100);                                        - 83      scores.insert("Bob"80);                                           - 84      println!("Alice's score: {}", scores["Alice"]);                     - 85   - 86  // Closures - 87  let square = |num: i32| num * num;                                  - 88      println!("Square of 5: {}", square(5));                             - 89   - 90  // Traits - 91  traitPrintable {                                                   - 92  fnprint(&self);                                                - 93      }                                                                   - 94   - 95  implPrintableforRectangle {                                      - 96  fnprint(&self) {                                               - 97              println!("Rectangle: width={}, height={}", self.width, self - 98          }                                                               - 99      }                                                                   -100      rect.print();                                                       -101   -102  // Modules -103  greet("Alice");                                                     -104  }                                                                       -105   - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  1  use std::collections::HashMap;                                          +  2   +  3  // Constants +  4  const PI: f64 = 3.14159;                                                +  5   +  6  // Structs +  7  structRectangle {                                                      +  8      width: u32,                                                         +  9      height: u32,                                                        + 10  }                                                                       + 11   + 12  implRectangle {                                                        + 13  fnarea(&self) -> u32 {                                             + 14          self.width * self.height                                        + 15      }                                                                   + 16  }                                                                       + 17   + 18  // Enums + 19  enumResult<TE> {                                                     + 20      Ok(T),                                                              + 21      Err(E),                                                             + 22  }                                                                       + 23   + 24  // Functions + 25  fngreet(name: &str) {                                                  + 26      println!("Hello, {}!", name);                                       + 27  }                                                                       + 28   + 29  fnmain() {                                                             + 30  // Variables + 31  let name = "John";                                                  + 32  letmut age = 30;                                                   + 33  let is_student = true;                                              + 34   + 35  // Printing variables + 36      println!("Hello, {}! You are {} years old.", name, age);            + 37   + 38  // Conditional statements + 39  if age >= 18 && is_student {                                        + 40          println!("You are an adult student.");                          + 41      } elseif age >= 18 {                                               + 42          println!("You are an adult.");                                  + 43      } else {                                                            + 44          println!("You are a minor.");                                   + 45      }                                                                   + 46   + 47  // Arrays + 48  let numbers = [12345];                                      + 49      println!("Numbers: {:?}", numbers);                                 + 50   + 51  // Vectors + 52  letmut fruits = vec!["apple""banana""orange"];                 + 53      fruits.push("grape");                                               + 54      println!("Fruits: {:?}", fruits);                                   + 55   + 56  // Loops + 57  for num in &numbers {                                               + 58          println!("Number: {}", num);                                    + 59      }                                                                   + 60   + 61  // Pattern matching + 62  let result = Result::Ok(42);                                        + 63  match result {                                                      + 64  Result::Ok(value) => println!("Value: {}", value),              + 65  Result::Err(error) => println!("Error: {:?}", error),           + 66      }                                                                   + 67   + 68  // Ownership and borrowing + 69  let s1 = String::from("hello");                                     + 70  let s2 = s1.clone();                                                + 71      println!("s1: {}, s2: {}", s1, s2);                                 + 72   + 73  // References + 74  let rect = Rectangle {                                              + 75          width: 10,                                                      + 76          height: 20,                                                     + 77      };                                                                  + 78      println!("Rectangle area: {}", rect.area());                        + 79   + 80  // Hash maps + 81  letmut scores = HashMap::new();                                    + 82      scores.insert("Alice"100);                                        + 83      scores.insert("Bob"80);                                           + 84      println!("Alice's score: {}", scores["Alice"]);                     + 85   + 86  // Closures + 87  let square = |num: i32| num * num;                                  + 88      println!("Square of 5: {}", square(5));                             + 89   + 90  // Traits + 91  traitPrintable {                                                   + 92  fnprint(&self);                                                + 93      }                                                                   + 94   + 95  implPrintableforRectangle {                                      + 96  fnprint(&self) {                                               + 97              println!("Rectangle: width={}, height={}", self.width, self + 98          }                                                               + 99      }                                                                   +100      rect.print();                                                       +101   +102  // Modules +103  greet("Alice");                                                     +104  }                                                                       +105   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[css].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[css].svg index 0d6b98a198..490993e813 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[css].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[css].svg @@ -19,84 +19,82 @@ font-weight: 700; } - .terminal-1378439235-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1378439235-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1378439235-r1 { fill: #121212 } -.terminal-1378439235-r2 { fill: #0178d4 } -.terminal-1378439235-r3 { fill: #c5c8c6 } -.terminal-1378439235-r4 { fill: #71716e } -.terminal-1378439235-r5 { fill: #569cd6 } -.terminal-1378439235-r6 { fill: #f8f8f2 } -.terminal-1378439235-r7 { fill: #dcdcaa } -.terminal-1378439235-r8 { fill: #cccccc } -.terminal-1378439235-r9 { fill: #9cdcfe } -.terminal-1378439235-r10 { fill: #999997;font-weight: bold } -.terminal-1378439235-r11 { fill: #b5cea8 } -.terminal-1378439235-r12 { fill: #7daf9c } -.terminal-1378439235-r13 { fill: #4ec9b0 } -.terminal-1378439235-r14 { fill: #ce9178 } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #71716e } +.terminal-r5 { fill: #569cd6 } +.terminal-r6 { fill: #f8f8f2 } +.terminal-r7 { fill: #dcdcaa } +.terminal-r8 { fill: #cccccc } +.terminal-r9 { fill: #999997;font-weight: bold } +.terminal-r10 { fill: #b5cea8 } +.terminal-r11 { fill: #7daf9c } +.terminal-r12 { 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   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[dracula].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[dracula].svg index 2ec01a5ea3..c46632b650 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[dracula].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[dracula].svg @@ -19,81 +19,81 @@ font-weight: 700; } - .terminal-789685810-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-789685810-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-789685810-r1 { fill: #121212 } -.terminal-789685810-r2 { fill: #0178d4 } -.terminal-789685810-r3 { fill: #c5c8c6 } -.terminal-789685810-r4 { fill: #6272a4 } -.terminal-789685810-r5 { fill: #ff79c6 } -.terminal-789685810-r6 { fill: #f8f8f2 } -.terminal-789685810-r7 { fill: #c2c2bf;font-weight: bold } -.terminal-789685810-r8 { fill: #bd93f9 } -.terminal-789685810-r9 { fill: #282a36 } -.terminal-789685810-r10 { fill: #50fa7b } -.terminal-789685810-r11 { fill: #f1fa8c } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #6272a4 } +.terminal-r5 { fill: #ff79c6 } +.terminal-r6 { fill: #f8f8f2 } +.terminal-r7 { fill: #50fa7b } +.terminal-r8 { fill: #c2c2bf;font-weight: bold } +.terminal-r9 { fill: #bd93f9 } +.terminal-r10 { fill: #282a36 } +.terminal-r11 { fill: #f1fa8c } - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def hello(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   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[github_light].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[github_light].svg index 8def597c67..0b33d27723 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[github_light].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[github_light].svg @@ -19,83 +19,84 @@ font-weight: 700; } - .terminal-3341902079-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3341902079-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3341902079-r1 { fill: #121212 } -.terminal-3341902079-r2 { fill: #0178d4 } -.terminal-3341902079-r3 { fill: #c5c8c6 } -.terminal-3341902079-r4 { fill: #bbbbbb } -.terminal-3341902079-r5 { fill: #cf222e } -.terminal-3341902079-r6 { fill: #24292e } -.terminal-3341902079-r7 { fill: #6639bb } -.terminal-3341902079-r8 { fill: #a4a4a4 } -.terminal-3341902079-r9 { fill: #7daf9c } -.terminal-3341902079-r10 { fill: #0450ae } -.terminal-3341902079-r11 { fill: #d73a49 } -.terminal-3341902079-r12 { fill: #fafbfc } -.terminal-3341902079-r13 { fill: #093069 } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #bbbbbb } +.terminal-r5 { fill: #cf222e } +.terminal-r6 { fill: #24292e } +.terminal-r7 { fill: #6639bb } +.terminal-r8 { fill: #a4a4a4 } +.terminal-r9 { fill: #e36209 } +.terminal-r10 { fill: #0450ae } +.terminal-r11 { fill: #d73a49 } +.terminal-r12 { fill: #fafbfc } +.terminal-r13 { fill: #7daf9c } +.terminal-r14 { fill: #093069 } - + - + - + - + - + - + - + - + - + - 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   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[monokai].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[monokai].svg index f69e242c44..3e94f22d02 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[monokai].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[monokai].svg @@ -19,82 +19,82 @@ font-weight: 700; } - .terminal-2677119763-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2677119763-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2677119763-r1 { fill: #121212 } -.terminal-2677119763-r2 { fill: #0178d4 } -.terminal-2677119763-r3 { fill: #c5c8c6 } -.terminal-2677119763-r4 { fill: #90908a } -.terminal-2677119763-r5 { fill: #f92672 } -.terminal-2677119763-r6 { fill: #f8f8f2 } -.terminal-2677119763-r7 { fill: #c2c2bf } -.terminal-2677119763-r8 { fill: #ae81ff } -.terminal-2677119763-r9 { fill: #272822 } -.terminal-2677119763-r10 { fill: #66d9ef;font-style: italic; } -.terminal-2677119763-r11 { fill: #a6e22e } -.terminal-2677119763-r12 { fill: #e6db74 } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #90908a } +.terminal-r5 { fill: #f92672 } +.terminal-r6 { fill: #f8f8f2 } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #c2c2bf } +.terminal-r9 { fill: #ae81ff } +.terminal-r10 { fill: #272822 } +.terminal-r11 { fill: #66d9ef;font-style: italic; } +.terminal-r12 { fill: #e6db74 } - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def hello(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   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[vscode_dark].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[vscode_dark].svg index 1185984e0c..43710bef03 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[vscode_dark].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_themes[vscode_dark].svg @@ -19,83 +19,81 @@ font-weight: 700; } - .terminal-2571030914-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2571030914-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2571030914-r1 { fill: #121212 } -.terminal-2571030914-r2 { fill: #0178d4 } -.terminal-2571030914-r3 { fill: #c5c8c6 } -.terminal-2571030914-r4 { fill: #6e7681 } -.terminal-2571030914-r5 { fill: #569cd6 } -.terminal-2571030914-r6 { fill: #cccccc } -.terminal-2571030914-r7 { fill: #dcdcaa } -.terminal-2571030914-r8 { fill: #9cdcfe } -.terminal-2571030914-r9 { fill: #b5cea8 } -.terminal-2571030914-r10 { fill: #1e1e1e } -.terminal-2571030914-r11 { fill: #7daf9c } -.terminal-2571030914-r12 { fill: #4ec9b0 } -.terminal-2571030914-r13 { fill: #ce9178 } + .terminal-r1 { fill: #121212 } +.terminal-r2 { fill: #0178d4 } +.terminal-r3 { fill: #c5c8c6 } +.terminal-r4 { fill: #6e7681 } +.terminal-r5 { fill: #569cd6 } +.terminal-r6 { fill: #cccccc } +.terminal-r7 { fill: #dcdcaa } +.terminal-r8 { fill: #b5cea8 } +.terminal-r9 { fill: #1e1e1e } +.terminal-r10 { fill: #7daf9c } +.terminal-r11 { 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   + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-False].svg index db53ec82cc..2d279d61b4 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-False].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-False].svg @@ -39,11 +39,11 @@ .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } .terminal-r7 { fill: #272822 } -.terminal-r8 { fill: #e6db74 } -.terminal-r9 { fill: #75715e } -.terminal-r10 { fill: #90908a } -.terminal-r11 { fill: #ae81ff } -.terminal-r12 { fill: #a6e22e } +.terminal-r8 { fill: #a6e22e } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #75715e } +.terminal-r11 { fill: #90908a } +.terminal-r12 { fill: #ae81ff } @@ -87,14 +87,14 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  defūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name == 42:                 # Trailing comment -3  print('I think you māy have figured it out!')                     -4  else:                                                                 -5  print('You need the one who is to come after me!')                +1  defūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     +4  else:                                                                 +5  print('You need the one who is to come after me!')                diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-True].svg index 5aacac14d8..e0f8a8b3ce 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-True].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press0-True].svg @@ -39,11 +39,11 @@ .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } .terminal-r7 { fill: #272822 } -.terminal-r8 { fill: #e6db74 } -.terminal-r9 { fill: #75715e } -.terminal-r10 { fill: #90908a } -.terminal-r11 { fill: #ae81ff } -.terminal-r12 { fill: #a6e22e } +.terminal-r8 { fill: #a6e22e } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #75715e } +.terminal-r11 { fill: #90908a } +.terminal-r12 { fill: #ae81ff } @@ -99,19 +99,19 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  defūnicode_ち(arg_āāち="hi",       -arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name ==          -42:                 # Trailing  -comment -3  print('I think you māy  -have figured it out!')              -4  else:                           -5  print('You need the one  -who is to come after me!')          +1  defūnicode_ち(arg_āāち="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-False].svg index 7ec31a9a52..9c6b5694ff 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-False].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-False].svg @@ -38,12 +38,12 @@ .terminal-r4 { fill: #c2c2bf } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #272822 } -.terminal-r8 { fill: #e6db74 } -.terminal-r9 { fill: #75715e } -.terminal-r10 { fill: #90908a } -.terminal-r11 { fill: #ae81ff } -.terminal-r12 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #272822 } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #75715e } +.terminal-r11 { fill: #90908a } +.terminal-r12 { fill: #ae81ff } @@ -87,14 +87,14 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def ūnicode_(arg_āāち="hi", arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name == 42:                 # Trailing comment -3  print('I think you māy have figured it out!')                     -4  else:                                                                 -5  print('You need the one who is to come after me!')                +1  defūnicode_(arg_āāち="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     +4  else:                                                                 +5  print('You need the one who is to come after me!')                diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-True].svg index 24cefe9c7d..1234acf5d4 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-True].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press1-True].svg @@ -38,12 +38,12 @@ .terminal-r4 { fill: #c2c2bf } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #272822 } -.terminal-r8 { fill: #e6db74 } -.terminal-r9 { fill: #75715e } -.terminal-r10 { fill: #90908a } -.terminal-r11 { fill: #ae81ff } -.terminal-r12 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #272822 } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #75715e } +.terminal-r11 { fill: #90908a } +.terminal-r12 { fill: #ae81ff } @@ -99,19 +99,19 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def ūnicode_(arg_āāち="hi",       -arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name ==          -42:                 # Trailing  -comment -3  print('I think you māy  -have figured it out!')              -4  else:                           -5  print('You need the one  -who is to come after me!')          +1  defūnicode_(arg_āāち="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-False].svg index e05b098615..5b0ce68284 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-False].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-False].svg @@ -38,13 +38,13 @@ .terminal-r4 { fill: #c2c2bf } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #272822;font-weight: bold } -.terminal-r8 { fill: #e6db74 } -.terminal-r9 { fill: #f8f8f2;font-weight: bold } -.terminal-r10 { fill: #75715e } -.terminal-r11 { fill: #90908a } -.terminal-r12 { fill: #ae81ff } -.terminal-r13 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #272822;font-weight: bold } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #f8f8f2;font-weight: bold } +.terminal-r11 { fill: #75715e } +.terminal-r12 { fill: #90908a } +.terminal-r13 { fill: #ae81ff } @@ -88,14 +88,14 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def ūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name == 42:                 # Trailing comment -3  print('I think you māy have figured it out!')                     -4  else:                                                                 -5  print('You need the one who is to come after me!')                +1  defūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     +4  else:                                                                 +5  print('You need the one who is to come after me!')                diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-True].svg index 2a0f017268..bf9eaee738 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-True].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press2-True].svg @@ -38,13 +38,13 @@ .terminal-r4 { fill: #c2c2bf } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #272822;font-weight: bold } -.terminal-r8 { fill: #e6db74 } -.terminal-r9 { fill: #f8f8f2;font-weight: bold } -.terminal-r10 { fill: #75715e } -.terminal-r11 { fill: #90908a } -.terminal-r12 { fill: #ae81ff } -.terminal-r13 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #272822;font-weight: bold } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #f8f8f2;font-weight: bold } +.terminal-r11 { fill: #75715e } +.terminal-r12 { fill: #90908a } +.terminal-r13 { fill: #ae81ff } @@ -100,19 +100,19 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def ūnicode_ち(arg_āāち="hi",       -arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name ==          -42:                 # Trailing  -comment -3  print('I think you māy  -have figured it out!')              -4  else:                           -5  print('You need the one  -who is to come after me!')          +1  defūnicode_ち(arg_āāち="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-False].svg index 73929fc504..d325897ea5 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-False].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-False].svg @@ -38,13 +38,13 @@ .terminal-r4 { fill: #c2c2bf } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #f8f8f2;font-weight: bold } -.terminal-r8 { fill: #e6db74 } -.terminal-r9 { fill: #272822;font-weight: bold } -.terminal-r10 { fill: #75715e } -.terminal-r11 { fill: #90908a } -.terminal-r12 { fill: #ae81ff } -.terminal-r13 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #f8f8f2;font-weight: bold } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #272822;font-weight: bold } +.terminal-r11 { fill: #75715e } +.terminal-r12 { fill: #90908a } +.terminal-r13 { fill: #ae81ff } @@ -88,14 +88,14 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def ūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name == 42:                 # Trailing comment -3  print('I think you māy have figured it out!')                     -4  else:                                                                 -5  print('You need the one who is to come after me!')                +1  defūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     +4  else:                                                                 +5  print('You need the one who is to come after me!')                diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-True].svg index c8a7fe6dea..7df6c464d8 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-True].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press3-True].svg @@ -38,13 +38,13 @@ .terminal-r4 { fill: #c2c2bf } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #f8f8f2;font-weight: bold } -.terminal-r8 { fill: #e6db74 } -.terminal-r9 { fill: #272822;font-weight: bold } -.terminal-r10 { fill: #75715e } -.terminal-r11 { fill: #90908a } -.terminal-r12 { fill: #ae81ff } -.terminal-r13 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #f8f8f2;font-weight: bold } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #272822;font-weight: bold } +.terminal-r11 { fill: #75715e } +.terminal-r12 { fill: #90908a } +.terminal-r13 { fill: #ae81ff } @@ -100,19 +100,19 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def ūnicode_ち(arg_āāち="hi",       -arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name ==          -42:                 # Trailing  -comment -3  print('I think you māy  -have figured it out!')              -4  else:                           -5  print('You need the one  -who is to come after me!')          +1  defūnicode_ち(arg_āāち="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-False].svg index 42723d1078..893cab97af 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-False].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-False].svg @@ -38,12 +38,12 @@ .terminal-r4 { fill: #c2c2bf } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #272822 } -.terminal-r8 { fill: #e6db74 } -.terminal-r9 { fill: #75715e } -.terminal-r10 { fill: #90908a } -.terminal-r11 { fill: #ae81ff } -.terminal-r12 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #272822 } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #75715e } +.terminal-r11 { fill: #90908a } +.terminal-r12 { fill: #ae81ff } @@ -87,14 +87,14 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def ūnicode_ち(arg_āā="hi", arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name == 42:                 # Trailing comment -3  print('I think you māy have figured it out!')                     -4  else:                                                                 -5  print('You need the one who is to come after me!')                +1  defūnicode_ち(arg_āā="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     +4  else:                                                                 +5  print('You need the one who is to come after me!')                diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-True].svg index bb371e634d..0ada6de74a 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-True].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press4-True].svg @@ -38,12 +38,12 @@ .terminal-r4 { fill: #c2c2bf } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #272822 } -.terminal-r8 { fill: #e6db74 } -.terminal-r9 { fill: #75715e } -.terminal-r10 { fill: #90908a } -.terminal-r11 { fill: #ae81ff } -.terminal-r12 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #272822 } +.terminal-r9 { fill: #e6db74 } +.terminal-r10 { fill: #75715e } +.terminal-r11 { fill: #90908a } +.terminal-r12 { fill: #ae81ff } @@ -99,19 +99,19 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def ūnicode_ち(arg_āā="hi",       -arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name ==          -42:                 # Trailing  -comment -3  print('I think you māy  -have figured it out!')              -4  else:                           -5  print('You need the one  -who is to come after me!')          +1  defūnicode_ち(arg_āā="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-False].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-False].svg index 3d5d2872cb..11ce1479c1 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-False].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-False].svg @@ -38,12 +38,12 @@ .terminal-r4 { fill: #90908a } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #e6db74 } -.terminal-r8 { fill: #75715e } -.terminal-r9 { fill: #c2c2bf } -.terminal-r10 { fill: #272822 } -.terminal-r11 { fill: #ae81ff } -.terminal-r12 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #e6db74 } +.terminal-r9 { fill: #75715e } +.terminal-r10 { fill: #c2c2bf } +.terminal-r11 { fill: #272822 } +.terminal-r12 { fill: #ae81ff } @@ -87,14 +87,14 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def ūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name == 42:                 # Trailing comment -3  print('I think you māy have figured it out!')                     +1  defūnicode_ち(arg_āāち="hi", arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name == 42:                 # Trailing comment +3  print('I think you māy have figured it out!')                     4  else:                                                                 -5  print('You need the one who is to come after me!')                +5  print('You need the one who is to come after me!')                diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-True].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-True].svg index f15a8676ff..716d4f6879 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-True].svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_text_area_unicode_wide_syntax_highlighting[press5-True].svg @@ -38,12 +38,12 @@ .terminal-r4 { fill: #c2c2bf } .terminal-r5 { fill: #f92672 } .terminal-r6 { fill: #f8f8f2 } -.terminal-r7 { fill: #e6db74 } -.terminal-r8 { fill: #75715e } -.terminal-r9 { fill: #272822 } -.terminal-r10 { fill: #90908a } -.terminal-r11 { fill: #ae81ff } -.terminal-r12 { fill: #a6e22e } +.terminal-r7 { fill: #a6e22e } +.terminal-r8 { fill: #e6db74 } +.terminal-r9 { fill: #75715e } +.terminal-r10 { fill: #272822 } +.terminal-r11 { fill: #90908a } +.terminal-r12 { fill: #ae81ff } @@ -99,19 +99,19 @@ - + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -1  def ūnicode_ち(arg_āāち="hi",       -arg_b="bye"):   # Trailing comment -2  if unicode_ārg_name ==          -42:                 # Trailing  -comment -3  print('I think you māy  -have figured it out!')              -4  else:                           -5  print('You need the one  -who is to come after me!')          +1  defūnicode_ち(arg_āāち="hi",       +arg_b="bye"):   # Trailing comment +2  if unicode_ārg_name ==          +42:                 # Trailing  +comment +3  print('I think you māy  +have figured it out!')              +4  else:                           +5  print('You need the one  +who is to come after me!')          ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁