From 37ea0084e670eba0abb8727a1f8f3218fd87e7c9 Mon Sep 17 00:00:00 2001 From: NIKHIL Date: Tue, 18 Nov 2025 23:21:59 +0530 Subject: [PATCH] Fix highlight priority handling and add consistency test --- src/textual/widgets/_text_area.py | 58 ++++++++++++++++++++++ tests/text_area/test_highlight_priority.py | 36 ++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 tests/text_area/test_highlight_priority.py diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 262da2c52a..a1eb7d7a87 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -81,6 +81,50 @@ "xml", ] """Languages that are included in the `syntax` extras.""" +HIGHLIGHT_PRIORITY = { + # Keywords have highest priority + "keyword": 1, + "keyword.function": 1, + "keyword.control": 1, + "keyword.return": 1, + "keyword.operator": 1, + + # Built-in types and functions + "type.builtin": 2, + "function.builtin": 2, + "constant.builtin": 2, + + # Function calls and types + "function.call": 3, + "type": 3, + + # Variables and identifiers + "variable": 4, + "variable.parameter": 4, + "property": 4, + + # Literals + "string": 2, + "number": 2, + "constant": 2, + + # Comments + "comment": 1, + + # Operators and punctuation (lowest priority) + "operator": 5, + "punctuation": 6, + "punctuation.bracket": 6, + "punctuation.delimiter": 6, +} + +def _get_highlight_priority(highlight_name: str) -> int: + """Get priority for a highlight. Lower number = higher priority (wins in conflicts).""" + # Handle compound names like "string.special" + for key in [highlight_name, highlight_name.split('.')[0]]: + if key in HIGHLIGHT_PRIORITY: + return HIGHLIGHT_PRIORITY[key] + return 99 # Default low priority for unknown highlights class ThemeDoesNotExist(Exception): @@ -1363,7 +1407,21 @@ def _render_line(self, y: int) -> Strip: byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) get_highlight_from_theme = theme.syntax_styles.get line_highlights = highlights[line_index] + + # Filter to keep only the highest priority highlight for each position + # Build a dict mapping (start, end) -> (highlight_name, priority) + position_highlights: dict[tuple[int, int | None], tuple[str, int]] = {} + for highlight_start, highlight_end, highlight_name in line_highlights: + priority = _get_highlight_priority(highlight_name) + key = (highlight_start, highlight_end) + + # Only keep this highlight if it's higher priority than existing + if key not in position_highlights or priority < position_highlights[key][1]: + position_highlights[key] = (highlight_name, priority) + + # Apply the filtered highlights + for (highlight_start, highlight_end), (highlight_name, _) in position_highlights.items(): node_style = get_highlight_from_theme(highlight_name) if node_style is not None: line.stylize( diff --git a/tests/text_area/test_highlight_priority.py b/tests/text_area/test_highlight_priority.py new file mode 100644 index 0000000000..d0e9129bb5 --- /dev/null +++ b/tests/text_area/test_highlight_priority.py @@ -0,0 +1,36 @@ +import pytest +from textual.widgets import TextArea + +@pytest.mark.asyncio +async def test_range_highlighting_priority_consistency(): + # Case 1: code with a line before + ta1 = TextArea.code_editor( + 'print("hello")\nx = range(10)', + language="python" + ) + ta1._build_highlight_map() + + # Case 2: only the range() call + ta2 = TextArea.code_editor( + 'x = range(10)', + language="python" + ) + ta2._build_highlight_map() + + # Get highlight lists + line1 = ta1._highlights[1] # second line + line2 = ta2._highlights[0] # first line + + # RANGE token begins at column 4 in both cases + RANGE_START = 4 + + def get_highlight_name(line): + for start, end, name in line: + if start == RANGE_START: + return name + return None + + name1 = get_highlight_name(line1) + name2 = get_highlight_name(line2) + + assert name1 == name2, f"Expected same highlight, got {name1=} and {name2=}"