diff --git a/CHANGELOG.md b/CHANGELOG.md index a2423a1258..888ce59308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added Widget.preflight_checks to perform some debug checks after a widget is instantiated, to catch common errors. https://github.com/Textualize/textual/pull/5588 +### 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 162d3fbd54..9288e63fa2 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 @@ -12,6 +15,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.""" @@ -128,14 +160,8 @@ def query_syntax_tree( "tree-sitter is not available on this architecture." ) - captures_kwargs = {} - if start_point is not None: - captures_kwargs["start_point"] = start_point - if end_point is not None: - captures_kwargs["end_point"] = end_point - - 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 687ef8107d..9f58388a14 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -70,6 +70,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: TextArea): + self.text_area: TextArea = text_area + """The text area associated with this highlight map.""" + + 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() + self._highlighted_blocks.clear() + + @property + def document(self) -> DocumentBase: + """The text document being highlighted.""" + 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 _build_part_of_highlight_map(self, start_index: int) -> 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 + 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._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 + middle_highlight = (0, None, highlight_name) + for node_row in range(node_start_row + 1, node_end_row): + 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) + ) + + # 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(highlight: Highlight) -> tuple[int, int, int]: + a, b, _ = highlight + max_range_index = 1 + if b is None: + max_range_index = 0 + b = a + return a, max_range_index, a - b + + for line_index in range(start_index, end_index): + highlights.get(line_index, []).sort(key=sort_key) + + @dataclass class TextAreaLanguage: """A container for a language which has been registered with the TextArea. @@ -456,15 +555,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.""" @@ -592,36 +691,11 @@ 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: - """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) - ) - - # 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)) + def _reset_highlights(self) -> None: + """Reset the lazily evaluated highlight map.""" - # 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 @@ -935,7 +1009,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() @@ -1348,7 +1422,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 @@ -1411,7 +1485,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: @@ -1459,7 +1533,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: 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