Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added

- Added suport for tree-sitter-language-pack as an optional dependency for syntax highlighting

### Fixed

- Fixed parsing Kitty extended keys with multiple codepoints https://github.com/Textualize/textual/pull/6592
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ clean: clean-screenshot-cache clean-offline-docs
setup:
poetry install
poetry install --extras syntax
poetry install --extras more-syntax

.PHONY: update
update:
Expand Down
24 changes: 24 additions & 0 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ If you would like to enable syntax highlighting in the [TextArea](./widgets/text
pip install "textual[syntax]"
```

By default this support:

- python
- markdown
- json
- toml
- yaml
- html
- css
- javascript
- rust
- go
- regex
- sql
- java
- bash
- xml

If the built-in languages aren't enough for you, you can install the "more-syntax" extra:

```
pip install "textual[more-syntax]"
```

### From conda-forge

Textual is also available on [conda-forge](https://conda-forge.org/). The preferred package manager for conda-forge is currently [micromamba](https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html):
Expand Down
34 changes: 33 additions & 1 deletion docs/widgets/text_area.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,42 @@ To enable syntax highlighting, you'll need to install the `syntax` extra depende
poetry add "textual[syntax]"
```

This will install `tree-sitter` and `tree-sitter-languages`.
This will install `tree-sitter` and `tree-sitter-languages` which support:

- python
- markdown
- json
- toml
- yaml
- html
- css
- javascript
- rust
- go
- regex
- sql
- java
- bash
- xml

These packages are distributed as binary wheels, so it may limit your applications ability to run in environments where these wheels are not available.
After installing, you can set the [`language`][textual.widgets._text_area.TextArea.language] reactive attribute on the `TextArea` to enable highlighting.

Alternatively if you need more languages can use the `more-syntax` extra:
=== "pip"

```
pip install "textual[more-syntax]"
```

=== "poetry"

```
poetry add "textual[more-syntax]"
```
This use `tree-sitter-language-pack` instead of`tree-sitter-languages`
You also can add manual support yourself, see below.

### Loading text

In this example we load some initial text into the `TextArea`, and set the language to `"python"` to enable syntax highlighting.
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ tree-sitter-sql = { version = ">=0.3.11", 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
tree-sitter-language-pack = { version = ">=1.12.0", optional = true, python = ">=3.10" }
pygments = "^2.19.2"

[tool.poetry.extras]
Expand All @@ -94,6 +95,10 @@ syntax = [
"tree-sitter-java",
"tree-sitter-bash",
]
more-syntax = [
"tree-sitter",
"tree-sitter-language-pack",
]

[tool.poetry.group.dev.dependencies]
black = "24.4.2"
Expand Down
64 changes: 40 additions & 24 deletions src/textual/_tree_sitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,54 @@
try:
from tree_sitter import Language

_LANGUAGE_CACHE: dict[str, Language] = {}
except ImportError:
_tree_sitter = False

def get_language(language_name: str) -> Language | None:
return None

else:
_tree_sitter = True
try:
import tree_sitter_language_pack

except ImportError:
_LANGUAGE_CACHE: dict[str, Language] = {}

def get_language(language_name: str) -> Language | None:
if language_name in _LANGUAGE_CACHE:
return _LANGUAGE_CACHE[language_name]

def get_language(language_name: str) -> Language | None:
if language_name in _LANGUAGE_CACHE:
return _LANGUAGE_CACHE[language_name]

try:
module = import_module(f"tree_sitter_{language_name}")
except ImportError:
return None
else:
try:
if language_name == "xml":
# xml uses language_xml() instead of language()
# it's the only outlier amongst the languages in the `textual[syntax]` extra
language = Language(module.language_xml())
else:
language = Language(module.language())
except (OSError, AttributeError):
log.warning(f"Could not load language {language_name!r}.")
module = import_module(f"tree_sitter_{language_name}")
except ImportError:
return None
else:
_LANGUAGE_CACHE[language_name] = language
return language
try:
if language_name == "xml":
# xml uses language_xml() instead of language()
# it's the only outlier amongst the languages in the `textual[syntax]` extra
language = Language(module.language_xml())
else:
language = Language(module.language())
except (OSError, AttributeError):
log.warning(f"Could not load language {language_name!r}.")
return None
else:
_LANGUAGE_CACHE[language_name] = language
return language

except ImportError:
_tree_sitter = False
else:

def get_language(language_name: str) -> Language | None:
return None
def get_language(language_name: str) -> Language | None:
try:
return tree_sitter_language_pack.get_language(language_name)
except (
tree_sitter_language_pack.LanguageNotFoundError,
tree_sitter_language_pack.DownloadError,
):
log.warning(f"Could not load language {language_name!r}.")
return None
Comment on lines +18 to +57

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tree-sitter-language-pack integration lacks test coverage. Consider adding tests that verify:

  1. The fallback chain works correctly (tslp -> individual packages -> None)
  2. get_language returns the correct Language object when using tslp
  3. Warning messages are logged appropriately when language loading fails via tslp
  4. The behavior is consistent between tslp and individual package loading

This is important because the new code path has not been validated through automated testing, which could lead to runtime issues if the tslp package behaves differently than expected.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +57

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML language special case handling (using language_xml() instead of language()) is only present in the individual package loading path but not in the tree-sitter-language-pack path. This assumes that tree-sitter-language-pack handles the XML special case internally.

Please verify that tree-sitter-language-pack correctly handles XML language loading, or add explicit handling for XML in the tslp code path if needed. If the special case is indeed handled by tslp, consider adding a comment explaining why the special case is not needed here.

Copilot uses AI. Check for mistakes.


TREE_SITTER = _tree_sitter
24 changes: 17 additions & 7 deletions src/textual/widgets/_text_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,11 +773,11 @@ def code_editor(
)

@staticmethod
def _get_builtin_highlight_query(language_name: str) -> str:
"""Get the highlight query for a builtin language.
def _get_highlight_query(language_name: str) -> str:
"""Get the highlight query for a given language.

Args:
language_name: The name of the builtin language.
language_name: The name of the language.

Returns:
The highlight query.
Expand All @@ -788,9 +788,19 @@ def _get_builtin_highlight_query(language_name: str) -> str:
)
highlight_query = highlight_query_path.read_text()
except OSError as error:
log.warning(f"Unable to load highlight query. {error}")
highlight_query = ""

# Only use tree_sitter_language_pack as a fallback
try:
import tree_sitter_language_pack
except ImportError:
log.warning(f"Unable to load highlight query. {error}")
highlight_query = ""
else:
result = tree_sitter_language_pack.get_highlights_query(language_name)
if result:
highlight_query = result
else:
log.warning(f"Unable to load highlight query. {error}")
highlight_query = ""
return highlight_query

def notify_style_update(self) -> None:
Expand Down Expand Up @@ -1139,7 +1149,7 @@ def _set_document(self, text: str, language: str | None) -> None:
document_language = get_language(language)
else:
# No user-registered language, so attempt to use a built-in language.
highlight_query = self._get_builtin_highlight_query(language)
highlight_query = self._get_highlight_query(language)
document_language = get_language(language)

# No built-in language, and no user-registered language: use plain text and warn.
Expand Down
Loading