diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..45c383a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,15 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "pip" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "monthly"
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..c947046
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,47 @@
+name: Build and Publish Package
+
+on:
+ pull_request:
+ branches:
+ - main
+ types:
+ - closed
+
+jobs:
+ publish-package:
+ if: ${{ github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/v') }}
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out textarea main branch
+ uses: actions/checkout@v4
+ with:
+ ref: main
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+ - name: Install Poetry
+ uses: snok/install-poetry@v1
+ with:
+ version: 1.2.2
+ - name: Configure poetry
+ run: poetry config --no-interaction pypi-token.pypi ${{ secrets.TEXTAREA_PYPI_TOKEN }}
+ - name: Get textarea Version
+ id: textarea_version
+ run: echo "textarea_version=$(poetry version --short)" >> $GITHUB_OUTPUT
+ - name: Build package
+ run: poetry build --no-interaction
+ - name: Publish package to PyPI
+ run: poetry publish --no-interaction
+ - name: Create a Github Release
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: v${{ steps.textarea_version.outputs.textarea_version }}
+ target_commitish: main
+ token: ${{ secrets.TEXTAREA_RELEASE_TOKEN }}
+ body_path: CHANGELOG.md
+ files: |
+ LICENSE
+ dist/*textual_textarea*.whl
+ dist/*textual_textarea*.tar.gz
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..86d8e3d
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,56 @@
+name: Create Release Branch
+
+on:
+ workflow_dispatch:
+ inputs:
+ newVersion:
+ description: A version number for this release (e.g., "0.1.0")
+ required: true
+
+jobs:
+ prepare-release:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+
+ steps:
+ - name: Check out textarea main branch
+ uses: actions/checkout@v4
+ with:
+ ref: main
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+ - name: Install Poetry
+ uses: snok/install-poetry@v1
+ with:
+ version: 1.2.2
+ - name: Create release branch
+ run: |
+ git checkout -b release/v${{ github.event.inputs.newVersion }}
+ git push --set-upstream origin release/v${{ github.event.inputs.newVersion }}
+ - name: Bump version
+ run: poetry version ${{ github.event.inputs.newVersion }} --no-interaction
+ - name: Ensure package can be built
+ run: poetry build --no-interaction
+ - name: Update CHANGELOG
+ uses: thomaseizinger/keep-a-changelog-new-release@v3
+ with:
+ version: ${{ github.event.inputs.newVersion }}
+ - name: Commit Changes
+ uses: stefanzweifel/git-auto-commit-action@v5
+ with:
+ commit_message: Bumps version to ${{ github.event.inputs.newVersion }}
+ - name: Create pull request into main
+ uses: thomaseizinger/create-pull-request@1.3.1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ head: release/v${{ github.event.inputs.newVersion }}
+ base: main
+ title: v${{ github.event.inputs.newVersion }}
+ body: >
+ This PR was automatically generated. It bumps the version number
+ in pyproject.toml and updates CHANGELOG.md. You may have to close
+ this PR and reopen it to get the required checks to run.
diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
new file mode 100644
index 0000000..0dcd355
--- /dev/null
+++ b/.github/workflows/static.yml
@@ -0,0 +1,46 @@
+name: "Perform Static Analysis"
+
+on:
+ pull_request:
+
+# will cancel previous workflows triggered by the same event and for the same ref for PRs or same SHA otherwise
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.ref || github.sha }}
+ cancel-in-progress: true
+
+jobs:
+ static:
+ name: Static Analysis - 3.11
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out Repo
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - name: Set up Python 3.11
+ uses: actions/setup-python@v5
+ id: setup-python
+ with:
+ python-version: "3.11"
+ - name: Install Poetry
+ uses: snok/install-poetry@v1
+ with:
+ version: 1.4.2
+ virtualenvs-create: true
+ virtualenvs-in-project: true
+ installer-parallel: true
+ - name: Load cached venv
+ id: cached-poetry-dependencies
+ uses: actions/cache@v4
+ with:
+ path: .venv
+ key: static-venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
+ - name: Install python dependencies
+ if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
+ run: poetry install --sync --no-interaction --without dev
+ - name: Run analysis
+ run: |
+ source .venv/bin/activate
+ ruff format .
+ ruff check .
+ mypy
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..6394c7c
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,92 @@
+name: Test
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+
+# will cancel previous workflows triggered by the same event and for the same ref for PRs or same SHA otherwise
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.ref || github.sha }}
+ cancel-in-progress: true
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ test-windows:
+ name: Windows - 3.10
+ runs-on: Windows-latest
+ timeout-minutes: 10
+ steps:
+ - name: Check out Repo
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+ - name: Install Poetry
+ uses: snok/install-poetry@v1
+ with:
+ version: 1.4.2
+ - name: Install python dependencies
+ run: poetry install --sync --no-interaction --only main,test
+ - name: Run tests
+ run: poetry run pytest
+
+ test:
+ name: ${{ matrix.os }} - ${{ matrix.py }}
+ runs-on: ${{ matrix.os }}-latest
+ timeout-minutes: 10
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu
+ - MacOs
+ py:
+ - "3.13"
+ - "3.12"
+ - "3.11"
+ - "3.10"
+ - "3.9"
+ steps:
+ - name: Check out Repo
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - name: Set up Python ${{ matrix.py }}
+ id: setup-python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.py }}
+ - name: Load cached Poetry installation
+ id: cached-poetry-install
+ uses: actions/cache@v4
+ with:
+ path: ~/.local
+ key: poetry-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
+ - name: Install Poetry
+ if: steps.cached-poetry-install.outputs.cache-hit != 'true'
+ uses: snok/install-poetry@v1
+ with:
+ version: 1.4.2
+ virtualenvs-create: true
+ virtualenvs-in-project: true
+ installer-parallel: true
+ - name: Load cached venv
+ id: cached-poetry-dependencies
+ uses: actions/cache@v4
+ with:
+ path: .venv
+ key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
+ - name: Install python dependencies
+ if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
+ run: poetry install --sync --no-interaction --only main,test
+ - name: Run tests
+ run: |
+ source .venv/bin/activate
+ pytest
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b30ffe0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,166 @@
+*.db
+*.db.wal
+*.sql
+.profiles
+Pipfile
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..8b173b7
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,24 @@
+repos:
+ - repo: https://github.com/charliermarsh/ruff-pre-commit
+ rev: v0.5.1
+ hooks:
+ - id: ruff-format
+ - id: ruff
+ args: [ --fix, --exit-non-zero-on-fix ]
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.10.1
+ hooks:
+ - id: mypy
+ additional_dependencies:
+ - textual[syntax]>=0.89.1
+ - pytest
+ args:
+ - "--disallow-untyped-calls"
+ - "--disallow-untyped-defs"
+ - "--disallow-incomplete-defs"
+ - "--strict-optional"
+ - "--warn-return-any"
+ - "--warn-no-return"
+ - "--warn-redundant-casts"
+ - "--no-warn-unused-ignores"
+ - "--allow-untyped-decorators"
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..365b864
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,325 @@
+# textual-textarea CHANGELOG
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased]
+
+## [0.15.0] - 2024-12-19
+
+- **Breaking:** changes function return signature `query_syntax_tree`.
+- **Breaking:** drops support for Python 3.8.
+- **Breaking:** drops support for Pygments themes.
+- Adds support for Python 3.13.
+- Adds support for Textual app themes, including syntax highlighting using the theme colors.
+- Updates syntax highlighting language libraries.
+
+## [0.14.4] - 2024-10-09
+
+- Fixes a crash caused by copying or pasting with the system clipboard in some rare system configurations.
+
+## [0.14.3] - 2024-10-09
+
+- Fixes a crash caused by pressing `ctrl+g` twice ([tconbeer/harlequin#654](https://github.com/tconbeer/harlequin/issues/654)).
+
+## [0.14.2] - 2024-08-16
+
+- Fixes a TypeError raised by Textual >= v0.76 from the goto input validator.
+
+## [0.14.1] - 2024-08-15
+
+- Fixes a bug where uncommenting a line using the `toggle_comment` action would leave behind a space in languages with comment markers that are longer than one character ([tconbeer/harlequin#616](https://github.com/tconbeer/harlequin/issues/616)).
+
+## [0.14.0] - 2024-07-09
+
+- Updates dependencies and removes black in favor of the ruff formatter.
+
+## [0.13.1] - 2024-06-28
+
+- Bumps the pyperclip version for improved clipboard support on Wayland (Linux) ([tconbeer/harlequin#585](https://github.com/tconbeer/harlequin/issues/585)).
+
+## [0.13.0] - 2024-04-19
+
+- Adds a "find" action with ctrl+f and "find next" action with F3.
+- Adds "go to line" action with ctrl+g.
+- Adds bindings for `ctrl+shift+home` and `ctrl+shift+end` to select while moving to document start/end.
+- Uses the new, native undo and redo functionality provided by the Textual TextArea widget. This has some subtly different behavior ([#240](https://github.com/tconbeer/textual-textarea/issues/240).
+
+## [0.12.0] - 2024-04-17
+
+- Show the computed filepath in the Save and Open widgets ([#232](https://github.com/tconbeer/textual-textarea/issues/232) - thank you [@bjornasm](https://github.com/bjornasm)!).
+- Fixes a crash from initializing the Error Modal incorrectly.
+- Fixes a crash from saving to a path in a non-existent directory.
+
+## [0.11.3] - 2024-02-09
+
+- No longer changes focus on `escape` (regression since 0.11.0)
+
+## [0.11.2] - 2024-02-08
+
+- Adds a `parser` property to the `CodeEditor` class to return the document's tree-sitter parser.
+
+## [0.11.1] - 2024-02-08
+
+- Adds a `syntax_tree` property to the `CodeEditor` class to return the document's tree-sitter syntax tree.
+
+## [0.11.0] - 2024-02-06
+
+- Bumps textual dependency to >=0.48.1
+- Breaking change: Renames the main class from TextArea to TextEditor, to avoid naming conflicts with the built-in TextArea widget (which caused issues with selectors, CSS, etc.).
+- Breaking change: Replaces the `cursor` and `selection_anchor` API with `selection`.
+- Adds public APIs: `line_count`, `get_line`, `get_text_range`, `copy_to_clipboard`, `pause_blink`, `restart_blink`, `prepare_query`, and `query_syntax_tree`.
+
+## [0.10.0] - 2024-01-30
+
+- Adds a `text` argument when initializing TextArea.
+- Improves time to first paint by finding the system clipboard in a thread, instead of blocking mounting. This has an especially large impact on Windows.
+
+## [0.9.5] - 2023-12-18
+
+- Ignore spurious `ctrl+@` keys generated by Windows. (See [textualize/textual#872](https://github.com/Textualize/textual/issues/872)).
+
+## [0.9.4] - 2023-12-18
+
+- No longer show bindings in the footer when the open or save inputs are focussed.
+
+## [0.9.3] - 2023-12-15
+
+- Fixes an issue where very long completions with short prefixes were truncated improperly.
+
+## [0.9.2] - 2023-12-13
+
+- Hides the cursor and autocomplete list when the TextArea widget is not focussed. ([#177](https://github.com/tconbeer/textual-textarea/issues/177)).
+
+## [0.9.1] - 2023-12-13
+
+- Fixes an issue where the autocomplete list was displayed in the wrong location after pressing backspace.
+
+## [0.9.0] - 2023-12-12
+
+- TextArea now provides auto-complete. By default, it will auto-complete paths; to auto-complete words or
+ members of a namespace, set TextArea.word_completer, TextArea.member_completer, TextArea.path_completer
+ to a Callable\[[str], list\[tuple[str, str]]]. The callables will receive the current word (or path, etc.) as their
+ argument and should return a list of completions, where completions are (label, value) pairs.
+- The TextArea is now focused when the Open or Save inputs are cancelled.
+
+## [0.8.0] - 2023-12-06
+
+- The TextArea has been completely overhauled. It now uses the built-in TextArea widget under the hood.
+- This package now requires Textual >= 0.41.0, as it requires Textual's built-in TextArea widget.
+- Double-click a word to select it; triple-click to select the row; quadruple-click to select the whole document.
+- Fixes a bug with toggling comments.
+
+## [0.7.3] - 2023-10-06
+
+- The PathInput cursor no longer blinks if the app is run in headless mode (during tests). This only matters to prevent
+ flaky tests for snapshot testing this widget and downstream apps.
+
+## [0.7.2] - 2023-10-06
+
+- The TextArea cursor no longer blinks if the app is run in headless mode (during tests). This only matters to prevent
+ flaky tests for snapshot testing this widget and downstream apps.
+
+## [0.7.1] - 2023-09-22
+
+- TextArea now posts a `TextAreaSaved` message if it successfully saves a file.
+
+## [0.7.0] - 2023-09-20
+
+### Features
+
+- TextArea now posts a `TextAreaClipboardError` message if it cannot access the system clipboard.
+
+### Fixes
+
+- TextArea now uses the contents of the system Paste message, instead of relying exclusively on the
+ system clipboard. This should improve compatibility when Harlequin's host does not share its
+ clipboard with the user's native system.
+- When using the system clipboard, TextArea now initializes the clipboard on mount, resulting in
+ better performance when copying and pasting.
+- `textual_textarea.key_handlers.Cursor` is now exported from the main `textual_textarea` package.
+- Cursor position is no longer updated on a right-click.
+
+## [0.6.0] - 2023-09-08
+
+### Features
+
+- Adds a new public method, `TextArea.insert_text_at_selection(text)`.
+
+## [0.5.4] - 2023-09-01
+
+### Bug Fixes
+
+- up, down, pageup, and pagedown now better maintain the cursor's x-position when starting with an x-position that is longer than adjacent lines ([#94](https://github.com/tconbeer/textual-textarea/issues/94)).
+
+## [0.5.3] - 2023-09-01
+
+### Bug Fixes
+
+- Undo is smarter about cursor positions and selections; it no longer saves a new checkpoint for every cursor position. ([#86](https://github.com/tconbeer/textual-textarea/issues/86)).
+- Clicks within the container but outside text will still update the cursor ([#93](https://github.com/tconbeer/textual-textarea/issues/93)).
+- The cursor is now scrolled into position much faster.
+
+## [0.5.2] - 2023-08-23
+
+### Bug Fixes
+
+- TextArea now uses the highlight color from the Pygments Style to highlight selected text.
+
+## [0.5.1] - 2023-08-23
+
+### Bug Fixes
+
+- Fixes a crash caused by shift+delete on a buffer with only one line.
+
+## [0.5.0] - 2023-08-22
+
+### Features
+
+- Undo any input with ctrl+z; redo with ctrl+y ([#12](https://github.com/tconbeer/textual-textarea/issues/12)).
+- shift+delete now deletes the current line if there is no selection ([#77](https://github.com/tconbeer/textual-textarea/issues/77)).
+
+### Tests
+
+- Adds basic fuzzing of text and keyboard inputs ([#50](https://github.com/tconbeer/textual-textarea/issues/50))
+
+## [0.4.2] - 2023-08-03
+
+### Bug Fixes
+
+- No longer clears selection for more keystrokes (e.g,. ctrl+j)
+- Better-maintains selection and cursor position when bulk commenting or uncommenting with ctrl+/
+
+## [0.4.1] - 2023-08-03
+
+### Features
+
+- Adds a parameter to PathInput to allow tab to advance the focus.
+
+## [0.4.0] - 2023-08-03
+
+### Features
+
+- Adds a suggester to autocomplete paths for the save and open file inputs.
+- Adds a validator to validate paths for the save and open file inputs.
+- `textual-textarea` now requires `textual` >=0.27.0
+- Adds reactive properties to the textarea for `selection_anchor` position and
+ `selected_text`.
+
+## [0.3.3] - 2023-07-28
+
+### Features
+
+- The open and save file inputs now expand the user home directory (`~`).
+
+### Bug Fixes
+
+- Selection should be better-maintained when pressing F-keys.
+
+## [0.3.2] - 2023-07-14
+
+### Bug Fixes
+
+- Improves support for pasting text with `ctrl+v` on all platforms. ([#53](https://github.com/tconbeer/textual-textarea/issues/53)).
+
+## [0.3.1] - 2023-06-26
+
+### Bug Fixes
+
+- Fixes issue where text area was aggressively capturing mouse events and not responding to mouse up events,
+ which would cause issues if your App had widgets other than the TextArea ([#42](https://github.com/tconbeer/textual-textarea/issues/42)).
+- Fixes an issue where PageUp could cause a crash ([#46](https://github.com/tconbeer/textual-textarea/issues/46)).
+
+## [0.3.0] - 2023-06-19
+
+- Select text using click and drag ([#8](https://github.com/tconbeer/textual-textarea/issues/8)).
+- Comment characters inserted with ctrl+/ are now based on the language that the
+ TextArea is initialized with ([#24](https://github.com/tconbeer/textual-textarea/issues/24)).
+- TextArea exposes a `language` property for the currently-configured language.
+
+## [0.2.2] - 2023-06-15
+
+### Features
+
+- Adds a cursor attribute to TextArea to make it easier to get and set the TextInput's cursor position.
+- Adds 3 attributes to TextArea to make it easier to access the child widgets: `text_input`, `text_container`, and `footer`.
+
+### Bug Fixes
+
+- Fixes a bug that was preventing the cursor from being scrolled into view.
+
+## [0.2.1] - 2023-06-15
+
+### Bug Fixes
+
+- Fixes a bug where the TextArea did not update or have focus after opening a file ([#28](https://github.com/tconbeer/textual-textarea/issues/28))
+- Fixes a bug where a missing space at the end of the buffer after opening a file could cause a crash
+
+## [0.2.0] - 2023-06-14
+
+### Features
+
+- Uses the system clipboard (if it exists) for copy and paste operations, unless initialized
+ with `use_system_clipboard=False`.
+- Adds a sample app that can be run with `python -m textual_textarea`.
+
+## [0.1.2] - 2023-06-01
+
+- Makes top-level TextArea widget focusable
+- Loosens textual dependency to >=0.21.0
+- Adds py.typed file
+
+## [0.1.1] - 2023-06-01
+
+- Exports TextArea class under the main textual_textarea module.
+
+## [0.1.0] - 2023-06-01
+
+- Initial release: TextArea is a feature-rich text area (multiline) input, with
+ support for syntax highlighting, themes, keyboard navigation, copy-paste, file
+ opening and saving, and more!
+
+[unreleased]: https://github.com/tconbeer/textual-textarea/compare/0.15.0...HEAD
+[0.15.0]: https://github.com/tconbeer/textual-textarea/compare/0.14.4...0.15.0
+[0.14.4]: https://github.com/tconbeer/textual-textarea/compare/0.14.3...0.14.4
+[0.14.3]: https://github.com/tconbeer/textual-textarea/compare/0.14.2...0.14.3
+[0.14.2]: https://github.com/tconbeer/textual-textarea/compare/0.14.1...0.14.2
+[0.14.1]: https://github.com/tconbeer/textual-textarea/compare/0.14.0...0.14.1
+[0.14.0]: https://github.com/tconbeer/textual-textarea/compare/0.13.1...0.14.0
+[0.13.1]: https://github.com/tconbeer/textual-textarea/compare/0.13.0...0.13.1
+[0.13.0]: https://github.com/tconbeer/textual-textarea/compare/0.12.0...0.13.0
+[0.12.0]: https://github.com/tconbeer/textual-textarea/compare/0.11.3...0.12.0
+[0.11.3]: https://github.com/tconbeer/textual-textarea/compare/0.11.2...0.11.3
+[0.11.2]: https://github.com/tconbeer/textual-textarea/compare/0.11.1...0.11.2
+[0.11.1]: https://github.com/tconbeer/textual-textarea/compare/0.11.0...0.11.1
+[0.11.0]: https://github.com/tconbeer/textual-textarea/compare/0.10.0...0.11.0
+[0.10.0]: https://github.com/tconbeer/textual-textarea/compare/0.9.5...0.10.0
+[0.9.5]: https://github.com/tconbeer/textual-textarea/compare/0.9.4...0.9.5
+[0.9.4]: https://github.com/tconbeer/textual-textarea/compare/0.9.3...0.9.4
+[0.9.3]: https://github.com/tconbeer/textual-textarea/compare/0.9.2...0.9.3
+[0.9.2]: https://github.com/tconbeer/textual-textarea/compare/0.9.1...0.9.2
+[0.9.1]: https://github.com/tconbeer/textual-textarea/compare/0.9.0...0.9.1
+[0.9.0]: https://github.com/tconbeer/textual-textarea/compare/0.8.0...0.9.0
+[0.8.0]: https://github.com/tconbeer/textual-textarea/compare/0.7.3...0.8.0
+[0.7.3]: https://github.com/tconbeer/textual-textarea/compare/0.7.2...0.7.3
+[0.7.2]: https://github.com/tconbeer/textual-textarea/compare/0.7.1...0.7.2
+[0.7.1]: https://github.com/tconbeer/textual-textarea/compare/0.7.0...0.7.1
+[0.7.0]: https://github.com/tconbeer/textual-textarea/compare/0.6.0...0.7.0
+[0.6.0]: https://github.com/tconbeer/textual-textarea/compare/0.5.4...0.6.0
+[0.5.4]: https://github.com/tconbeer/textual-textarea/compare/0.5.3...0.5.4
+[0.5.3]: https://github.com/tconbeer/textual-textarea/compare/0.5.2...0.5.3
+[0.5.2]: https://github.com/tconbeer/textual-textarea/compare/0.5.1...0.5.2
+[0.5.1]: https://github.com/tconbeer/textual-textarea/compare/0.5.0...0.5.1
+[0.5.0]: https://github.com/tconbeer/textual-textarea/compare/0.4.2...0.5.0
+[0.4.2]: https://github.com/tconbeer/textual-textarea/compare/0.4.1...0.4.2
+[0.4.1]: https://github.com/tconbeer/textual-textarea/compare/0.4.0...0.4.1
+[0.4.0]: https://github.com/tconbeer/textual-textarea/compare/0.3.3...0.4.0
+[0.3.3]: https://github.com/tconbeer/textual-textarea/compare/0.3.2...0.3.3
+[0.3.2]: https://github.com/tconbeer/textual-textarea/compare/0.3.1...0.3.2
+[0.3.1]: https://github.com/tconbeer/textual-textarea/compare/0.3.0...0.3.1
+[0.3.0]: https://github.com/tconbeer/textual-textarea/compare/0.2.2...0.3.0
+[0.2.2]: https://github.com/tconbeer/textual-textarea/compare/0.2.1...0.2.2
+[0.2.1]: https://github.com/tconbeer/textual-textarea/compare/0.2.0...0.2.1
+[0.2.0]: https://github.com/tconbeer/textual-textarea/compare/0.1.2...0.2.0
+[0.1.2]: https://github.com/tconbeer/textual-textarea/compare/0.1.1...0.1.2
+[0.1.1]: https://github.com/tconbeer/textual-textarea/compare/0.1.0...0.1.1
+[0.1.0]: https://github.com/tconbeer/textual-textarea/compare/9832e9bbe1cd7a2ce9a4f09746eb1c2ddc8df842...0.1.0
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..04ec0c8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Ted Conbeer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..91db1c9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,21 @@
+.PHONY: check
+check:
+ ruff format .
+ ruff check . --fix
+ mypy
+ pytest
+
+.PHONY: lint
+lint:
+ ruff format .
+ ruff check . --fix
+ mypy
+
+.PHONY: serve
+serve:
+ textual run --dev -c python -m textual_textarea
+
+profiles: .profiles/startup.html
+
+.profiles/startup.html: src/scripts/profile_startup.py pyproject.toml $(wildcard src/textual_textarea/**/*.py)
+ pyinstrument -r html -o .profiles/startup.html "src/scripts/profile_startup.py"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9a03d7a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,150 @@
+# Textual Textarea
+
+
+## Note: This is **NOT** the official TextArea widget!
+
+With v0.38.0, Textual added a built-in TextArea widget. You probably want to use
+that widget instead of this one. This project predated the official widget; versions < v0.8.0
+had a completely separate implmentation.
+
+Since v0.8.0, this project uses the built-in TextArea widget, but adds the features outlined below.
+
+## Installation
+
+```
+pip install textual-textarea
+```
+
+## Features
+Full-featured text editor experience with VS-Code-like bindings, in your Textual App:
+- Syntax highlighting and support for Pygments themes.
+- Move cursor and scroll with mouse or keys (including ctrl+arrow, PgUp/Dn, ctrl+Home/End).
+- Open (ctrl+o) and save (ctrl+s) files.
+- Cut (ctrl+x), copy (ctrl+c), paste (ctrl+u/v), optionally using the system clipboard.
+- Comment selections with ctrl+/.
+- Indent and dedent (optionally for a multiline selection) to tab stops with Tab and shift+Tab.
+- Automatic completions of quotes and brackets.
+- Select text by double-, triple-, or quadruple-clicking.
+- Quit with ctrl+q.
+
+## Usage
+
+### Initializing the Widget
+
+The TextArea is a Textual Widget. You can add it to a Textual
+app using `compose` or `mount`:
+
+```python
+from textual_textarea import TextEditor
+from textual.app import App, ComposeResult
+
+class TextApp(App, inherit_bindings=False):
+ def compose(self) -> ComposeResult:
+ yield TextEditor(text="hi", language="python", theme="nord-darker", id="ta")
+
+ def on_mount(self) -> None:
+ editor = self.query_one("#id", expect_type=TextEditor)
+ editor.focus()
+
+app = TextApp()
+app.run()
+```
+
+In addition to the standard Widget arguments, TextArea accepts three additional, optional arguments when initializing the widget:
+
+- language (str): Must be `None` or the short name of a [Pygments lexer](https://pygments.org/docs/lexers/), e.g., `python`, `sql`, `as3`. Defaults to `None`.
+- theme (str): Must be name of a [Pygments style](https://pygments.org/styles/), e.g., `bw`, `github-dark`, `solarized-light`. Defaults to `monokai`.
+- use_system_clipboard (bool): Set to `False` to make the TextArea's copy and paste operations ignore the system clipboard. Defaults to `True`. Some Linux users may need to apt-install `xclip` or `xsel` to enable the system clipboard features.
+
+The TextArea supports many actions and key bindings. **For proper binding of `ctrl+c` to the COPY action,
+you must initialize your App with `inherit_bindings=False`** (as shown above), so that `ctrl+c` does not quit the app. The TextArea implements `ctrl+q` as quit; you way wish to mimic that in your app so that other in-focus widgets use the same behavior.
+
+### Interacting with the Widget
+
+#### Getting and Setting Text
+
+The TextArea exposes a `text` property that contains the full text contained in the widget. You can retrieve or set the text by interacting with this property:
+
+```python
+editor = self.query_one(TextEditor)
+old_text = editor.text
+editor.text = "New Text!\n\nMany Lines!"
+```
+
+Similarly, the TextEditor exposes a `selected_text` property (read-only):
+```python
+editor = self.query_one(TextEditor)
+selection = editor.selected_text
+```
+
+#### Inserting Text
+
+You can insert text at the current selection:
+```python
+editor = self.query_one(TextEditor)
+editor.text = "01234"
+editor.selection = Selection((0, 2), (0, 2))
+editor.insert_text_at_selection("\nabc\n")
+assert editor.text == "01\nabc\n234"
+assert editor.selection == Selection((2, 0), (2, 0))
+```
+
+#### Getting and Setting The Cursor Position
+
+The TextEditor exposes a `selection` property that returns a textual.widgets.text_area.Selection:
+
+```python
+editor = self.query_one(TextEditor)
+old_selection = editor.selection
+editor.selection = Selection((999, 0),(999, 0)) # the cursor will move as close to line 999, pos 0 as possible
+cursor_line_number = editor.selection.end[0]
+cursor_x_position = editor.selection.end[1]
+```
+
+
+#### Getting and Setting The Language
+
+Syntax highlighting and comment insertion depends on the configured language for the TextEditor.
+
+The TextArea exposes a `language` property that returns `None` or a string that is equal to the short name of an installed tree-sitter language:
+
+```python
+editor = self.query_one(TextEditor)
+old_language = editor.language
+editor.language = "python"
+```
+
+#### Getting Theme Colors
+
+If you would like the rest of your app to match the colors from the TextArea's theme, they are exposed via the `theme_colors` property.
+
+```python
+editor = self.query_one(TextEditor)
+color = editor.theme_colors.contrast_text_color
+bgcolor = editor.theme_colors.bgcolor
+highlight = editor.theme_colors.selection_bgcolor
+```
+
+
+#### Adding Bindings and other Behavior
+
+You can subclass TextEditor to add your own behavior. This snippet adds an action that posts a Submitted message containing the text of the TextEditor when the user presses ctrl+j:
+
+```python
+from textual.message import Message
+from textual_textarea import TextEditor
+
+
+class CodeEditor(TextEditor):
+ BINDINGS = [
+ ("ctrl+j", "submit", "Run Query"),
+ ]
+
+ class Submitted(Message, bubble=True):
+ def __init__(self, text: str) -> None:
+ super().__init__()
+ self.text = text
+
+ async def action_submit(self) -> None:
+ self.post_message(self.Submitted(self.text))
+```
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..ac8250e
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,1816 @@
+# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
+
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.4.4"
+description = "Happy Eyeballs for asyncio"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"},
+ {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"},
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.11.9"
+description = "Async http client/server framework (asyncio)"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0411777249f25d11bd2964a230b3ffafcbed6cd65d0f2b132bc2b8f5b8c347c7"},
+ {file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:499368eb904566fbdf1a3836a1532000ef1308f34a1bcbf36e6351904cced771"},
+ {file = "aiohttp-3.11.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b5a5009b0159a8f707879dc102b139466d8ec6db05103ec1520394fdd8ea02c"},
+ {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:176f8bb8931da0613bb0ed16326d01330066bb1e172dd97e1e02b1c27383277b"},
+ {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6435a66957cdba1a0b16f368bde03ce9c79c57306b39510da6ae5312a1a5b2c1"},
+ {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:202f40fb686e5f93908eee0c75d1e6fbe50a43e9bd4909bf3bf4a56b560ca180"},
+ {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39625703540feb50b6b7f938b3856d1f4886d2e585d88274e62b1bd273fae09b"},
+ {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6beeac698671baa558e82fa160be9761cf0eb25861943f4689ecf9000f8ebd0"},
+ {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:96726839a42429318017e67a42cca75d4f0d5248a809b3cc2e125445edd7d50d"},
+ {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3f5461c77649358610fb9694e790956b4238ac5d9e697a17f63619c096469afe"},
+ {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4313f3bc901255b22f01663eeeae167468264fdae0d32c25fc631d5d6e15b502"},
+ {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d6e274661c74195708fc4380a4ef64298926c5a50bb10fbae3d01627d7a075b7"},
+ {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db2914de2559809fdbcf3e48f41b17a493b58cb7988d3e211f6b63126c55fe82"},
+ {file = "aiohttp-3.11.9-cp310-cp310-win32.whl", hash = "sha256:27935716f8d62c1c73010428db310fd10136002cfc6d52b0ba7bdfa752d26066"},
+ {file = "aiohttp-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:afbe85b50ade42ddff5669947afde9e8a610e64d2c80be046d67ec4368e555fa"},
+ {file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:afcda759a69c6a8be3aae764ec6733155aa4a5ad9aad4f398b52ba4037942fe3"},
+ {file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5bba6b83fde4ca233cfda04cbd4685ab88696b0c8eaf76f7148969eab5e248a"},
+ {file = "aiohttp-3.11.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:442356e8924fe1a121f8c87866b0ecdc785757fd28924b17c20493961b3d6697"},
+ {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f737fef6e117856400afee4f17774cdea392b28ecf058833f5eca368a18cf1bf"},
+ {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea142255d4901b03f89cb6a94411ecec117786a76fc9ab043af8f51dd50b5313"},
+ {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1e9e447856e9b7b3d38e1316ae9a8c92e7536ef48373de758ea055edfd5db5"},
+ {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f6173302f8a329ca5d1ee592af9e628d3ade87816e9958dcf7cdae2841def7"},
+ {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c6147c6306f537cff59409609508a1d2eff81199f0302dd456bb9e7ea50c39"},
+ {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e9d036a9a41fc78e8a3f10a86c2fc1098fca8fab8715ba9eb999ce4788d35df0"},
+ {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2ac9fd83096df36728da8e2f4488ac3b5602238f602706606f3702f07a13a409"},
+ {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3108f0ad5c6b6d78eec5273219a5bbd884b4aacec17883ceefaac988850ce6e"},
+ {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:96bbec47beb131bbf4bae05d8ef99ad9e5738f12717cfbbf16648b78b0232e87"},
+ {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc726c3fa8f606d07bd2b500e5dc4c0fd664c59be7788a16b9e34352c50b6b6b"},
+ {file = "aiohttp-3.11.9-cp311-cp311-win32.whl", hash = "sha256:5720ebbc7a1b46c33a42d489d25d36c64c419f52159485e55589fbec648ea49a"},
+ {file = "aiohttp-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:17af09d963fa1acd7e4c280e9354aeafd9e3d47eaa4a6bfbd2171ad7da49f0c5"},
+ {file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c1f2d7fd583fc79c240094b3e7237d88493814d4b300d013a42726c35a734bc9"},
+ {file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4b8a1b6c7a68c73191f2ebd3bf66f7ce02f9c374e309bdb68ba886bbbf1b938"},
+ {file = "aiohttp-3.11.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd3f711f4c99da0091ced41dccdc1bcf8be0281dc314d6d9c6b6cf5df66f37a9"},
+ {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44cb1a1326a0264480a789e6100dc3e07122eb8cd1ad6b784a3d47d13ed1d89c"},
+ {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a7ddf981a0b953ade1c2379052d47ccda2f58ab678fca0671c7c7ca2f67aac2"},
+ {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ffa45cc55b18d4ac1396d1ddb029f139b1d3480f1594130e62bceadf2e1a838"},
+ {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cca505829cdab58c2495ff418c96092d225a1bbd486f79017f6de915580d3c44"},
+ {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44d323aa80a867cb6db6bebb4bbec677c6478e38128847f2c6b0f70eae984d72"},
+ {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b2fab23003c4bb2249729a7290a76c1dda38c438300fdf97d4e42bf78b19c810"},
+ {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:be0c7c98e38a1e3ad7a6ff64af8b6d6db34bf5a41b1478e24c3c74d9e7f8ed42"},
+ {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cc5e0d069c56645446c45a4b5010d4b33ac6c5ebfd369a791b5f097e46a3c08"},
+ {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9bcf97b971289be69638d8b1b616f7e557e1342debc7fc86cf89d3f08960e411"},
+ {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c7333e7239415076d1418dbfb7fa4df48f3a5b00f8fdf854fca549080455bc14"},
+ {file = "aiohttp-3.11.9-cp312-cp312-win32.whl", hash = "sha256:9384b07cfd3045b37b05ed002d1c255db02fb96506ad65f0f9b776b762a7572e"},
+ {file = "aiohttp-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:f5252ba8b43906f206048fa569debf2cd0da0316e8d5b4d25abe53307f573941"},
+ {file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:282e0a7ddd36ebc411f156aeaa0491e8fe7f030e2a95da532cf0c84b0b70bc66"},
+ {file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd3e6b0c7d4954cca59d241970011f8d3327633d555051c430bd09ff49dc494"},
+ {file = "aiohttp-3.11.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30f9f89ae625d412043f12ca3771b2ccec227cc93b93bb1f994db6e1af40a7d3"},
+ {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a3b5b2c012d70c63d9d13c57ed1603709a4d9d7d473e4a9dfece0e4ea3d5f51"},
+ {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ef1550bb5f55f71b97a6a395286db07f7f2c01c8890e613556df9a51da91e8d"},
+ {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317251b9c9a2f1a9ff9cd093775b34c6861d1d7df9439ce3d32a88c275c995cd"},
+ {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cbe97839b009826a61b143d3ca4964c8590d7aed33d6118125e5b71691ca46"},
+ {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:618b18c3a2360ac940a5503da14fa4f880c5b9bc315ec20a830357bcc62e6bae"},
+ {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0cf4d814689e58f57ecd5d8c523e6538417ca2e72ff52c007c64065cef50fb2"},
+ {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:15c4e489942d987d5dac0ba39e5772dcbed4cc9ae3710d1025d5ba95e4a5349c"},
+ {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8df0ff5a911c6d21957a9182402aad7bf060eaeffd77c9ea1c16aecab5adbf"},
+ {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ed95d66745f53e129e935ad726167d3a6cb18c5d33df3165974d54742c373868"},
+ {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:647ec5bee7e4ec9f1034ab48173b5fa970d9a991e565549b965e93331f1328fe"},
+ {file = "aiohttp-3.11.9-cp313-cp313-win32.whl", hash = "sha256:ef2c9499b7bd1e24e473dc1a85de55d72fd084eea3d8bdeec7ee0720decb54fa"},
+ {file = "aiohttp-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:84de955314aa5e8d469b00b14d6d714b008087a0222b0f743e7ffac34ef56aff"},
+ {file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e738aabff3586091221044b7a584865ddc4d6120346d12e28e788307cd731043"},
+ {file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28f29bce89c3b401a53d6fd4bee401ee943083bf2bdc12ef297c1d63155070b0"},
+ {file = "aiohttp-3.11.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31de2f10f63f96cc19e04bd2df9549559beadd0b2ee2da24a17e7ed877ca8c60"},
+ {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f31cebd8c27a36af6c7346055ac564946e562080ee1a838da724585c67474f"},
+ {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0bcb7f6976dc0b6b56efde13294862adf68dd48854111b422a336fa729a82ea6"},
+ {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8b13b9950d8b2f8f58b6e5842c4b842b5887e2c32e3f4644d6642f1659a530"},
+ {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c23e62f3545c2216100603614f9e019e41b9403c47dd85b8e7e5015bf1bde0"},
+ {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec656680fc53a13f849c71afd0c84a55c536206d524cbc831cde80abbe80489e"},
+ {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:36df00e0541f264ce42d62280281541a47474dfda500bc5b7f24f70a7f87be7a"},
+ {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8dcfd14c712aa9dd18049280bfb2f95700ff6a8bde645e09f17c3ed3f05a0130"},
+ {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14624d96f0d69cf451deed3173079a68c322279be6030208b045ab77e1e8d550"},
+ {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4b01d9cfcb616eeb6d40f02e66bebfe7b06d9f2ef81641fdd50b8dd981166e0b"},
+ {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:928f92f80e2e8d6567b87d3316c1fd9860ccfe36e87a9a7f5237d4cda8baa1ba"},
+ {file = "aiohttp-3.11.9-cp39-cp39-win32.whl", hash = "sha256:c8a02f74ae419e3955af60f570d83187423e42e672a6433c5e292f1d23619269"},
+ {file = "aiohttp-3.11.9-cp39-cp39-win_amd64.whl", hash = "sha256:0a97d657f6cf8782a830bb476c13f7d777cfcab8428ac49dde15c22babceb361"},
+ {file = "aiohttp-3.11.9.tar.gz", hash = "sha256:a9266644064779840feec0e34f10a89b3ff1d2d6b751fe90017abcad1864fa7c"},
+]
+
+[package.dependencies]
+aiohappyeyeballs = ">=2.3.0"
+aiosignal = ">=1.1.2"
+async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""}
+attrs = ">=17.3.0"
+frozenlist = ">=1.1.1"
+multidict = ">=4.5,<7.0"
+propcache = ">=0.2.0"
+yarl = ">=1.17.0,<2.0"
+
+[package.extras]
+speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"]
+
+[[package]]
+name = "aiohttp-jinja2"
+version = "1.6"
+description = "jinja2 template renderer for aiohttp.web (http server for asyncio)"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "aiohttp-jinja2-1.6.tar.gz", hash = "sha256:a3a7ff5264e5bca52e8ae547bbfd0761b72495230d438d05b6c0915be619b0e2"},
+ {file = "aiohttp_jinja2-1.6-py3-none-any.whl", hash = "sha256:0df405ee6ad1b58e5a068a105407dc7dcc1704544c559f1938babde954f945c7"},
+]
+
+[package.dependencies]
+aiohttp = ">=3.9.0"
+jinja2 = ">=3.0.0"
+
+[[package]]
+name = "aiosignal"
+version = "1.3.1"
+description = "aiosignal: a list of registered asynchronous callbacks"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
+ {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
+]
+
+[package.dependencies]
+frozenlist = ">=1.1.0"
+
+[[package]]
+name = "async-timeout"
+version = "5.0.1"
+description = "Timeout context manager for asyncio programs"
+optional = false
+python-versions = ">=3.8"
+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"},
+]
+
+[[package]]
+name = "attrs"
+version = "24.2.0"
+description = "Classes Without Boilerplate"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
+ {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
+]
+
+[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", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "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"]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+description = "Validate configuration and produce human readable error messages."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
+ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
+]
+
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+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"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "distlib"
+version = "0.3.9"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+files = [
+ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"},
+ {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"},
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.2"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
+ {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "filelock"
+version = "3.16.1"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"},
+ {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"},
+]
+
+[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)"]
+
+[[package]]
+name = "frozenlist"
+version = "1.5.0"
+description = "A list-like structure which implements collections.abc.MutableSequence"
+optional = false
+python-versions = ">=3.8"
+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"},
+ {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"},
+ {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"},
+ {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"},
+ {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"},
+ {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"},
+ {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"},
+ {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"},
+ {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"},
+ {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"},
+ {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"},
+ {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"},
+ {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"},
+ {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"},
+ {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"},
+ {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"},
+ {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"},
+ {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"},
+ {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"},
+ {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"},
+ {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"},
+ {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"},
+ {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"},
+ {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"},
+ {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"},
+ {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"},
+ {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"},
+ {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"},
+ {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"},
+ {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"},
+ {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"},
+ {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"},
+ {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"},
+ {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"},
+ {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"},
+ {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"},
+ {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"},
+ {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"},
+ {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"},
+ {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"},
+ {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"},
+ {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"},
+ {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"},
+ {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"},
+ {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"},
+ {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"},
+ {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"},
+ {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"},
+ {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"},
+ {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"},
+ {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"},
+ {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"},
+ {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"},
+ {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"},
+ {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"},
+ {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"},
+ {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"},
+ {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"},
+ {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"},
+ {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"},
+ {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"},
+ {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"},
+ {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"},
+ {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"},
+ {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"},
+ {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"},
+ {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"},
+ {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"},
+ {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"},
+ {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"},
+ {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"},
+ {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"},
+ {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"},
+ {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"},
+ {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"},
+ {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"},
+ {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"},
+ {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"},
+ {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"},
+ {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"},
+ {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"},
+ {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"},
+ {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"},
+ {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"},
+ {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"},
+ {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"},
+ {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"},
+ {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"},
+ {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"},
+ {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"},
+ {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"},
+]
+
+[[package]]
+name = "identify"
+version = "2.6.3"
+description = "File identification library for Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"},
+ {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "idna"
+version = "3.10"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
+ {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
+]
+
+[package.extras]
+all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.4"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
+ {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "linkify-it-py"
+version = "2.0.3"
+description = "Links recognition library with FULL unicode support."
+optional = false
+python-versions = ">=3.7"
+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"},
+]
+
+[package.dependencies]
+uc-micro-py = "*"
+
+[package.extras]
+benchmark = ["pytest", "pytest-benchmark"]
+dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"]
+doc = ["myst-parser", "sphinx", "sphinx-book-theme"]
+test = ["coverage", "pytest", "pytest-cov"]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+optional = false
+python-versions = ">=3.8"
+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"},
+]
+
+[package.dependencies]
+linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""}
+mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""}
+mdurl = ">=0.1,<1.0"
+
+[package.extras]
+benchmarking = ["psutil", "pytest", "pytest-benchmark"]
+code-style = ["pre-commit (>=3.0,<4.0)"]
+compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
+linkify = ["linkify-it-py (>=1,<3)"]
+plugins = ["mdit-py-plugins"]
+profiling = ["gprof2dot"]
+rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
+ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
+ {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"},
+ {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"},
+ {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"},
+ {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"},
+ {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"},
+ {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"},
+ {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"},
+ {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"},
+ {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"},
+ {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"},
+ {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"},
+ {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"},
+ {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"},
+ {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"},
+ {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"},
+ {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"},
+ {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"},
+ {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"},
+ {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"},
+ {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"},
+ {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"},
+ {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"},
+ {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"},
+ {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"},
+ {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"},
+ {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"},
+ {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"},
+ {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"},
+ {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"},
+ {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"},
+ {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"},
+ {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"},
+ {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"},
+ {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"},
+ {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"},
+ {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"},
+ {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"},
+ {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"},
+ {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"},
+ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
+]
+
+[[package]]
+name = "mdit-py-plugins"
+version = "0.4.2"
+description = "Collection of plugins for markdown-it-py"
+optional = false
+python-versions = ">=3.8"
+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"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=1.0.0,<4.0.0"
+
+[package.extras]
+code-style = ["pre-commit"]
+rtd = ["myst-parser", "sphinx-book-theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+description = "Markdown URL utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
+]
+
+[[package]]
+name = "msgpack"
+version = "1.1.0"
+description = "MessagePack serializer"
+optional = false
+python-versions = ">=3.8"
+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"},
+ {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"},
+ {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"},
+ {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"},
+ {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"},
+ {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"},
+ {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"},
+ {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"},
+ {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"},
+ {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"},
+ {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"},
+ {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"},
+ {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"},
+ {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"},
+ {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"},
+ {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"},
+ {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"},
+ {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"},
+ {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"},
+ {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"},
+ {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"},
+ {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"},
+ {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"},
+ {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"},
+ {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"},
+ {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"},
+ {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"},
+ {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"},
+ {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"},
+ {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"},
+ {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"},
+ {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"},
+ {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"},
+ {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"},
+ {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"},
+ {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"},
+ {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"},
+ {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"},
+ {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"},
+ {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"},
+ {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"},
+ {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"},
+ {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"},
+ {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"},
+ {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"},
+ {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"},
+ {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"},
+ {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"},
+ {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"},
+ {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"},
+ {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"},
+ {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"},
+ {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"},
+ {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"},
+ {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"},
+ {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"},
+ {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"},
+ {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"},
+ {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"},
+ {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"},
+ {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"},
+ {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"},
+ {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"},
+]
+
+[[package]]
+name = "multidict"
+version = "6.1.0"
+description = "multidict implementation"
+optional = false
+python-versions = ">=3.8"
+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"},
+ {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"},
+ {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"},
+ {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"},
+ {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"},
+ {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"},
+ {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"},
+ {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"},
+ {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"},
+ {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"},
+ {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"},
+ {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"},
+ {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"},
+ {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"},
+ {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"},
+ {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"},
+ {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"},
+ {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"},
+ {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"},
+ {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"},
+ {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"},
+ {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"},
+ {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"},
+ {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"},
+ {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"},
+ {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"},
+ {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"},
+ {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"},
+ {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"},
+ {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"},
+ {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"},
+ {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"},
+ {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"},
+ {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"},
+ {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"},
+ {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"},
+ {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"},
+ {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"},
+ {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"},
+ {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"},
+ {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"},
+ {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"},
+ {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"},
+ {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"},
+ {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"},
+ {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"},
+ {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"},
+ {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"},
+ {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"},
+ {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"},
+ {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"},
+ {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"},
+ {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"},
+ {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"},
+ {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"},
+ {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"},
+ {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"},
+ {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"},
+ {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"},
+ {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"},
+ {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"},
+ {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"},
+ {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"},
+ {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"},
+ {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"},
+ {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"},
+ {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"},
+ {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"},
+ {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"},
+ {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"},
+ {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"},
+ {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"},
+ {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"},
+ {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"},
+ {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"},
+ {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"},
+ {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"},
+ {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"},
+ {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"},
+ {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"},
+ {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"},
+ {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"},
+ {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"},
+ {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"},
+ {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"},
+ {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"},
+ {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"},
+ {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"},
+ {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"},
+ {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"},
+ {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""}
+
+[[package]]
+name = "mypy"
+version = "1.13.0"
+description = "Optional static typing for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
+ {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
+ {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
+ {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
+ {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
+ {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
+ {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
+ {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
+ {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
+ {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
+ {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
+ {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
+ {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
+ {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
+ {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
+ {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
+ {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
+ {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
+ {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
+ {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
+ {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
+ {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
+ {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
+ {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
+ {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
+ {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
+ {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
+ {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
+ {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
+ {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
+ {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
+ {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
+]
+
+[package.dependencies]
+mypy-extensions = ">=1.0.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = ">=4.6.0"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+faster-cache = ["orjson"]
+install-types = ["pip"]
+mypyc = ["setuptools (>=50)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "nodeenv"
+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"
+files = [
+ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
+ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
+]
+
+[[package]]
+name = "packaging"
+version = "24.2"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
+ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
+]
+
+[[package]]
+name = "platformdirs"
+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"
+files = [
+ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
+ {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
+]
+
+[package.extras]
+docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
+type = ["mypy (>=1.11.2)"]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pre-commit"
+version = "3.8.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
+ {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
+[[package]]
+name = "propcache"
+version = "0.2.1"
+description = "Accelerated property cache"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"},
+ {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"},
+ {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"},
+ {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"},
+ {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"},
+ {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"},
+ {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"},
+ {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"},
+ {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"},
+ {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"},
+ {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"},
+ {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"},
+ {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"},
+ {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"},
+ {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"},
+ {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"},
+ {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"},
+ {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"},
+ {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"},
+ {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"},
+ {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"},
+ {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"},
+ {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"},
+ {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"},
+ {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"},
+ {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"},
+ {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"},
+ {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"},
+ {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"},
+ {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"},
+ {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"},
+ {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"},
+ {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"},
+ {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"},
+ {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"},
+ {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"},
+ {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"},
+ {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"},
+ {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"},
+ {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"},
+ {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"},
+ {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"},
+ {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"},
+ {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"},
+ {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541"},
+ {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e"},
+ {file = "propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4"},
+ {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097"},
+ {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd"},
+ {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681"},
+ {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16"},
+ {file = "propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04"},
+ {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587"},
+ {file = "propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb"},
+ {file = "propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1"},
+ {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"},
+ {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"},
+]
+
+[[package]]
+name = "pygments"
+version = "2.18.0"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
+ {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
+]
+
+[package.extras]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pyinstrument"
+version = "5.0.0"
+description = "Call stack profiler for Python. Shows you why your code is slow!"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pyinstrument-5.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6a83cf18f5594e1b1899b12b46df7aabca556eef895846ccdaaa3a46a37d1274"},
+ {file = "pyinstrument-5.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1cc236313272d0222261be8e2b2a08e42d7ccbe54db9059babf4d77040da1880"},
+ {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd685d68a31f3715ca61f82c37c1c2f8b75f45646bd9840e04681d91862bd85"},
+ {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cecd0f6558f13fba74a9f036b2b168956206e9525dcb84c6add2d73ab61dc22"},
+ {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a8485c2e41082a20822001a6651667bb5327f6f5f6759987198593e45bb376"},
+ {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a6294b7111348765ba4c311fc91821ed8b59c6690c4dab23aa7165a67da9e972"},
+ {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a164f3dae5c7db2faa501639659d64034cde8db62a4d6744712593a369bc8629"},
+ {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f6bac8a434407de6f2ebddbcdecdb19b324c9315cbb8b8c2352714f7ced8181"},
+ {file = "pyinstrument-5.0.0-cp310-cp310-win32.whl", hash = "sha256:7e8dc887e535f5c5e5a2a64a0729496f11ddcef0c23b0a555d5ab6fa19759445"},
+ {file = "pyinstrument-5.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c337190a1818841732643ba93065411591df526bc9de44b97ba8f56b581d2ef"},
+ {file = "pyinstrument-5.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c9052f548ec5ccecc50676fbf1a1d0b60bdbd3cd67630c5253099af049d1f0ad"},
+ {file = "pyinstrument-5.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:197d25487f52da3f8ec26d46db7202bc5d703cc73c1503371166417eb7cea14e"},
+ {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a072d928dc16a32e0f3d1e51726f4472a69d66d838ee1d1bf248737fd70b9415"},
+ {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2c7ae2c984879a645fce583bf3053b7e57495f60c1e158bb71ad7dfced1fbf1"},
+ {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8284bf8847629c9a5054702b9306eab3ab14c2474959e01e606369ffbcf938bc"},
+ {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fd94cc725efb1dd41ae8e20a5f06a6a5363dec959e8a9dacbac3f4d12d28f03"},
+ {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e0fdb9fe6f9c694940410dcc82e23a3fe2928114328efd35047fc0bb8a6c959f"},
+ {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ffe938e63173ceb8ce7b6b309ce26c9d44d16f53c0162d89d6e706eb9e69802"},
+ {file = "pyinstrument-5.0.0-cp311-cp311-win32.whl", hash = "sha256:80d2a248516f372a89e0fe9ddf4a9d6388a4c6481b6ebd3dfe01b3cd028c0275"},
+ {file = "pyinstrument-5.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:7ccf4267aff62de0e1d976e8f5da25dcb69737ae86e38d3cfffa24877837e7d1"},
+ {file = "pyinstrument-5.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:dec3529a5351ea160baeef1ef2a6e28b1a7a7b3fb5e9863fae8de6da73d0f69a"},
+ {file = "pyinstrument-5.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5a39e3ef84c56183f8274dfd584b8c2fae4783c6204f880513e70ab2440b9137"},
+ {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3938f063ee065e05826628dadf1fb32c7d26b22df4a945c22f7fe25ea1ba6a2"},
+ {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18990cc16b2e23b54738aa2f222863e1d36daaaec8f67b1613ddfa41f5b24db"},
+ {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3731412b5bfdcef8014518f145140c69384793e218863a33a39ccfe5fb42045"},
+ {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02b2eaf38460b14eea646d6bb7f373eb5bb5691d13f788e80bdcb3a4eaa2519e"},
+ {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e57db06590f13657b2bce8c4d9cf8e9e2bd90bb729bcbbe421c531ba67ad7add"},
+ {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddaa3001c1b798ec9bf1266ef476bbc0834b74d547d531f5ed99e7d05ac5d81b"},
+ {file = "pyinstrument-5.0.0-cp312-cp312-win32.whl", hash = "sha256:b69ff982acf5ef2f4e0f32ce9b4b598f256faf88438f233ea3a72f1042707e5b"},
+ {file = "pyinstrument-5.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:0bf4ef061d60befe72366ce0ed4c75dee5be089644de38f9936d2df0bcf44af0"},
+ {file = "pyinstrument-5.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79a54def2d4aa83a4ed37c6cffc5494ae5de140f0453169eb4f7c744cc249d3a"},
+ {file = "pyinstrument-5.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9538f746f166a40c8802ebe5c3e905d50f3faa189869cd71c083b8a639e574bb"},
+ {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bbab65cae1483ad8a18429511d1eac9e3efec9f7961f2fd1bf90e1e2d69ef15"},
+ {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4351ad041d208c597e296a0e9c2e6e21cc96804608bcafa40cfa168f3c2b8f79"},
+ {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceee5252f4580abec29bcc5c965453c217b0d387c412a5ffb8afdcda4e648feb"},
+ {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b3050a4e7033103a13cfff9802680e2070a9173e1a258fa3f15a80b4eb9ee278"},
+ {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3b1f44a34da7810938df615fb7cbc43cd879b42ca6b5cd72e655aee92149d012"},
+ {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fde075196c8a3b2be191b8da05b92ff909c78d308f82df56d01a8cfdd6da07b9"},
+ {file = "pyinstrument-5.0.0-cp313-cp313-win32.whl", hash = "sha256:1a9b62a8b54e05e7723eb8b9595fadc43559b73290c87b3b1cb2dc5944559790"},
+ {file = "pyinstrument-5.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2478d2c55f77ad8e281e67b0dfe7c2176304bb824c307e86e11890f5e68d7feb"},
+ {file = "pyinstrument-5.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c2e3b4283f85232fd5818e2153e6798bceb39a8c3ccfaa22fae08faf554740b7"},
+ {file = "pyinstrument-5.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fb1139d2822abff1cbf1c81c018341f573b7afa23a94ce74888a0f6f47828cbc"},
+ {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c971566d86ba46a7233d3f5b0d85d7ee4c9863f541f5d8f796c3947ebe17f68"},
+ {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:429376235960179d6ab9b97e7871090059d39de160b4e3b2723672f30e8eea8e"},
+ {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8599b4b0630c776b30fc3c4f7476d5e3814ee7fe42d99131644fe3c00b40fdf1"},
+ {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc688afa2a5368042a7cb56866d5a28fdff8f37a282f7be79b17cae042841b"},
+ {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5d34c06e2276d1f549a540bccb063688ea3d876e6df7c391205f1c8b4b96d5c8"},
+ {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d3b2ec6e028731dbb2ba8cf06f19030162789e6696bca990a09519881ad42fb"},
+ {file = "pyinstrument-5.0.0-cp38-cp38-win32.whl", hash = "sha256:5ed6f5873a7526ec5915e45d956d044334ef302653cf63649e48c41561aaa285"},
+ {file = "pyinstrument-5.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:9e87d65bae7d0f5ef50908e35d67d43b7cc566909995cc99e91721bb49b4ea06"},
+ {file = "pyinstrument-5.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bd953163616bc29c2ccb1e4c0e48ccdd11e0a97fc849da26bc362bba372019ba"},
+ {file = "pyinstrument-5.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d2a7279ed9b6d7cdae247bc2e57095a32f35dfe32182c334ab0ac3eb02e0eac"},
+ {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68001dfcb8a37b624a1c3de5d2ee7d634f63eac7a6dd1357b7370a5cdbdcf567"},
+ {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c4c3cc6410ad5afe0e352a7fb09fb1ab85eb5676ec5ec8522123759d9cc68f"},
+ {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d87ddab66b1b3525ad3abc49a88aaa51efcaf83578e9d2a702c03a1cea39f28"},
+ {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03182ffaa9c91687cbaba80dc0c5a47015c5ea170fe642f632d88e885cf07356"},
+ {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:39b60417c9c12eed04e1886644e92aa0b281d72e5d0b097b16253cade43110f7"},
+ {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7bb389b6d1573361bd1367b296133c5c69184e35fc18db22e29e8cdf56f158f9"},
+ {file = "pyinstrument-5.0.0-cp39-cp39-win32.whl", hash = "sha256:ae69478815edb3c63e7ebf82e1e13e38c3fb2bab833b1c013643c3475b1b8cf5"},
+ {file = "pyinstrument-5.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:83caeb4150c0334e9e290c0f9bb164ff6bdc199065ecb62016268e8a88589a51"},
+ {file = "pyinstrument-5.0.0.tar.gz", hash = "sha256:144f98eb3086667ece461f66324bf1cc1ee0475b399ab3f9ded8449cc76b7c90"},
+]
+
+[package.extras]
+bin = ["click", "nox"]
+docs = ["furo (==2024.7.18)", "myst-parser (==3.0.1)", "sphinx (==7.4.7)", "sphinx-autobuild (==2024.4.16)", "sphinxcontrib-programoutput (==0.17)"]
+examples = ["django", "litestar", "numpy"]
+test = ["cffi (>=1.17.0)", "flaky", "greenlet (>=3)", "ipython", "pytest", "pytest-asyncio (==0.23.8)", "trio"]
+types = ["typing-extensions"]
+
+[[package]]
+name = "pyperclip"
+version = "1.9.0"
+description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310"},
+]
+
+[[package]]
+name = "pytest"
+version = "8.3.4"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
+ {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=1.5,<2"
+tomli = {version = ">=1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.21.2"
+description = "Pytest support for asyncio"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"},
+ {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.8"
+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"},
+ {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
+ {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
+ {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
+ {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
+ {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
+ {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
+ {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
+ {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
+ {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
+ {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
+ {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
+ {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
+ {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
+ {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
+ {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
+ {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
+ {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
+ {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
+ {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
+ {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
+ {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
+ {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
+ {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
+ {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
+ {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
+ {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
+ {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
+ {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
+ {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
+ {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
+ {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
+ {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
+ {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
+ {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
+ {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
+ {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
+ {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
+ {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
+ {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
+ {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
+ {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
+ {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
+ {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
+ {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
+ {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
+ {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
+ {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
+ {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
+ {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
+ {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
+ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
+]
+
+[[package]]
+name = "rich"
+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"
+files = [
+ {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
+ {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=2.2.0"
+pygments = ">=2.13.0,<3.0.0"
+typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<9)"]
+
+[[package]]
+name = "ruff"
+version = "0.5.7"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"},
+ {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"},
+ {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"},
+ {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"},
+ {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"},
+ {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"},
+ {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"},
+ {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"},
+ {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"},
+ {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"},
+ {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"},
+]
+
+[[package]]
+name = "textual"
+version = "0.89.1"
+description = "Modern Text User Interface framework"
+optional = false
+python-versions = "<4.0.0,>=3.8.1"
+files = [
+ {file = "textual-0.89.1-py3-none-any.whl", hash = "sha256:0a5d214df6e951b4a2c421e13d0b608482882471c1e34ea74a3631adede8054f"},
+ {file = "textual-0.89.1.tar.gz", hash = "sha256:66befe80e2bca5a8c876cd8ceeaf01752267b6b1dc1d0f73071f1f1e15d90cc8"},
+]
+
+[package.dependencies]
+markdown-it-py = {version = ">=2.1.0", extras = ["linkify", "plugins"]}
+platformdirs = ">=3.6.0,<5"
+rich = ">=13.3.3"
+tree-sitter = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-bash = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-css = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-go = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-html = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-java = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-javascript = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-json = {version = ">=0.24.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-markdown = {version = ">=0.3.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-python = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-regex = {version = ">=0.24.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-rust = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-sql = {version = ">=0.3.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-toml = {version = ">=0.6.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-xml = {version = ">=0.7.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+tree-sitter-yaml = {version = ">=0.6.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""}
+typing-extensions = ">=4.4.0,<5.0.0"
+
+[package.extras]
+syntax = ["tree-sitter (>=0.23.0)", "tree-sitter-bash (>=0.23.0)", "tree-sitter-css (>=0.23.0)", "tree-sitter-go (>=0.23.0)", "tree-sitter-html (>=0.23.0)", "tree-sitter-java (>=0.23.0)", "tree-sitter-javascript (>=0.23.0)", "tree-sitter-json (>=0.24.0)", "tree-sitter-markdown (>=0.3.0)", "tree-sitter-python (>=0.23.0)", "tree-sitter-regex (>=0.24.0)", "tree-sitter-rust (>=0.23.0)", "tree-sitter-sql (>=0.3.0)", "tree-sitter-toml (>=0.6.0)", "tree-sitter-xml (>=0.7.0)", "tree-sitter-yaml (>=0.6.0)"]
+
+[[package]]
+name = "textual-dev"
+version = "1.7.0"
+description = "Development tools for working with Textual"
+optional = false
+python-versions = "<4.0.0,>=3.8.1"
+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"},
+]
+
+[package.dependencies]
+aiohttp = ">=3.8.1"
+click = ">=8.1.2"
+msgpack = ">=1.0.3"
+textual = ">=0.86.2"
+textual_serve = ">=1.0.3"
+typing-extensions = ">=4.4.0,<5.0.0"
+
+[[package]]
+name = "textual-serve"
+version = "1.1.1"
+description = "Turn your Textual TUIs in to web applications"
+optional = false
+python-versions = ">=3.8"
+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"},
+]
+
+[package.dependencies]
+aiohttp = ">=3.9.5"
+aiohttp-jinja2 = ">=1.6"
+jinja2 = ">=3.1.4"
+rich = "*"
+textual = ">=0.66.0"
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.8"
+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"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
+ {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
+ {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
+ {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
+ {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
+ {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
+ {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
+ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
+ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
+]
+
+[[package]]
+name = "tree-sitter"
+version = "0.23.2"
+description = "Python bindings to the Tree-sitter parsing library"
+optional = false
+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"},
+]
+
+[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)"]
+
+[[package]]
+name = "tree-sitter-bash"
+version = "0.23.3"
+description = "Bash grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_bash-0.23.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa4b5dde719291eea3a81b1f9ece6afeee2deadc2b2f769bee92f955da7595cf"},
+ {file = "tree_sitter_bash-0.23.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff7bffc3d594e7f1054de051e19df1b24082963598a175dda64083c6b3eea1a"},
+ {file = "tree_sitter_bash-0.23.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4427baccbd7549a2ebb1859b6d42cdab0739c05d53c2b3daad9cadc069a7b3f6"},
+ {file = "tree_sitter_bash-0.23.3-cp39-abi3-win_amd64.whl", hash = "sha256:525c5cce28a7c5624fb016ac8f3ae33d32968567b718f7878c6351229d2e8394"},
+ {file = "tree_sitter_bash-0.23.3-cp39-abi3-win_arm64.whl", hash = "sha256:1f703d1bf6235355f6c900be64bf9f61fc4b1d0cfed6829b4eeb74a6b41ea910"},
+ {file = "tree_sitter_bash-0.23.3.tar.gz", hash = "sha256:7b15ed89a1ea8e3e3c2399758746413e464d4c1c3a6d3b75d643ae2bc2fb356b"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-css"
+version = "0.23.1"
+description = "CSS grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "tree_sitter_css-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6b38462fef7d14b0bfa6e542faab7d3cfd267b8dc138efcf6e2cee11f6988084"},
+ {file = "tree_sitter_css-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:33239e810c518b27fa7b4592d31f6cb63c43d4ea55532b4eb346ac4c9974a7f4"},
+ {file = "tree_sitter_css-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c4cefceb654f89de8e79563d960f87b9a4680f288d87e20bacca7c339392070"},
+ {file = "tree_sitter_css-0.23.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6524a5d097224128c9cda797c09f5704af0705e0ff272cf2f41ec192aa06aa62"},
+ {file = "tree_sitter_css-0.23.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2999be3784999ced8b8d6a4470f0aec28cdc42b31fd9861041a70c834a2c8850"},
+ {file = "tree_sitter_css-0.23.1-cp39-abi3-win_amd64.whl", hash = "sha256:8824f079e7454491347eda4cdbf9cde606c4e5de518cc85bb69cd9dfd67b8982"},
+ {file = "tree_sitter_css-0.23.1-cp39-abi3-win_arm64.whl", hash = "sha256:6ff44819511fe517f6d32f1d8a3563da30093ca155dd1198585819598e83d755"},
+ {file = "tree_sitter_css-0.23.1.tar.gz", hash = "sha256:a5dadf23e201f05606feaa638d0e423050a3d56cea2324c8859857fbbc3f69e8"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-go"
+version = "0.23.4"
+description = "Go grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_go-0.23.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:330ecbb38d6ea4ef41eba2d473056889705e64f6a51c2fb613de05b1bcb5ba22"},
+ {file = "tree_sitter_go-0.23.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd14d23056ae980debfccc0db67d0a168da03792ca2968b1b5dd58ce288084e7"},
+ {file = "tree_sitter_go-0.23.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c3b40912487fdb78c4028860dd79493a521ffca0104f209849823358db3618a0"},
+ {file = "tree_sitter_go-0.23.4-cp39-abi3-win_amd64.whl", hash = "sha256:ae4b231cad2ef76401d33617879cda6321c4d0853f7fd98cb5654c50a218effb"},
+ {file = "tree_sitter_go-0.23.4-cp39-abi3-win_arm64.whl", hash = "sha256:2ac907362a3c347145dc1da0858248546500a323de90d2cb76d2a3fdbfc8da25"},
+ {file = "tree_sitter_go-0.23.4.tar.gz", hash = "sha256:0ebff99820657066bec21690623a14c74d9e57a903f95f0837be112ddadf1a52"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-html"
+version = "0.23.2"
+description = "HTML grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_html-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b3775732fffc0abd275a419ef018fd4c1ad4044b2a2e422f3378d93c30eded"},
+ {file = "tree_sitter_html-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bdaa7ac5030d416aea0c512d4810ef847bbbd62d61e3d213f370b64ce147293"},
+ {file = "tree_sitter_html-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2e9631b66041a4fd792d7f79a0c4128adb3bfc71f3dcb7e1a3eab5dbee77d67"},
+ {file = "tree_sitter_html-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:85095f49f9e57f0ac9087a3e830783352c8447fdda55b1c1139aa47e5eaa0e21"},
+ {file = "tree_sitter_html-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:0f65ed9e877144d0f04ade5644e5b0e88bf98a9e60bce65235c99905623e2f1a"},
+ {file = "tree_sitter_html-0.23.2.tar.gz", hash = "sha256:bc9922defe23144d9146bc1509fcd00d361bf6b3303f9effee6532c6a0296961"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-java"
+version = "0.23.4"
+description = "Java grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "tree_sitter_java-0.23.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:91cf4507a64529737639941e82d901891edb33e594daced2d75de829ac50962f"},
+ {file = "tree_sitter_java-0.23.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3f74e9c2f01b4c511cdf99e5a947642f37e8d621804d65ec7858a321bdcb1ba6"},
+ {file = "tree_sitter_java-0.23.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:874d489bffc8c418a48f899ae75d3b774b999013840b6591f60b882ff03f9aa3"},
+ {file = "tree_sitter_java-0.23.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79107666639d6a531565fbf6f4a00a06df1589c61101b832f5315f1969899184"},
+ {file = "tree_sitter_java-0.23.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ff8aaccedaf484db9be41ddf1c44cdc4805556f0607de29c22300f14db693616"},
+ {file = "tree_sitter_java-0.23.4-cp39-abi3-win_amd64.whl", hash = "sha256:e7477ffd19e0d0121e340fd7320efe028d2ae04784ffe13bec9a189cdeff0744"},
+ {file = "tree_sitter_java-0.23.4-cp39-abi3-win_arm64.whl", hash = "sha256:c7688f1b004bdf390e7c0bf05384485866086675bf24caf4b2781353552b48a1"},
+ {file = "tree_sitter_java-0.23.4.tar.gz", hash = "sha256:611857a92a232143ee20a4eb7cb46d6ff212b2d21cc7e3377b2943282c16a366"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-javascript"
+version = "0.23.1"
+description = "JavaScript grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_javascript-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6bc1055b061c5055ec58f39ee9b2e9efb8e6e0ae970838af74da0afb811f0a"},
+ {file = "tree_sitter_javascript-0.23.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:056dc04fb6b24293f8c5fec43c14e7e16ba2075b3009c643abf8c85edc4c7c3c"},
+ {file = "tree_sitter_javascript-0.23.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a11ca1c0f736da42967586b568dff8a465ee148a986c15ebdc9382806e0ce871"},
+ {file = "tree_sitter_javascript-0.23.1-cp39-abi3-win_amd64.whl", hash = "sha256:041fa22b34250ea6eb313d33104d5303f79504cb259d374d691e38bbdc49145b"},
+ {file = "tree_sitter_javascript-0.23.1-cp39-abi3-win_arm64.whl", hash = "sha256:eb28130cd2fb30d702d614cbf61ef44d1c7f6869e7d864a9cc17111e370be8f7"},
+ {file = "tree_sitter_javascript-0.23.1.tar.gz", hash = "sha256:b2059ce8b150162cda05a457ca3920450adbf915119c04b8c67b5241cd7fcfed"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-json"
+version = "0.24.8"
+description = "JSON grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_json-0.24.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8627f7d375fda9fc193ebee368c453f374f65c2f25c58b6fea4e6b49a7fccbc"},
+ {file = "tree_sitter_json-0.24.8-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85cca779872f7278f3a74eb38533d34b9c4de4fd548615e3361fa64fe350ad0a"},
+ {file = "tree_sitter_json-0.24.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:deeb45850dcc52990fbb52c80196492a099e3fa3512d928a390a91cf061068cc"},
+ {file = "tree_sitter_json-0.24.8-cp39-abi3-win_amd64.whl", hash = "sha256:e4849a03cd7197267b2688a4506a90a13568a8e0e8588080bd0212fcb38974e3"},
+ {file = "tree_sitter_json-0.24.8-cp39-abi3-win_arm64.whl", hash = "sha256:591e0096c882d12668b88f30d3ca6f85b9db3406910eaaab6afb6b17d65367dd"},
+ {file = "tree_sitter_json-0.24.8.tar.gz", hash = "sha256:ca8486e52e2d261819311d35cf98656123d59008c3b7dcf91e61d2c0c6f3120e"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-markdown"
+version = "0.3.2"
+description = "Markdown grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_markdown-0.3.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3555e5732223b030c8b5742fb565b4528566d96700ea7de9a2902e51fb91be21"},
+ {file = "tree_sitter_markdown-0.3.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7fd68cbbccd917067696952773a553ef4d604017d9332b7a6f6a05549f1c0a3"},
+ {file = "tree_sitter_markdown-0.3.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a693f0251f13fa925631fdc9e30f2435f5569d1b3b3d2c3d3b24060d3234f98a"},
+ {file = "tree_sitter_markdown-0.3.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:507b9d99500dcbeefb069815b689c3dd36892375e878669f98125d0cd0a814a4"},
+ {file = "tree_sitter_markdown-0.3.2-cp39-abi3-win_amd64.whl", hash = "sha256:a89a374920d648599d07036e0ec979f54fde684ddaee1bddf406339c51565cbc"},
+ {file = "tree_sitter_markdown-0.3.2-cp39-abi3-win_arm64.whl", hash = "sha256:017e7c09c44861f35a4499564ecd0d97a25341905dc9d0dec2e6a38ee4e6b52d"},
+ {file = "tree_sitter_markdown-0.3.2.tar.gz", hash = "sha256:64501234ae4ce5429551624e2fd675008cf86824bd8b9352223653e39218e753"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.23,<1.0)"]
+
+[[package]]
+name = "tree-sitter-python"
+version = "0.23.5"
+description = "Python grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "tree_sitter_python-0.23.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:095d104f7f13694ee95ca8540b39a77a57bf9f037797c2658b8400c5b5ece117"},
+ {file = "tree_sitter_python-0.23.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a27f874083b2a204a5c1aa85ebe15e23a441816ee60a6bcf6a4daad7044176ca"},
+ {file = "tree_sitter_python-0.23.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cb5aebdd64a30e3557e481bd0f6d553a1570e088626529fac5a70165a67b84e"},
+ {file = "tree_sitter_python-0.23.5-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c0cbebf127b578e183ce084feb8f0ca7e9e26bcd0a4f6cf1a8f47e13b0b5a"},
+ {file = "tree_sitter_python-0.23.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c62262e96e67f6f63467f74b37c6fa4b618b3d2ddd7ad16280c101d1f9be8a8a"},
+ {file = "tree_sitter_python-0.23.5-cp39-abi3-win_amd64.whl", hash = "sha256:2b52ec8279193b0f8979aef4b0ac60c99e2856ab02eeeb1a62b55a03c012e3fd"},
+ {file = "tree_sitter_python-0.23.5-cp39-abi3-win_arm64.whl", hash = "sha256:efd1a1a44322c46b27ff439e6103d8199d53ffc38c021e7856067c9c90617460"},
+ {file = "tree_sitter_python-0.23.5.tar.gz", hash = "sha256:bd18325d93d633b4d411f24bb5e7d34ee653cd3254e5963fb3c2738ee3c4a1ee"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-regex"
+version = "0.24.3"
+description = "Regex grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_regex-0.24.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6abbf0708dbef6d70444bf9482528b39bae255ce59ed147dd1a731127e49a8da"},
+ {file = "tree_sitter_regex-0.24.3-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdb7134e1c954a18c321053f753da1c5aea9dc6d92e814796d34d03c4b76c012"},
+ {file = "tree_sitter_regex-0.24.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bf21ff69b8356d83b19ece6468ffe855d397eeeee1e34e8a11de0dc2be5ee896"},
+ {file = "tree_sitter_regex-0.24.3-cp39-abi3-win_amd64.whl", hash = "sha256:7cb8f173054859a3d8b8f111833c638b1f1fef878fafb191e6974bbcaf5e930f"},
+ {file = "tree_sitter_regex-0.24.3-cp39-abi3-win_arm64.whl", hash = "sha256:2eb9001e9ccb97d3d608e07f524335b0e5614abf67a004004c6c90abf0feb7cf"},
+ {file = "tree_sitter_regex-0.24.3.tar.gz", hash = "sha256:58bb63f9e0ff01430da56ff158bddcb1b62a31f115abdf93cc6af76cc3aff86e"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-rust"
+version = "0.23.2"
+description = "Rust grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_rust-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deced590a85ce848cda56f33728bad93b95827c1e3c736b707b24fb4280b3788"},
+ {file = "tree_sitter_rust-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:540cf826932fe7cfd800361e368617e138c3d7914fad3b90786b7505af216be6"},
+ {file = "tree_sitter_rust-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:880be40b220e87105b60db48c57cdd8019b5039b324afb1d643fa9c2fc187873"},
+ {file = "tree_sitter_rust-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:d8e0bea4fd76fc8b325247f3d1bb3dc2707db7dd3818b02c251efdea1b47909c"},
+ {file = "tree_sitter_rust-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:3ea49daa887ad59230758e7a96432193af4a2c7183781e1e85c35d4f8cb30b6b"},
+ {file = "tree_sitter_rust-0.23.2.tar.gz", hash = "sha256:9088a0e0342d3de2749088811f5561994423cb10dab5ad3251003dffaa0a1bd1"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-sql"
+version = "0.3.7"
+description = "Tree-sitter Grammar for SQL"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_sql-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa7beae1c2e841edc6de5a80d3ee6d401b579d9d27ce9553f2c569bef4b95b8f"},
+ {file = "tree_sitter_sql-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5fc2daa8fc8d49265327ddaff0b5bbda5a6a0e37d05b157fdbcb2530f1e96e8"},
+ {file = "tree_sitter_sql-0.3.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5f3c34121b625ee8f43e6ccffaca5205b206afa31592aedd1f078b4f6f4cb321"},
+ {file = "tree_sitter_sql-0.3.7-cp38-abi3-win_amd64.whl", hash = "sha256:09e4af2b4c32b09e602c83f1e584e5f390901fce929fa7c3da2ddf416e30681e"},
+ {file = "tree_sitter_sql-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:e7b09235e5492ac8f71abaeb078e28dbfa94881998a5fbf22c659da49165054c"},
+ {file = "tree_sitter_sql-0.3.7.tar.gz", hash = "sha256:5eb671ad597e6245d96aa44fd584c990d3eaffe80faddf941bfe8ebee6a8e2dd"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-toml"
+version = "0.7.0"
+description = "TOML grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_toml-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a045e0acfcf91b7065066f7e51ea038ed7385c1e35e7e8fae18f252d3f8adb8c"},
+ {file = "tree_sitter_toml-0.7.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a2f8cf9d73f07b6628093b35e5c5fbac039247e32cb075eaa5289a5914e73af"},
+ {file = "tree_sitter_toml-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:860ffa4513b2dc3083d8e412bd815a350b0a9490624b37e7c8f6ed5c6f9ce63c"},
+ {file = "tree_sitter_toml-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:2760a04f06937b01b1562a2135cd7e8207e399e73ef75bbebc77e37b1ad3b15d"},
+ {file = "tree_sitter_toml-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:fd00fd8a51c65aa19c40539431cb1773d87c30af5757b4041fa6c229058420b4"},
+ {file = "tree_sitter_toml-0.7.0.tar.gz", hash = "sha256:29e257612fa8f0c1fcbc4e7e08ddc561169f1725265302e64d81086354144a70"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-xml"
+version = "0.7.0"
+description = "XML & DTD grammars for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_xml-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c0fe5f2d6cc09974c8375c8ea9b24909f493b5bf04aacdc4c694b5d2ae6b040"},
+ {file = "tree_sitter_xml-0.7.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd3209516a4d84dff90bc91d2ad2ce246de8504cede4358849687fa8e71536e7"},
+ {file = "tree_sitter_xml-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87578e15fa55f44ecd9f331233b6f8a2cbde3546b354c830ecb862a632379455"},
+ {file = "tree_sitter_xml-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:9ba2dafc6ce9feaf4ccc617d3aeea57f8e0ca05edad34953e788001ebff79133"},
+ {file = "tree_sitter_xml-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:fc759f710a8fd7a01c23e2d7cb013679199045bea3dc0e5151650a11322aaf40"},
+ {file = "tree_sitter_xml-0.7.0.tar.gz", hash = "sha256:ab0ff396f20230ad8483d968151ce0c35abe193eb023b20fbd8b8ce4cf9e9f61"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "tree-sitter-yaml"
+version = "0.7.0"
+description = "YAML grammar for tree-sitter"
+optional = false
+python-versions = ">=3.9"
+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"},
+ {file = "tree_sitter_yaml-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cd1725142f19e41c51d27c99cfc60780f596e069eb181cfa6433d993a19aa3d"},
+ {file = "tree_sitter_yaml-0.7.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d1b268378254f75bb27396d83c96d886ccbfcda6bd8c2778e94e3e1d2459085"},
+ {file = "tree_sitter_yaml-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:27c2e7f4f49ddf410003abbb82a7b00ec77ea263d8ef08dbce1a15d293eed2fd"},
+ {file = "tree_sitter_yaml-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:98dce0d6bc376f842cfb1d3c32512eea95b37e61cd2c87074bb4b05c999917c8"},
+ {file = "tree_sitter_yaml-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:f0f8d8e05fa8e70f08d0f18a209d6026e171844f4ea7090e7c779b9c375b3a31"},
+ {file = "tree_sitter_yaml-0.7.0.tar.gz", hash = "sha256:9c8bb17d9755c3b0e757260917240c0d19883cd3b59a5d74f205baa8bf8435a4"},
+]
+
+[package.extras]
+core = ["tree-sitter (>=0.22,<1.0)"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+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"},
+]
+
+[[package]]
+name = "uc-micro-py"
+version = "1.0.3"
+description = "Micro subset of unicode data files for linkify-it-py projects."
+optional = false
+python-versions = ">=3.7"
+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"},
+]
+
+[package.extras]
+test = ["coverage", "pytest", "pytest-cov"]
+
+[[package]]
+name = "virtualenv"
+version = "20.28.0"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"},
+ {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+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)"]
+
+[[package]]
+name = "yarl"
+version = "1.18.3"
+description = "Yet another URL library"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"},
+ {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"},
+ {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"},
+ {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"},
+ {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"},
+ {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"},
+ {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"},
+ {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"},
+ {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"},
+ {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"},
+ {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"},
+ {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"},
+ {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"},
+ {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"},
+ {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"},
+ {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"},
+ {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"},
+ {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"},
+ {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"},
+ {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"},
+ {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"},
+ {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"},
+ {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"},
+ {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"},
+ {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"},
+ {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"},
+ {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"},
+ {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"},
+ {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"},
+ {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"},
+ {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"},
+ {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"},
+ {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"},
+ {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"},
+ {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"},
+ {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"},
+ {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"},
+ {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"},
+ {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"},
+ {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"},
+ {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"},
+ {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"},
+ {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"},
+ {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"},
+ {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"},
+ {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"},
+ {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"},
+ {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"},
+ {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"},
+ {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"},
+ {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"},
+ {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"},
+ {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"},
+ {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"},
+ {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"},
+ {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"},
+ {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"},
+]
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+propcache = ">=0.2.0"
+
+[metadata]
+lock-version = "2.0"
+python-versions = ">=3.9,<3.14"
+content-hash = "7e3f85218393ee605e36a620bb089ff426faaf3d3044d94573c0f6cb7c98e7e6"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..f7ec345
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,67 @@
+[tool.poetry]
+name = "textual-textarea"
+version = "0.15.0"
+description = "A text area (multi-line input) with syntax highlighting for Textual"
+authors = ["Ted Conbeer "]
+license = "MIT"
+homepage = "https://github.com/tconbeer/textual-textarea"
+repository = "https://github.com/tconbeer/textual-textarea"
+readme = "README.md"
+packages = [{ include = "textual_textarea", from = "src" }]
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry.dependencies]
+python = ">=3.9,<3.14"
+textual = { version = ">=0.89.1,<2.0", extras = ["syntax"] }
+pyperclip = "^1.9.0"
+
+[tool.poetry.group.dev.dependencies]
+pre-commit = "^3.3.1"
+textual = "0.89.1"
+textual-dev = "^1.2.1"
+pyinstrument = "^5"
+
+[tool.poetry.group.static.dependencies]
+ruff = "^0.5"
+mypy = "^1.10.0"
+
+[tool.poetry.group.test.dependencies]
+pytest = ">=7.3.1,<9.0.0"
+pytest-asyncio = "^0.21"
+
+[tool.ruff.lint]
+select = ["A", "B", "E", "F", "I"]
+
+[tool.mypy]
+python_version = "3.9"
+files = [
+ "src/textual_textarea/**/*.py",
+ "tests/**/*.py",
+]
+mypy_path = "src:stubs"
+
+show_column_numbers = true
+
+# show error messages from unrelated files
+follow_imports = "normal"
+
+# be strict
+disallow_untyped_calls = true
+disallow_untyped_defs = true
+check_untyped_defs = true
+disallow_untyped_decorators = true
+disallow_incomplete_defs = true
+disallow_subclassing_any = true
+strict_optional = true
+
+warn_return_any = true
+warn_no_return = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+warn_unused_configs = true
+
+no_implicit_reexport = true
+strict_equality = true
\ No newline at end of file
diff --git a/src/scripts/__init__.py b/src/scripts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/scripts/profile_startup.py b/src/scripts/profile_startup.py
new file mode 100644
index 0000000..f35ec32
--- /dev/null
+++ b/src/scripts/profile_startup.py
@@ -0,0 +1,23 @@
+from textual.app import App, ComposeResult
+from textual_textarea import TextEditor
+
+
+class TextApp(App, inherit_bindings=False):
+ def compose(self) -> ComposeResult:
+ self.ta = TextEditor(
+ text="class TextApp(App):",
+ language="python",
+ theme="monokai",
+ use_system_clipboard=True,
+ id="ta",
+ )
+ yield self.ta
+
+ def on_mount(self) -> None:
+ self.ta.focus()
+ self.exit()
+
+
+if __name__ == "__main__":
+ app = TextApp()
+ app.run()
diff --git a/src/scripts/sample_code.py b/src/scripts/sample_code.py
new file mode 100644
index 0000000..22c5d5a
--- /dev/null
+++ b/src/scripts/sample_code.py
@@ -0,0 +1,20 @@
+from textual.app import App, ComposeResult
+from textual_textarea import TextEditor
+
+
+class TextApp(App, inherit_bindings=False):
+ def compose(self) -> ComposeResult:
+ self.editor = TextEditor(
+ language="python",
+ theme="monokai",
+ use_system_clipboard=True,
+ )
+ yield self.editor
+
+ def on_mount(self) -> None:
+ self.editor.focus()
+
+
+if __name__ == "__main__":
+ app = TextApp()
+ app.run()
diff --git a/src/scripts/screenshot.py b/src/scripts/screenshot.py
new file mode 100644
index 0000000..feb30ce
--- /dev/null
+++ b/src/scripts/screenshot.py
@@ -0,0 +1,32 @@
+import asyncio
+from pathlib import Path
+
+from textual.app import App, ComposeResult
+from textual.widgets.text_area import Selection
+from textual_textarea import TextEditor
+
+contents = (Path(__file__).parent / "sample_code.py").open("r").read()
+
+
+class TextApp(App, inherit_bindings=False):
+ def compose(self) -> ComposeResult:
+ self.editor = TextEditor(
+ language="python",
+ theme="monokai",
+ use_system_clipboard=True,
+ )
+ yield self.editor
+
+ def on_mount(self) -> None:
+ self.editor.focus()
+
+
+async def take_screenshot() -> None:
+ app = TextApp()
+ async with app.run_test(size=(80, 24)):
+ app.editor.text = contents
+ app.editor.selection = Selection((7, 12), (8, 12))
+ app.save_screenshot("textarea.svg")
+
+
+asyncio.run(take_screenshot())
diff --git a/src/textual_textarea/__init__.py b/src/textual_textarea/__init__.py
new file mode 100644
index 0000000..14582bb
--- /dev/null
+++ b/src/textual_textarea/__init__.py
@@ -0,0 +1,15 @@
+from textual_textarea.messages import (
+ TextAreaClipboardError,
+ TextAreaSaved,
+ TextAreaThemeError,
+)
+from textual_textarea.path_input import PathInput
+from textual_textarea.text_editor import TextEditor
+
+__all__ = [
+ "TextEditor",
+ "PathInput",
+ "TextAreaClipboardError",
+ "TextAreaThemeError",
+ "TextAreaSaved",
+]
diff --git a/src/textual_textarea/__main__.py b/src/textual_textarea/__main__.py
new file mode 100644
index 0000000..2b67f7e
--- /dev/null
+++ b/src/textual_textarea/__main__.py
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+import sys
+
+from textual.app import App, ComposeResult
+from textual.widgets import Footer, Placeholder
+
+from textual_textarea import TextEditor
+
+
+class FocusablePlaceholder(Placeholder, can_focus=True):
+ pass
+
+
+class TextApp(App, inherit_bindings=False):
+ BINDINGS = [("ctrl+q", "quit")]
+ CSS = """
+ TextEditor {
+ height: 1fr;
+ }
+ Placeholder {
+ height: 0fr;
+ }
+ """
+
+ def compose(self) -> ComposeResult:
+ try:
+ language = sys.argv[1]
+ except IndexError:
+ language = "sql"
+ yield FocusablePlaceholder()
+ self.editor = TextEditor(
+ language=language,
+ use_system_clipboard=True,
+ id="ta",
+ )
+ yield self.editor
+ yield Footer()
+
+ def watch_theme(self, theme: str) -> None:
+ self.editor.theme = theme
+
+ def on_mount(self) -> None:
+ self.theme = "gruvbox"
+ self.editor.focus()
+
+ def _completer(prefix: str) -> list[tuple[tuple[str, str], str]]:
+ words = [
+ "satisfy",
+ "season",
+ "second",
+ "seldom",
+ "select",
+ "self",
+ "separate",
+ "set",
+ "space",
+ "super",
+ "supercalifragilisticexpialadocioussupercalifragilisticexpialadocious",
+ ]
+ return [((w, "word"), w) for w in words if w.startswith(prefix)]
+
+ self.editor.word_completer = _completer
+
+
+app = TextApp()
+app.run()
diff --git a/src/textual_textarea/autocomplete.py b/src/textual_textarea/autocomplete.py
new file mode 100644
index 0000000..51fa81d
--- /dev/null
+++ b/src/textual_textarea/autocomplete.py
@@ -0,0 +1,289 @@
+from __future__ import annotations
+
+from typing import Callable
+
+from rich.console import RenderableType
+from rich.style import Style
+from rich.text import Text
+from textual import on, work
+from textual.css.scalar import Scalar, ScalarOffset, Unit
+from textual.events import Key, Resize
+from textual.geometry import Size
+from textual.message import Message
+from textual.reactive import Reactive, reactive
+from textual.widget import Widget
+from textual.widgets import OptionList
+from textual.widgets._option_list import NewOptionListContent
+from textual.widgets.option_list import Option
+
+from textual_textarea.messages import TextAreaHideCompletionList
+
+
+class Completion(Option):
+ def __init__(
+ self,
+ prompt: RenderableType,
+ id: str | None = None, # noqa: A002
+ disabled: bool = False,
+ value: str | None = None,
+ ) -> None:
+ super().__init__(prompt, id, disabled)
+ self.value = value
+
+
+class CompletionList(OptionList, can_focus=False, inherit_bindings=False):
+ COMPONENT_CLASSES = {
+ "completion-list--type-label",
+ "completion-list--type-label-highlighted",
+ }
+ DEFAULT_CSS = """
+ CompletionList {
+ layer: overlay;
+ padding: 0;
+ border: none;
+ width: 40;
+ max-height: 8;
+ display: none;
+ }
+ CompletionList.open {
+ display: block;
+ }
+ CompletionList .completion-list--type-label {
+ color: $foreground-muted;
+ background: transparent;
+ }
+ """
+
+ class CompletionsReady(Message, bubble=False):
+ def __init__(
+ self,
+ prefix: str,
+ items: list[tuple[str, str]] | list[tuple[tuple[str, str], str]],
+ ) -> None:
+ super().__init__()
+ self.items = items
+ self.prefix = prefix
+
+ INNER_CONTENT_WIDTH = 37 # should be 3 less than width for scroll bar.
+ is_open: Reactive[bool] = reactive(False)
+ cursor_offset: tuple[int, int] = (0, 0)
+ additional_x_offset: int = 0
+
+ def __init__(
+ self,
+ *content: NewOptionListContent,
+ name: str | None = None,
+ id: str | None = None, # noqa: A002
+ classes: str | None = None,
+ disabled: bool = False,
+ ):
+ super().__init__(
+ *content, name=name, id=id, classes=classes, disabled=disabled, wrap=False
+ )
+
+ def set_offset(self, x_offset: int, y_offset: int) -> None:
+ """The CSS Offset of this widget from its parent."""
+ self.styles.offset = ScalarOffset.from_offset(
+ (
+ x_offset,
+ y_offset,
+ )
+ )
+
+ @property
+ def x_offset(self) -> int:
+ """The x-coord of the CSS Offset of this widget from its parent."""
+ return int(self.styles.offset.x.value)
+
+ @property
+ def y_offset(self) -> int:
+ """The y-coord of the CSS Offset of this widget from its parent."""
+ return int(self.styles.offset.y.value)
+
+ @property
+ def parent_height(self) -> int:
+ """
+ The content size height of the parent widget
+ """
+ return self.parent_size.height
+
+ @property
+ def parent_width(self) -> int:
+ """
+ The content size height of the parent widget
+ """
+ return self.parent_size.width
+
+ @property
+ def parent_size(self) -> Size:
+ """
+ The content size of the parent widget
+ """
+ parent = self.parent
+ if isinstance(parent, Widget):
+ return parent.content_size
+ else:
+ return self.screen.content_size
+
+ @on(CompletionsReady)
+ def populate_and_position_list(self, event: CompletionsReady) -> None:
+ event.stop()
+ self.clear_options()
+ type_label_style_full = self.get_component_rich_style(
+ "completion-list--type-label"
+ )
+ type_label_fg_style = Style(color=type_label_style_full.color)
+ prompts = [
+ Text.assemble(item[0][0], " ", (item[0][1], type_label_fg_style))
+ if isinstance(item[0], tuple)
+ else Text.from_markup(item[0])
+ for item in event.items
+ ]
+
+ # if the completions' prompts are wider than the widget,
+ # we have to trunctate them
+ max_length = max(map(lambda x: x.cell_len, prompts))
+ truncate_amount = max(
+ 0,
+ min(
+ max_length - self.INNER_CONTENT_WIDTH,
+ len(event.prefix) - 2,
+ ),
+ )
+ if truncate_amount > 0:
+ additional_x_offset = truncate_amount - 1
+ items = [
+ Completion(prompt=f"…{prompt[truncate_amount:]}", value=item[1])
+ for prompt, item in zip(prompts, event.items)
+ ]
+ else:
+ additional_x_offset = 0
+ items = [
+ Completion(prompt=prompt, value=item[1])
+ for prompt, item in zip(prompts, event.items)
+ ]
+
+ # set x offset if not already open.
+ if not self.is_open:
+ try:
+ x_offset = self._get_x_offset(
+ prefix_length=len(event.prefix),
+ additional_x_offset=additional_x_offset,
+ cursor_x=self.cursor_offset[0],
+ container_width=self.parent_width,
+ width=self._width,
+ )
+ except ValueError:
+ x_offset = 0
+ self.styles.width = self._parent_container_size.width
+ self.set_offset(x_offset, self.y_offset)
+ # adjust x offset if we have to due to truncation
+ elif additional_x_offset != self.additional_x_offset:
+ self.set_offset(
+ min(
+ self.x_offset + (additional_x_offset - self.additional_x_offset),
+ self.parent_width - self._width,
+ ),
+ self.y_offset,
+ )
+
+ self.add_options(items=items)
+ self.action_first()
+ self.additional_x_offset = additional_x_offset
+ self.is_open = True
+
+ def watch_is_open(self, is_open: bool) -> None:
+ if not is_open:
+ self.remove_class("open")
+ self.additional_x_offset = 0
+ return
+
+ self.add_class("open")
+ self.styles.max_height = Scalar(
+ value=8.0, unit=Unit.CELLS, percent_unit=Unit.PERCENT
+ )
+
+ def on_resize(self, event: Resize) -> None:
+ try:
+ y_offset = self._get_y_offset(
+ cursor_y=self.cursor_offset[1],
+ height=event.size.height,
+ container_height=self.parent_height,
+ )
+ except ValueError:
+ if self.styles.max_height is not None and self.styles.max_height.value > 1:
+ self.styles.max_height = Scalar(
+ value=self.styles.max_height.value - 1,
+ unit=self.styles.max_height.unit,
+ percent_unit=self.styles.max_height.percent_unit,
+ )
+ else:
+ self.post_message(TextAreaHideCompletionList())
+ else:
+ self.set_offset(self.x_offset, y_offset)
+
+ @work(thread=True, exclusive=True, group="completers")
+ def show_completions(
+ self,
+ prefix: str,
+ completer: Callable[
+ [str], list[tuple[str, str]] | list[tuple[tuple[str, str], str]]
+ ]
+ | None,
+ ) -> None:
+ matches = completer(prefix) if completer is not None else []
+ if matches:
+ self.post_message(self.CompletionsReady(prefix=prefix, items=matches))
+ else:
+ self.post_message(TextAreaHideCompletionList())
+
+ def process_keypress(self, event: Key) -> None:
+ if event.key in ("tab", "enter", "shift+tab"):
+ self.action_select()
+ elif event.key == "up":
+ self.action_cursor_up()
+ elif event.key == "down":
+ self.action_cursor_down()
+ elif event.key == "pageup":
+ self.action_page_up()
+ elif event.key == "pagedown":
+ self.action_page_down()
+
+ @property
+ def _parent_container_size(self) -> Size:
+ return getattr(self.parent, "container_size", self.screen.container_size)
+
+ @property
+ def _width(self) -> int:
+ if self.styles.width and self.styles.width.unit == Unit.CELLS:
+ return int(self.styles.width.value)
+ else:
+ return self.outer_size.width
+
+ @staticmethod
+ def _get_x_offset(
+ prefix_length: int,
+ additional_x_offset: int,
+ cursor_x: int,
+ container_width: int,
+ width: int,
+ ) -> int:
+ x = cursor_x - prefix_length + additional_x_offset
+ max_x = container_width - width
+ if max_x < 0:
+ raise ValueError("doesn't fit")
+
+ return min(x, max_x)
+
+ @staticmethod
+ def _get_y_offset(cursor_y: int, height: int, container_height: int) -> int:
+ fits_above = height < cursor_y + 1
+ fits_below = height < container_height - cursor_y
+ if fits_below:
+ y = cursor_y + 1
+ elif fits_above:
+ y = cursor_y - height
+ else:
+ raise ValueError("Doesn't fit.")
+
+ return y
diff --git a/src/textual_textarea/cancellable_input.py b/src/textual_textarea/cancellable_input.py
new file mode 100644
index 0000000..ab23b74
--- /dev/null
+++ b/src/textual_textarea/cancellable_input.py
@@ -0,0 +1,19 @@
+from textual.binding import Binding
+from textual.message import Message
+from textual.widgets import Input
+
+
+class CancellableInput(Input):
+ BINDINGS = [
+ Binding("escape", "cancel", "Cancel", show=False),
+ ]
+
+ class Cancelled(Message):
+ """
+ Posted when the user presses Esc to cancel the input.
+ """
+
+ pass
+
+ def action_cancel(self) -> None:
+ self.post_message(self.Cancelled())
diff --git a/src/textual_textarea/colors.py b/src/textual_textarea/colors.py
new file mode 100644
index 0000000..b477492
--- /dev/null
+++ b/src/textual_textarea/colors.py
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+from rich.style import Style
+from textual.color import Color
+from textual.theme import Theme
+from textual.widgets.text_area import TextAreaTheme
+
+
+def text_area_theme_from_app_theme(
+ theme_name: str, theme: Theme, css_vars: dict[str, str]
+) -> TextAreaTheme:
+ builtin = TextAreaTheme.get_builtin_theme(theme_name)
+ if builtin is not None:
+ return builtin
+
+ if "background" in css_vars:
+ background_color = Color.parse(
+ css_vars.get("background", "#000000" if theme.dark else "#FFFFFF")
+ )
+ foreground_color = Color.parse(
+ css_vars.get("foreground", background_color.inverse)
+ )
+ else:
+ foreground_color = Color.parse(
+ css_vars.get("foreground", "#FFFFFF" if theme.dark else "#000000")
+ )
+ background_color = foreground_color.inverse
+
+ muted = background_color.blend(foreground_color, factor=0.5)
+
+ computed_theme = TextAreaTheme(
+ name=theme_name,
+ base_style=Style(
+ color=foreground_color.rich_color, bgcolor=background_color.rich_color
+ ),
+ syntax_styles={
+ "comment": muted.hex, # type: ignore
+ "string": theme.accent, # type: ignore
+ "string.documentation": muted.hex, # type: ignore
+ "string.special": theme.accent, # type: ignore
+ "number": theme.accent, # type: ignore
+ "float": theme.accent, # type: ignore
+ "function": theme.secondary, # type: ignore
+ "function.call": theme.secondary, # type: ignore
+ "method": theme.secondary, # type: ignore
+ "method.call": theme.secondary, # type: ignore
+ "constant": foreground_color.hex, # type: ignore
+ "constant.builtin": foreground_color.hex, # type: ignore
+ "boolean": theme.accent, # type: ignore
+ "class": f"{foreground_color.hex} bold", # type: ignore
+ "type": f"{foreground_color.hex} bold", # type: ignore
+ "variable": foreground_color.hex, # type: ignore
+ "parameter": f"{theme.accent} bold", # type: ignore
+ "operator": theme.secondary, # type: ignore
+ "punctuation.bracket": foreground_color.hex, # type: ignore
+ "punctuation.delimeter": foreground_color.hex, # type: ignore
+ "keyword": f"{theme.primary} bold", # type: ignore
+ "keyword.function": theme.secondary, # type: ignore
+ "keyword.return": theme.primary, # type: ignore
+ "keyword.operator": f"{theme.primary} bold", # type: ignore
+ "exception": theme.error, # type: ignore
+ "heading": theme.primary, # type: ignore
+ "bold": "bold", # type: ignore
+ "italic": "italic", # type: ignore
+ },
+ )
+ return computed_theme
diff --git a/src/textual_textarea/comments.py b/src/textual_textarea/comments.py
new file mode 100644
index 0000000..9a227ae
--- /dev/null
+++ b/src/textual_textarea/comments.py
@@ -0,0 +1,185 @@
+INLINE_MARKERS = {
+ "abap": '"',
+ "actionscript": "//",
+ "as": "//",
+ "actionscript3": "//",
+ "as3": "//",
+ "ada": "--",
+ "ada95": "--",
+ "ada2005": "--",
+ "antlr-objc": "//",
+ "apl": "⍝",
+ "applescript": "--",
+ "autohotkey": ";",
+ "ahk": ";",
+ "autoit": ";",
+ "basemake": "#",
+ "bash": "#",
+ "sh": "#",
+ "ksh": "#",
+ "zsh": "#",
+ "shell": "#",
+ "batch": "::",
+ "bat": "::",
+ "dosbatch": "::",
+ "winbatch": "::",
+ "bbcbasic": "REM",
+ "blitzbasic": "REM",
+ "b3d": "REM",
+ "bplus": "REM",
+ "boo": "#",
+ "c": "//",
+ "csharp": "//",
+ "c#": "//",
+ "cs": "//",
+ "cpp": "//",
+ "c++": "//",
+ "cbmbas": "REM",
+ "clojure": ";",
+ "clj": ";",
+ "clojurescript": ";",
+ "cljs": ";",
+ "cmake": "#",
+ "cobol": "*>",
+ "cobolfree": "*>",
+ "common-lisp": ";",
+ "cl": ";",
+ "lisp": ";",
+ "d": "//",
+ "delphi": "//",
+ "pas": "//",
+ "pascal": "//",
+ "objectpascal": "//",
+ "eiffel": "--",
+ "elixir": "#",
+ "ex": "#",
+ "exs": "#",
+ "iex": "#",
+ "elm": "--",
+ "emacs-lisp": ";",
+ "elisp": ";",
+ "emacs": ";",
+ "erlang": "%",
+ "erl": "%",
+ "fsharp": "//",
+ "f#": "//",
+ "factor": "!",
+ "fish": "#",
+ "fishshell": "#",
+ "forth": "\\",
+ "fortran": "!",
+ "f90": "!",
+ "fortranfixed": "!",
+ "go": "//",
+ "golang": "//",
+ "haskell": "--",
+ "hs": "--",
+ "inform6": "!",
+ "i6": "!",
+ "i6t": "!",
+ "inform7": "!",
+ "i7": "!",
+ "j": "NB.",
+ "java": "//",
+ "jsp": "//",
+ "javascript": "//",
+ "js": "//",
+ "julia": "#",
+ "jl": "#",
+ "jlcon": "#",
+ "julia-repl": "#",
+ "kotlin": "//",
+ "lua": "--",
+ "make": "#",
+ "makefile": "#",
+ "mf": "#",
+ "bsdmake": "#",
+ "matlab": "%",
+ "matlabsession": "%",
+ "monkey": "'",
+ "mysql": "#",
+ "newlisp": ";",
+ "nimrod": "#",
+ "nim": "#",
+ "objective-c": "//",
+ "objectivec": "//",
+ "obj-c": "//",
+ "objc": "//",
+ "objective-c++": "//",
+ "objectivec++": "//",
+ "obj-c++": "//",
+ "objc++": "//",
+ "perl": "#",
+ "pl": "#",
+ "perl6": "#",
+ "pl6": "#",
+ "raku": "#",
+ "php": "#",
+ "php3": "#",
+ "php4": "#",
+ "php5": "#",
+ "plpgsql": "--",
+ "psql": "--",
+ "postgresql-console": "--",
+ "postgres-console": "--",
+ "postgres-explain": "--",
+ "postgresql": "--",
+ "postgres": "--",
+ "postscript": "%",
+ "postscr": "%",
+ "powershell": "#",
+ "pwsh": "#",
+ "posh": "#",
+ "ps1": "#",
+ "psm1": "#",
+ "pwsh-session": "#",
+ "ps1con": "#",
+ "prolog": "%",
+ "python": "#",
+ "py": "#",
+ "sage": "#",
+ "python3": "#",
+ "py3": "#",
+ "python2": "#",
+ "py2": "#",
+ "py2tb": "#",
+ "pycon": "#",
+ "pytb": "#",
+ "py3tb": "#",
+ "py+ul4": "#",
+ "qbasic": "REM",
+ "basic": "REM",
+ "ragel-ruby": "#",
+ "ragel-rb": "#",
+ "rebol": ";",
+ "red": ";",
+ "red/system": ";",
+ "ruby": "#",
+ "rb": "#",
+ "duby": "#",
+ "rbcon": "#",
+ "irb": "#",
+ "rust": "//",
+ "rs": "//",
+ "sass": "//",
+ "scala": "//",
+ "scheme": ";",
+ "scm": ";",
+ "sql": "--",
+ "sql+jinja": "--",
+ "sqlite3": "--",
+ "swift": "//",
+ "tex": "%",
+ "latex": "%",
+ "tsql": "--",
+ "t-sql": "--",
+ "vbscript": "'",
+ "vhdl": "--",
+ "wast": ";;",
+ "wat": ";;",
+ "yaml": "#",
+ "yaml+jinja": "#",
+ "salt": "#",
+ "sls": "#",
+ "zig": "//",
+}
diff --git a/src/textual_textarea/containers.py b/src/textual_textarea/containers.py
new file mode 100644
index 0000000..d8f42c5
--- /dev/null
+++ b/src/textual_textarea/containers.py
@@ -0,0 +1,54 @@
+from typing import Any, Union
+
+from textual.containers import Container, ScrollableContainer
+from textual.widget import Widget
+
+
+class TextContainer(
+ ScrollableContainer,
+ inherit_bindings=False,
+ can_focus=False,
+ can_focus_children=True,
+):
+ DEFAULT_CSS = """
+ TextContainer {
+ height: 1fr;
+ width: 100%;
+ layers: main overlay;
+ }
+ """
+
+ def scroll_to(
+ self, x: Union[float, None] = None, y: Union[float, None] = None, **_: Any
+ ) -> None:
+ return super().scroll_to(x, y, animate=True, duration=0.01)
+
+
+class FooterContainer(
+ Container,
+ inherit_bindings=False,
+ can_focus=False,
+ can_focus_children=True,
+):
+ DEFAULT_CSS = """
+ FooterContainer {
+ dock: bottom;
+ height: auto;
+ width: 100%
+ }
+ FooterContainer.hide {
+ height: 0;
+ }
+ """
+
+ def __init__(
+ self,
+ *children: Widget,
+ name: Union[str, None] = None,
+ id: Union[str, None] = None, # noqa: A002
+ classes: Union[str, None] = None,
+ disabled: bool = False,
+ ) -> None:
+ super().__init__(
+ *children, name=name, id=id, classes=classes, disabled=disabled
+ )
diff --git a/src/textual_textarea/error_modal.py b/src/textual_textarea/error_modal.py
new file mode 100644
index 0000000..8fa4a8f
--- /dev/null
+++ b/src/textual_textarea/error_modal.py
@@ -0,0 +1,75 @@
+from typing import Union
+
+from textual.app import ComposeResult
+from textual.containers import Vertical, VerticalScroll
+from textual.screen import ModalScreen
+from textual.widgets import Static
+
+
+class ErrorModal(ModalScreen):
+ DEFAULT_CSS = """
+ ErrorModal {
+ align: center middle;
+ padding: 0;
+ }
+ #error_modal__outer {
+ border: round $error;
+ background: $background;
+ margin: 5 10;
+ padding: 1 2;
+ max-width: 88;
+ }
+
+ #error_modal__header {
+ dock: top;
+ color: $text-muted;
+ margin: 0 0 1 0;
+ padding: 0 1;
+ }
+
+ #error_modal__inner {
+ border: round $background;
+ padding: 1 1 1 2;
+ }
+
+ #error_modal__info {
+ padding: 0 3 0 0;
+ }
+
+ #error_modal__footer {
+ dock: bottom;
+ color: $text-muted;
+ margin: 1 0 0 0;
+ padding: 0 1;
+ }
+ """
+
+ def __init__(
+ self,
+ title: str,
+ header: str,
+ error: BaseException,
+ name: Union[str, None] = None,
+ id: Union[str, None] = None, # noqa: A002
+ classes: Union[str, None] = None,
+ ) -> None:
+ super().__init__(name, id, classes)
+ self.title = title
+ self.header = header
+ self.error = error
+
+ def compose(self) -> ComposeResult:
+ with Vertical(id="error_modal__outer"):
+ yield Static(self.header, id="error_modal__header")
+ with Vertical(id="error_modal__inner"):
+ with VerticalScroll():
+ yield Static(str(self.error), id="error_modal__info")
+ yield Static("Press any key to continue.", id="error_modal__footer")
+
+ def on_mount(self) -> None:
+ container = self.query_one("#error_modal__outer")
+ container.border_title = self.title
+
+ def on_key(self) -> None:
+ self.app.pop_screen()
+ self.app.action_focus_next()
diff --git a/src/textual_textarea/find_input.py b/src/textual_textarea/find_input.py
new file mode 100644
index 0000000..a5eb306
--- /dev/null
+++ b/src/textual_textarea/find_input.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+from textual import on
+from textual.events import Blur, Key
+from textual.widgets import Input
+
+from textual_textarea.cancellable_input import CancellableInput
+
+
+class FindInput(CancellableInput):
+ def __init__(
+ self,
+ value: str = "",
+ history: list[str] | None = None,
+ classes: str | None = None,
+ ) -> None:
+ super().__init__(
+ value=value,
+ placeholder="Find; enter for next; ESC to close; ↑↓ for history",
+ password=False,
+ type="text",
+ id="textarea__find_input",
+ classes=classes,
+ )
+ self.history: list[str] = [] if history is None else history
+ self.history_index: int | None = None
+
+ @on(Key)
+ def handle_special_keys(self, event: Key) -> None:
+ if event.key not in ("up", "down", "f3"):
+ self.history_index = None
+ return
+ event.stop()
+ event.prevent_default()
+ if event.key == "down":
+ self._handle_down()
+ elif event.key == "up":
+ self._handle_up()
+ elif event.key == "f3":
+ self.post_message(Input.Submitted(self, self.value))
+
+ @on(Blur)
+ def handle_blur(self) -> None:
+ if self.value and (not self.history or self.value != self.history[-1]):
+ self.history.append(self.value)
+
+ def _handle_down(self) -> None:
+ if self.history_index is None:
+ self.checkpoint()
+ self.value = ""
+ elif self.history_index == -1:
+ self.history_index = None
+ self.value = ""
+ else:
+ self.history_index += 1
+ self.value = self.history[self.history_index]
+ self.action_end()
+
+ def checkpoint(self) -> bool:
+ if self.value and (not self.history or self.value != self.history[-1]):
+ self.history.append(self.value)
+ return True
+ return False
+
+ def _handle_up(self) -> None:
+ if not self.history:
+ if self.value:
+ self.history.append(self.value)
+ self.value = ""
+ return
+
+ if self.history_index is None:
+ self.history_index = -1 if self.checkpoint() else 0
+
+ self.history_index = max(-1 * len(self.history), self.history_index - 1)
+ self.value = self.history[self.history_index]
+ self.action_end()
diff --git a/src/textual_textarea/goto_input.py b/src/textual_textarea/goto_input.py
new file mode 100644
index 0000000..42df5cf
--- /dev/null
+++ b/src/textual_textarea/goto_input.py
@@ -0,0 +1,60 @@
+from __future__ import annotations
+
+from textual.validation import ValidationResult, Validator
+
+from textual_textarea.cancellable_input import CancellableInput
+
+
+class GotoLineValidator(Validator):
+ def __init__(
+ self,
+ max_line_number: int,
+ min_line_number: int = 1,
+ failure_description: str = "Not a valid line number.",
+ ) -> None:
+ super().__init__(failure_description)
+ self.max_line_number = max_line_number
+ self.min_line_number = min_line_number
+
+ def validate(self, value: str) -> ValidationResult:
+ try:
+ lno = int(value)
+ except (ValueError, TypeError):
+ return self.failure("Not a valid line number.")
+
+ if lno < self.min_line_number:
+ return self.failure(f"Line number must be >= {self.min_line_number}")
+ elif lno > self.max_line_number:
+ return self.failure(f"Line number must be <= {self.max_line_number}")
+
+ return self.success()
+
+
+class GotoLineInput(CancellableInput):
+ def __init__(
+ self,
+ *,
+ max_line_number: int,
+ id: str | None = None, # noqa: A002
+ classes: str | None = None,
+ current_line: int | None = None,
+ min_line_number: int = 1,
+ ) -> None:
+ current_line_text = (
+ f"Current line: {current_line}. " if current_line is not None else ""
+ )
+ range_text = (
+ f"Enter a line number between {min_line_number} and " f"{max_line_number}."
+ )
+ placeholder = f"{current_line_text}{range_text} ESC to cancel."
+ super().__init__(
+ "",
+ placeholder=placeholder,
+ type="integer",
+ validators=GotoLineValidator(
+ max_line_number=max_line_number, min_line_number=min_line_number
+ ),
+ validate_on={"changed"},
+ id=id,
+ classes=classes,
+ )
diff --git a/src/textual_textarea/messages.py b/src/textual_textarea/messages.py
new file mode 100644
index 0000000..f6a4826
--- /dev/null
+++ b/src/textual_textarea/messages.py
@@ -0,0 +1,38 @@
+from pathlib import Path
+from typing import Union
+
+from textual.message import Message
+
+
+class TextAreaClipboardError(Message, bubble=True):
+ """
+ Posted when textarea cannot access the system clipboard
+ """
+
+ def __init__(self, action: str) -> None:
+ super().__init__()
+ self.action = action
+
+
+class TextAreaThemeError(Message, bubble=True):
+ """
+ Posted when textarea cannot instantiate a theme
+ """
+
+ def __init__(self, theme: str) -> None:
+ super().__init__()
+ self.theme = theme
+
+
+class TextAreaSaved(Message, bubble=True):
+ """
+ Posted when the textarea saved a file successfully.
+ """
+
+ def __init__(self, path: Union[Path, str]) -> None:
+ self.path = str(path)
+ super().__init__()
+
+
+class TextAreaHideCompletionList(Message):
+ pass
diff --git a/src/textual_textarea/path_input.py b/src/textual_textarea/path_input.py
new file mode 100644
index 0000000..4958af3
--- /dev/null
+++ b/src/textual_textarea/path_input.py
@@ -0,0 +1,127 @@
+from __future__ import annotations
+
+import stat
+from pathlib import Path
+
+from rich.highlighter import Highlighter
+from textual.binding import Binding
+from textual.suggester import Suggester
+from textual.validation import ValidationResult, Validator
+
+from textual_textarea.cancellable_input import CancellableInput
+
+
+def path_completer(prefix: str) -> list[tuple[str, str]]:
+ try:
+ original = Path(prefix)
+ p = original.expanduser()
+ if p.is_dir():
+ matches = list(p.iterdir())
+ else:
+ matches = list(p.parent.glob(f"{p.name}*"))
+ if original != p and original.parts and original.parts[0] == "~":
+ prompts = [str(Path("~") / m.relative_to(Path.home())) for m in matches]
+ elif not original.is_absolute() and prefix.startswith("./"):
+ prompts = [f"./{m}" for m in matches]
+ else:
+ prompts = [str(m) for m in matches]
+ return [(p, p) for p in prompts]
+ except Exception:
+ return []
+
+
+class PathSuggester(Suggester):
+ def __init__(self) -> None:
+ super().__init__(use_cache=True, case_sensitive=True)
+
+ async def get_suggestion(self, value: str) -> str | None:
+ matches = path_completer(value)
+ if len(matches) == 1:
+ return str(matches[0][0])
+ else:
+ return None
+
+
+class PathValidator(Validator):
+ def __init__(
+ self,
+ dir_okay: bool,
+ file_okay: bool,
+ must_exist: bool,
+ failure_description: str = "Not a valid path.",
+ ) -> None:
+ self.dir_okay = dir_okay
+ self.file_okay = file_okay
+ self.must_exist = must_exist
+ super().__init__(failure_description)
+
+ def validate(self, value: str) -> ValidationResult:
+ if self.dir_okay and self.file_okay and not self.must_exist:
+ return self.success()
+ try:
+ p = Path(value).expanduser().resolve()
+ except Exception:
+ return self.failure("Not a valid path.")
+
+ try:
+ st = p.stat()
+ except FileNotFoundError:
+ if self.must_exist:
+ return self.failure("File or directory does not exist.")
+ return self.success()
+
+ if not self.dir_okay and stat.S_ISDIR(st.st_mode):
+ return self.failure("Path cannot be a directory.")
+ elif not self.file_okay and stat.S_ISREG(st.st_mode):
+ return self.failure("Path cannot be a regular file.")
+
+ return self.success()
+
+
+class PathInput(CancellableInput):
+ BINDINGS = [
+ Binding("tab", "complete", "Accept Completion", show=False),
+ ]
+
+ def __init__(
+ self,
+ value: str | None = None,
+ placeholder: str = "",
+ highlighter: Highlighter | None = None,
+ password: bool = False,
+ *,
+ name: str | None = None,
+ id: str | None = None, # noqa: A002
+ classes: str | None = None,
+ disabled: bool = False,
+ dir_okay: bool = True,
+ file_okay: bool = True,
+ must_exist: bool = False,
+ tab_advances_focus: bool = False,
+ ) -> None:
+ self.tab_advances_focus = tab_advances_focus
+ super().__init__(
+ value,
+ placeholder,
+ highlighter,
+ password,
+ suggester=PathSuggester(),
+ validators=PathValidator(dir_okay, file_okay, must_exist),
+ name=name,
+ id=id,
+ classes=classes,
+ disabled=disabled,
+ )
+
+ def action_complete(self) -> None:
+ if self._suggestion and self._suggestion != self.value:
+ self.action_cursor_right()
+ elif self.tab_advances_focus:
+ self.app.action_focus_next()
+
+ def _toggle_cursor(self) -> None:
+ """Toggle visibility of cursor."""
+ if self.app.is_headless:
+ self._cursor_visible = True
+ else:
+ self._cursor_visible = not self._cursor_visible
diff --git a/src/textual_textarea/py.typed b/src/textual_textarea/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/src/textual_textarea/text_editor.py b/src/textual_textarea/text_editor.py
new file mode 100644
index 0000000..335a6e9
--- /dev/null
+++ b/src/textual_textarea/text_editor.py
@@ -0,0 +1,1465 @@
+from __future__ import annotations
+
+import re
+from contextlib import suppress
+from math import ceil, floor
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence
+
+import pyperclip
+from rich.console import RenderableType
+from textual import events, on, work
+from textual._cells import cell_len
+from textual._node_list import DuplicateIds
+from textual.app import ComposeResult
+from textual.binding import Binding
+from textual.events import Paste
+from textual.message import Message
+from textual.reactive import reactive
+from textual.timer import Timer
+from textual.widget import Widget
+from textual.widgets import Input, Label, OptionList, TextArea
+from textual.widgets.text_area import Location, Selection, SyntaxAwareDocument
+
+from textual_textarea.autocomplete import CompletionList
+from textual_textarea.cancellable_input import CancellableInput
+from textual_textarea.colors import text_area_theme_from_app_theme
+from textual_textarea.comments import INLINE_MARKERS
+from textual_textarea.containers import FooterContainer, TextContainer
+from textual_textarea.error_modal import ErrorModal
+from textual_textarea.find_input import FindInput
+from textual_textarea.goto_input import GotoLineInput
+from textual_textarea.messages import (
+ TextAreaClipboardError,
+ TextAreaHideCompletionList,
+ TextAreaSaved,
+ TextAreaThemeError,
+)
+from textual_textarea.path_input import PathInput, path_completer
+
+if TYPE_CHECKING:
+ from tree_sitter import Node, Parser, Query, Tree
+
+BRACKETS = {
+ "(": ")",
+ "[": "]",
+ "{": "}",
+}
+CLOSERS = {'"': '"', "'": "'", **BRACKETS}
+
+# these patterns need to match a reversed string!
+DOUBLE_QUOTED_EXPR = r'"([^"\\]*(\\.[^"\\]*|""[^"\\]*)*)"(b?r|f|b|rb|&?u|@)?'
+SINGLE_QUOTED_EXPR = r"'([^'\\]*(\\.[^'\\]*|''[^'\\]*)*)'(b?r|f|b|rb|&?u|x)?"
+BACKTICK_EXPR = r"`([^`\\]*(\\.[^`\\]*)*)`"
+PATH_PROG = re.compile(r"[^\"\'\s]+")
+MEMBER_PROG = re.compile(
+ rf"\w*(`|'|\")?(\.|::?)(\w+|{SINGLE_QUOTED_EXPR}|{DOUBLE_QUOTED_EXPR}|{BACKTICK_EXPR})",
+ flags=re.IGNORECASE,
+)
+WORD_PROG = re.compile(r"\w+")
+NON_WORD_CHAR_PROG = re.compile(r"\W")
+
+
+class TextAreaPlus(TextArea, inherit_bindings=False):
+ DEFAULT_CSS = """
+ TextAreaPlus {
+ width: 1fr;
+ height: 1fr;
+ border: none;
+ layer: main;
+
+ &:focus {
+ border: none;
+ }
+ }
+ """
+ BINDINGS = [
+ # Cursor movement
+ Binding("up", "cursor_up", "cursor up", show=False),
+ Binding("down", "cursor_down", "cursor down", show=False),
+ Binding("left", "cursor_left", "cursor left", show=False),
+ Binding("right", "cursor_right", "cursor right", show=False),
+ Binding("ctrl+left", "cursor_word_left", "cursor word left", show=False),
+ Binding("ctrl+right", "cursor_word_right", "cursor word right", show=False),
+ Binding("home", "cursor_line_start", "cursor line start", show=False),
+ Binding("end", "cursor_line_end", "cursor line end", show=False),
+ Binding("ctrl+home", "cursor_doc_start", "cursor doc start", show=False),
+ Binding("ctrl+end", "cursor_doc_end", "cursor doc end", show=False),
+ Binding("pageup", "cursor_page_up", "cursor page up", show=False),
+ Binding("pagedown", "cursor_page_down", "cursor page down", show=False),
+ # scrolling
+ Binding("ctrl+up", "scroll_one('up')", "scroll one up", show=False),
+ Binding("ctrl+down", "scroll_one('down')", "scroll one down", show=False),
+ # Making selections (generally holding the shift key and moving cursor)
+ Binding(
+ "ctrl+shift+left",
+ "cursor_word_left(True)",
+ "cursor left word select",
+ show=False,
+ ),
+ Binding(
+ "ctrl+shift+right",
+ "cursor_word_right(True)",
+ "cursor right word select",
+ show=False,
+ ),
+ Binding(
+ "shift+home",
+ "cursor_line_start(True)",
+ "cursor line start select",
+ show=False,
+ ),
+ Binding(
+ "shift+end", "cursor_line_end(True)", "cursor line end select", show=False
+ ),
+ Binding(
+ "ctrl+shift+home",
+ "cursor_doc_start(True)",
+ "select to cursor doc start",
+ show=False,
+ ),
+ Binding(
+ "ctrl+shift+end",
+ "cursor_doc_end(True)",
+ "select to cursor doc end",
+ show=False,
+ ),
+ Binding("shift+up", "cursor_up(True)", "cursor up select", show=False),
+ Binding("shift+down", "cursor_down(True)", "cursor down select", show=False),
+ Binding("shift+left", "cursor_left(True)", "cursor left select", show=False),
+ Binding("shift+right", "cursor_right(True)", "cursor right select", show=False),
+ # Binding("f5", "select_word", "select word", show=False),
+ # Binding("f6", "select_line", "select line", show=False),
+ Binding("ctrl+a", "select_all", "select all", show=False),
+ # Editing
+ Binding("ctrl+underscore", "toggle_comment", "toggle comment", show=False),
+ Binding("ctrl+x", "cut", "copy", show=False),
+ Binding("ctrl+c", "copy", "copy", show=False),
+ Binding("ctrl+u,ctrl+v,shift+insert", "paste", "paste", show=False),
+ Binding("ctrl+z", "undo", "undo", show=False),
+ Binding("ctrl+y", "redo", "redo", show=False),
+ # Deletion
+ Binding("backspace", "delete_left", "delete left", show=False),
+ Binding("delete", "delete_right", "delete right", show=False),
+ Binding("shift+delete", "delete_line", "delete line", show=False),
+ # Binding(
+ # "ctrl+w", "delete_word_left", "delete left to start of word", show=False
+ # ),
+ # Binding(
+ # "ctrl+f", "delete_word_right", "delete right to start of word", show=False
+ # ),
+ # Binding(
+ # "ctrl+u", "delete_to_start_of_line", "delete to line start", show=False
+ # ),
+ # Binding("ctrl+k", "delete_to_end_of_line", "delete to line end", show=False),
+ ]
+
+ clipboard: str = ""
+ completer_active: Literal["path", "member", "word"] | None = None
+
+ class ShowCompletionList(Message):
+ def __init__(self, prefix: str) -> None:
+ super().__init__()
+ self.prefix = prefix
+
+ def __repr__(self) -> str:
+ return f"ShowCompletionList({self.prefix=})"
+
+ def __str__(self) -> str:
+ return f"ShowCompletionList({self.prefix=})"
+
+ class CompletionListKey(Message):
+ def __init__(self, key: events.Key) -> None:
+ super().__init__()
+ self.key = key
+
+ class ClipboardReady(Message):
+ def __init__(
+ self, copy: Callable[[Any], None], paste: Callable[[], str]
+ ) -> None:
+ super().__init__()
+ self.copy = copy
+ self.paste = paste
+
+ def __init__(
+ self,
+ text: str = "",
+ *,
+ language: str | None = None,
+ theme: str = "css",
+ use_system_clipboard: bool = True,
+ read_only: bool = False,
+ name: str | None = None,
+ id: str | None = None, # noqa: A002
+ classes: str | None = None,
+ disabled: bool = False,
+ ) -> None:
+ super().__init__(
+ text,
+ language=language,
+ theme=theme,
+ name=name,
+ id=id,
+ classes=classes,
+ disabled=disabled,
+ soft_wrap=False,
+ tab_behavior="indent",
+ show_line_numbers=True,
+ read_only=read_only,
+ )
+ self.cursor_blink = False if self.app.is_headless else True
+ self.use_system_clipboard = use_system_clipboard
+ self.double_click_location: Location | None = None
+ self.double_click_timer: Timer | None = None
+ self.consecutive_clicks: int = 0
+ self.system_copy: Callable[[Any], None] | None = None
+ self.system_paste: Callable[[], str] | None = None
+
+ def on_mount(self) -> None:
+ self._determine_clipboard()
+ self.history.checkpoint()
+
+ def on_blur(self, event: events.Blur) -> None:
+ self.post_message(TextAreaHideCompletionList())
+
+ def on_key(self, event: events.Key) -> None:
+ # Naked shift or ctrl keys on Windows get sent as NUL chars; Textual
+ # interprets these as `ctrl+@` presses, which is inconsistent with
+ # other platforms. We ignore these presses.
+ # https://github.com/Textualize/textual/issues/872
+ if event.key == "ctrl+@":
+ event.stop()
+ event.prevent_default()
+ return
+
+ if event.key in (
+ "apostrophe",
+ "quotation_mark",
+ "left_parenthesis",
+ "left_square_bracket",
+ "left_curly_bracket",
+ "right_parenthesis",
+ "right_square_bracket",
+ "right_curly_bracket",
+ ):
+ self._handle_quote_or_bracket(event)
+ elif event.key == "enter":
+ self._handle_enter(event)
+ elif event.key == "tab":
+ self._handle_tab(event)
+ elif event.key == "shift+tab":
+ self._handle_shift_tab(event)
+ elif event.key in ("up", "down", "pageup", "pagedown"):
+ self._handle_up_down(event)
+ elif event.key == "backspace":
+ self._handle_backspace(event)
+ elif event.key in ("slash", "backslash"):
+ self._handle_slash(event)
+ elif event.key in ("full_stop", "colon"):
+ self._handle_separator(event)
+ elif event.key == "escape":
+ self._handle_escape(event)
+ elif event.character and event.is_printable:
+ self._handle_printable_character(event)
+ else:
+ self.post_message(TextAreaHideCompletionList())
+
+ def on_mouse_down(self, event: events.MouseDown) -> None:
+ self.post_message(TextAreaHideCompletionList())
+ target = self.get_target_document_location(event)
+ if (
+ self.double_click_location is not None
+ and self.double_click_location == target
+ ):
+ event.prevent_default()
+ self._selecting = True
+ self.capture_mouse()
+ self._pause_blink(visible=True)
+
+ def on_mouse_up(self, event: events.MouseUp) -> None:
+ target = self.get_target_document_location(event)
+ if (
+ self.consecutive_clicks > 0
+ and self.double_click_location is not None
+ and self.double_click_location == target
+ ):
+ if self.consecutive_clicks == 1:
+ self.action_select_word()
+ elif self.consecutive_clicks == 2:
+ self.action_select_line()
+ self.action_cursor_right(select=True)
+ else:
+ self.action_select_all()
+ self.consecutive_clicks += 1
+ else:
+ self.history.checkpoint()
+ self.double_click_location = target
+ self.consecutive_clicks += 1
+
+ if self.double_click_timer is not None:
+ self.double_click_timer.reset()
+ else:
+ self.double_click_timer = self.set_timer(
+ delay=0.5, callback=self._clear_double_click, name="double_click_timer"
+ )
+
+ def on_paste(self, event: Paste) -> None:
+ event.prevent_default()
+ event.stop()
+ self.post_message(TextAreaHideCompletionList())
+ self.history.checkpoint()
+ self.replace(event.text, *self.selection, maintain_selection_offset=False)
+
+ @on(ClipboardReady)
+ def _set_clipboard(self, message: ClipboardReady) -> None:
+ self.system_copy = message.copy
+ self.system_paste = message.paste
+
+ def watch_language(self, language: str) -> None:
+ self.inline_comment_marker = INLINE_MARKERS.get(language)
+
+ def replace_current_word(self, new_word: str) -> None:
+ current_word = self._get_word_before_cursor()
+ offset = len(current_word)
+ self.replace(
+ new_word,
+ start=(self.cursor_location[0], self.cursor_location[1] - offset),
+ end=self.cursor_location,
+ maintain_selection_offset=False,
+ )
+
+ @work(thread=True)
+ def _determine_clipboard(self) -> None:
+ if self.use_system_clipboard:
+ copy, paste = pyperclip.determine_clipboard()
+ self.post_message(self.ClipboardReady(copy=copy, paste=paste))
+
+ def action_copy(self) -> None:
+ self._copy_selection()
+
+ def action_cut(self) -> None:
+ self.post_message(TextAreaHideCompletionList())
+ self.history.checkpoint()
+ self._copy_selection()
+ if not self.selected_text:
+ self.action_delete_line()
+ self.delete(*self.selection)
+
+ def action_cursor_doc_start(self, select: bool = False) -> None:
+ self.post_message(TextAreaHideCompletionList())
+ if select:
+ self.selection = Selection(start=self.selection.start, end=(0, 0))
+ else:
+ self.selection = Selection(start=(0, 0), end=(0, 0))
+
+ def action_cursor_doc_end(self, select: bool = False) -> None:
+ self.post_message(TextAreaHideCompletionList())
+ if select:
+ self.selection = Selection(
+ start=self.selection.start, end=self.document.end
+ )
+ else:
+ self.selection = Selection(start=self.document.end, end=self.document.end)
+
+ def action_delete_line(self) -> None:
+ self.post_message(TextAreaHideCompletionList())
+ self.history.checkpoint()
+ if self.selection.start != self.cursor_location: # selection active
+ self.delete(*self.selection, maintain_selection_offset=False)
+ else:
+ line, col = self.cursor_location
+ if self.document.line_count == 1:
+ super().action_delete_line()
+ elif self.cursor_at_last_line:
+ eol = len(self.document[line - 1])
+ self.replace(
+ "", start=(line - 1, eol), end=self.get_cursor_line_end_location()
+ )
+ self.cursor_location = (line - 1, eol)
+ else:
+ self.delete(start=(line, 0), end=(line + 1, 0))
+ self.cursor_location = (line, 0)
+
+ def action_paste(self) -> None:
+ self.post_message(TextAreaHideCompletionList())
+ if self.use_system_clipboard and self.system_paste is not None:
+ try:
+ self.clipboard = self.system_paste()
+ except Exception:
+ # no system clipboard; common in CI runners. Use internal
+ # clipboard state of self.clipboard
+ self.post_message(TextAreaClipboardError(action="paste"))
+ if self.clipboard:
+ self.post_message(Paste(self.clipboard))
+
+ def action_select_word(self) -> None:
+ self.post_message(TextAreaHideCompletionList())
+ prev = self._get_character_before_cursor()
+ next_char = self._get_character_at_cursor()
+ at_start_of_word = self._word_pattern.match(prev) is None
+ at_end_of_word = self._word_pattern.match(next_char) is None
+ if at_start_of_word and not at_end_of_word:
+ self.action_cursor_word_right(select=True)
+ elif at_end_of_word and not at_start_of_word:
+ self.action_cursor_word_left(select=True)
+ self.section = Selection(start=self.selection.end, end=self.selection.start)
+ else:
+ self.action_cursor_word_left(select=False)
+ self.action_cursor_word_right(select=True)
+
+ def action_scroll_one(self, direction: str = "down") -> None:
+ self.post_message(TextAreaHideCompletionList())
+ if direction == "down":
+ self.scroll_relative(y=1, animate=False)
+ elif direction == "up":
+ self.scroll_relative(y=-1, animate=False)
+
+ def action_toggle_comment(self) -> None:
+ self.post_message(TextAreaHideCompletionList())
+ if self.inline_comment_marker:
+ self.history.checkpoint()
+ lines, first, last = self._get_selected_lines()
+ stripped_lines = [line.lstrip() for line in lines]
+ indents = [len(line) - len(line.lstrip()) for line in lines]
+ # if lines are already commented, remove them
+ if lines and all(
+ [
+ not line or line.startswith(self.inline_comment_marker)
+ for line in stripped_lines
+ ]
+ ):
+ marker_offset = len(self.inline_comment_marker)
+ offsets = [
+ (
+ 0
+ if not line
+ else (
+ marker_offset + 1
+ if line[marker_offset].isspace()
+ else marker_offset
+ )
+ )
+ for line in stripped_lines
+ ]
+ for lno, indent, offset in zip(
+ range(first[0], last[0] + 1), indents, offsets
+ ):
+ self.delete(
+ start=(lno, indent),
+ end=(lno, indent + offset),
+ maintain_selection_offset=True,
+ )
+ # add comment tokens to all lines
+ else:
+ comment_indent = min(
+ [indent for indent, line in zip(indents, stripped_lines) if line]
+ )
+ insertion = f"{self.inline_comment_marker} "
+ for lno, stripped_line in enumerate(stripped_lines, start=first[0]):
+ if stripped_line:
+ # insert one character at a time, to create a single undo-able
+ # batch of edits.
+ # See https://github.com/Textualize/textual/issues/4428
+ for i, char in enumerate(insertion):
+ self.insert(
+ char,
+ location=(lno, comment_indent + i),
+ maintain_selection_offset=True,
+ )
+
+ def action_undo(self) -> None:
+ self.post_message(TextAreaHideCompletionList())
+ super().action_undo()
+
+ def action_redo(self) -> None:
+ self.post_message(TextAreaHideCompletionList())
+ super().action_redo()
+
+ def _clear_double_click(self) -> None:
+ self.consecutive_clicks = 0
+ self.double_click_location = None
+ self.double_click_timer = None
+
+ def _copy_selection(self) -> None:
+ if self.selected_text:
+ self.clipboard = self.selected_text
+ else:
+ whole_line = self.get_text_range(
+ self.get_cursor_line_start_location(),
+ self.get_cursor_line_end_location(),
+ )
+ self.clipboard = f"{whole_line}{self.document.newline}"
+ if self.use_system_clipboard and self.system_copy is not None:
+ try:
+ self.system_copy(self.clipboard)
+ except Exception:
+ # no system clipboard; common in CI runners
+ self.post_message(TextAreaClipboardError(action="copy"))
+
+ def _get_character_at_cursor(self) -> str:
+ if self.cursor_at_end_of_line:
+ return ""
+ return self.get_text_range(
+ start=self.cursor_location, end=self.get_cursor_right_location()
+ )
+
+ def _get_character_before_cursor(self) -> str:
+ if self.cursor_at_start_of_line:
+ return ""
+ return self.get_text_range(
+ start=self.get_cursor_left_location(), end=self.cursor_location
+ )
+
+ def _get_word_before_cursor(self, event: events.Key | None = None) -> str:
+ lno = self.cursor_location[0]
+ line = self.get_text_range(start=(lno, 0), end=self.cursor_location)
+
+ if event is not None and event.key == "backspace":
+ if len(line) > 1:
+ search_string = line[:-1]
+ else:
+ search_string = ""
+ elif event is not None and event.character is not None:
+ search_string = f"{line}{event.character}"
+ else:
+ search_string = line
+
+ if self.completer_active == "path":
+ pattern = PATH_PROG
+ elif self.completer_active == "member":
+ pattern = MEMBER_PROG
+ else:
+ pattern = WORD_PROG
+
+ match = pattern.match(search_string[::-1])
+ if match:
+ return match.group(0)[::-1]
+ else:
+ return ""
+
+ def _handle_backspace(self, event: events.Key) -> None:
+ if self.completer_active is not None:
+ current_word = self._get_word_before_cursor(event)
+ if current_word:
+ self.post_message(self.ShowCompletionList(prefix=current_word))
+ else:
+ self.post_message(TextAreaHideCompletionList())
+
+ def _handle_enter(self, event: events.Key) -> None:
+ event.stop()
+ event.prevent_default()
+ if self.completer_active is not None:
+ self.post_message(self.CompletionListKey(event))
+ return
+ if self.read_only:
+ return
+ nl = self.document.newline
+ first, last = sorted([*self.selection])
+ indent = self._get_indent_level_of_line(index=first[0])
+ self.selection = Selection(start=first, end=first)
+ char_before = self._get_character_before_cursor()
+ if char_before in BRACKETS:
+ if self.indent_type == "tabs":
+ new_indent = indent + 1
+ indent_char = "\t"
+ else:
+ new_indent = indent + self.indent_width - (indent % self.indent_width)
+ indent_char = " "
+ self.replace(f"{nl}{indent_char*new_indent}", first, last)
+ char_at = self._get_character_at_cursor()
+ if char_at == BRACKETS[char_before]:
+ loc = self.selection
+ self.insert(f"{nl}{indent * indent_char}")
+ self.selection = loc
+ else:
+ indent_char = "\t" if self.indent_type == "tabs" else " "
+ self.insert(f"{nl}{indent * indent_char}", location=self.cursor_location)
+
+ def _handle_quote_or_bracket(self, event: events.Key) -> None:
+ event.stop()
+ event.prevent_default()
+ if self.read_only:
+ return
+ if self.completer_active != "member":
+ self.post_message(TextAreaHideCompletionList())
+ else:
+ prefix = self._get_word_before_cursor(event=event)
+ self.post_message(self.ShowCompletionList(prefix=prefix))
+ assert event.character is not None
+ if self.selection.start == self.selection.end:
+ self._insert_closed_character_at_cursor(event.character)
+ elif event.key in (
+ "right_parenthesis",
+ "right_square_bracket",
+ "right_curly_bracket",
+ ):
+ self.replace(event.character, *self.selection)
+ else:
+ self._insert_characters_around_selection(event.character)
+
+ def _handle_shift_tab(self, event: events.Key) -> None:
+ event.stop()
+ event.prevent_default()
+ if self.read_only:
+ self.app.action_focus_previous()
+ return
+ if self.completer_active is not None:
+ self.post_message(self.CompletionListKey(event))
+ return
+ self._indent_selection(kind="dedent")
+
+ def _handle_separator(self, event: events.Key) -> None:
+ event.stop()
+ if self.completer_active != "path":
+ self.completer_active = "member"
+ prefix = self._get_word_before_cursor(event)
+ self.post_message(self.ShowCompletionList(prefix=prefix))
+
+ def _handle_escape(self, event: events.Key) -> None:
+ """
+ starting in textual 0.49, escape is handled by on_key instead of
+ a binding, so we inherited behavior we don't want. Trap this event
+ and hide the completion list.
+ """
+ event.stop()
+ event.prevent_default()
+ self.selection = Selection(self.selection.end, self.selection.end)
+ self.post_message(TextAreaHideCompletionList())
+
+ def _handle_slash(self, event: events.Key) -> None:
+ event.stop()
+ self.completer_active = "path"
+ prefix = self._get_word_before_cursor(event)
+ self.post_message(self.ShowCompletionList(prefix=prefix))
+
+ def _handle_tab(self, event: events.Key) -> None:
+ event.stop()
+ event.prevent_default()
+ if self.completer_active is not None:
+ self.post_message(self.CompletionListKey(event))
+ return
+ if self.read_only:
+ self.app.action_focus_next()
+ return
+ first, last = sorted([*self.selection])
+ # in some cases, selections are replaced with indent
+ if first[0] == last[0] and (
+ first[1] == last[1]
+ or first[1] != 0
+ or last[1] != len(self.document.get_line(last[0])) - 1
+ ):
+ indent_char = "\t" if self.indent_type == "tabs" else " "
+ indent_width = 1 if self.indent_type == "tabs" else self.indent_width
+ self.replace(
+ f"{indent_char*(indent_width - first[1] % indent_width)}",
+ first,
+ last,
+ maintain_selection_offset=False,
+ )
+ # usually, selected lines are prepended with four-ish spaces
+ else:
+ self._indent_selection(kind="indent")
+
+ def _handle_up_down(self, event: events.Key) -> None:
+ if self.completer_active is not None:
+ event.stop()
+ event.prevent_default()
+ self.post_message(self.CompletionListKey(event))
+
+ def _handle_printable_character(self, event: events.Key) -> None:
+ assert event.character is not None, "Error! Printable key with no character."
+ if self.completer_active is None:
+ if WORD_PROG.match(event.character) is not None:
+ self.completer_active = "word"
+ else:
+ return
+ current_word = self._get_word_before_cursor(event)
+ if current_word:
+ self.post_message(self.ShowCompletionList(prefix=current_word))
+ else:
+ self.post_message(TextAreaHideCompletionList())
+
+ def _indent_selection(self, kind: Literal["indent", "dedent"]) -> None:
+ rounder, offset = (ceil, -1) if kind == "dedent" else (floor, 1)
+
+ original_selection = self.selection
+ lines, first, last = self._get_selected_lines()
+ if kind == "dedent" and not lines:
+ return
+
+ indent_width = 1 if self.indent_type == "tabs" else self.indent_width
+ indent_char = "\t" if self.indent_type == "tabs" else " " * self.indent_width
+ raw_indents = [
+ self._get_indent_level_of_line(lno) for lno in range(first[0], last[0] + 1)
+ ]
+ tab_stops = [rounder(space / indent_width) for space in raw_indents]
+
+ new_lines = [
+ f"{indent_char * max(0, indent+offset)}{line.lstrip()}"
+ for line, indent in zip(lines, tab_stops)
+ ]
+ self.replace(
+ self.document.newline.join(new_lines),
+ start=(first[0], 0),
+ end=(last[0], len(self.document.get_line(last[0]))),
+ )
+
+ change_at_start = (
+ 0
+ if original_selection.start[1] == 0
+ else len(new_lines[original_selection.start[0] - first[0]])
+ - len(lines[original_selection.start[0] - first[0]])
+ )
+ change_at_cursor = (
+ 0
+ if original_selection.end[1] == 0
+ else len(new_lines[original_selection.end[0] - first[0]])
+ - len(lines[original_selection.end[0] - first[0]])
+ )
+ self.selection = Selection(
+ start=(
+ original_selection.start[0],
+ original_selection.start[1] + change_at_start,
+ ),
+ end=(
+ original_selection.end[0],
+ original_selection.end[1] + change_at_cursor,
+ ),
+ )
+
+ def _insert_characters_around_selection(self, character: str) -> None:
+ first = min(*self.selection)
+ self.insert(character, location=first, maintain_selection_offset=True)
+ first, last = sorted([*self.selection])
+ self.insert(CLOSERS[character], location=last, maintain_selection_offset=False)
+ self.selection = Selection(start=first, end=last)
+
+ def _insert_closed_character_at_cursor(self, character: str) -> None:
+ if self._get_character_at_cursor() == character:
+ self.action_cursor_right()
+ else:
+ if (character in BRACKETS and self._should_complete_brackets()) or (
+ character in CLOSERS and self._should_complete_quotes()
+ ):
+ self.insert(character, self.cursor_location)
+ loc = self.selection
+ self.insert(CLOSERS[character], self.cursor_location)
+ self.selection = loc
+ else:
+ self.insert(character, self.cursor_location)
+
+ def _should_complete_brackets(self) -> bool:
+ if self.cursor_at_end_of_line:
+ return True
+
+ next_char = self._get_character_at_cursor()
+ if not next_char or next_char.isspace():
+ return True
+ elif next_char in """>:,.="'""":
+ return True
+
+ return False
+
+ def _should_complete_quotes(self) -> bool:
+ next_char = self._get_character_at_cursor()
+ prev_char = self._get_character_before_cursor()
+ if (
+ self.cursor_at_end_of_line or next_char.isspace() or next_char in ")>:,.="
+ ) and (
+ self.cursor_at_start_of_line
+ or prev_char.isspace()
+ or NON_WORD_CHAR_PROG.match(prev_char) is not None
+ ):
+ return True
+ return False
+
+ def _get_indent_level_of_line(self, index: int | None = None) -> int:
+ if index is None:
+ index = self.cursor_location[0]
+ line = self.document.get_line(index)
+ while line.isspace() and index > 0:
+ index -= 1
+ line = self.document.get_line(index)
+ if line.isspace():
+ return 0
+ indent_char = "\t" if self.indent_type == "tabs" else " "
+ indent_level = len(line) - len(line.lstrip(indent_char))
+ return indent_level
+
+ def _get_selected_lines(self) -> tuple[list[str], Location, Location]:
+ [first, last] = sorted([self.selection.start, self.selection.end])
+ lines = [self.document.get_line(i) for i in range(first[0], last[0] + 1)]
+ return lines, first, last
+
+
+class TextEditor(Widget, can_focus=True, can_focus_children=False):
+ """
+ A Widget that presents a feature-rich, multiline text editor interface.
+
+ Attributes:
+ text (str): The contents of the TextEditor
+ language (str): Must be the short name of a Pygments lexer
+ (https://pygments.org/docs/lexers/), e.g., "python", "sql", "as3".
+ theme (str): Must be name of a Pygments style (https://pygments.org/styles/),
+ e.g., "bw", "github-dark", "solarized-light".
+ """
+
+ DEFAULT_CSS = """
+ #textarea__save_open_input_label {
+ margin: 0 0 0 3;
+ }
+ .validation-error {
+ color: $error;
+ text-style: italic;
+ }
+ Input.textarea--footer-input {
+ border: round $foreground;
+ color: $foreground;
+ background: $background;
+ &.-invalid {
+ border: round $error 60%;
+ }
+ &.-invalid:focus {
+ border: round $error;
+ }
+ }
+ """
+
+ BINDINGS = [
+ Binding("ctrl+s", "save", "Save Query"),
+ Binding("ctrl+o", "load", "Open Query"),
+ Binding("ctrl+f", "find", "Find"),
+ Binding("f3", "find(True)", "Find Next"),
+ Binding("ctrl+g", "goto_line", "Go To Line"),
+ Binding("ctrl+q", "quit", "Quit"),
+ ]
+
+ theme: reactive[str] = reactive("monokai")
+
+ def __init__(
+ self,
+ *children: Widget,
+ name: str | None = None,
+ id: str | None = None, # noqa: A002
+ classes: str | None = None,
+ disabled: bool = False,
+ read_only: bool = False,
+ language: str | None = None,
+ theme: str = "css",
+ text: str = "",
+ use_system_clipboard: bool = True,
+ path_completer: (
+ Callable[
+ [str],
+ Sequence[tuple[RenderableType, str]]
+ | Sequence[tuple[tuple[str, str], str]],
+ ]
+ | None
+ ) = path_completer,
+ member_completer: (
+ Callable[
+ [str],
+ Sequence[tuple[RenderableType, str]]
+ | Sequence[tuple[tuple[str, str], str]],
+ ]
+ | None
+ ) = None,
+ word_completer: (
+ Callable[
+ [str],
+ Sequence[tuple[RenderableType, str]]
+ | Sequence[tuple[tuple[str, str], str]],
+ ]
+ | None
+ ) = None,
+ ) -> None:
+ """
+ Initializes an instance of a TextArea.
+
+ Args:
+ (see also textual.widget.Widget)
+ language (str): Must be the short name of a tree-sitter language,
+ e.g., "python", "sql"
+ theme (str): Must be name of a Textual Theme.
+ """
+ super().__init__(
+ *children,
+ name=name,
+ id=id,
+ classes=classes,
+ disabled=disabled,
+ )
+ self._language = language
+ self._theme = theme
+ self._initial_text = text
+ self._find_history: list[str] = []
+ self.use_system_clipboard = use_system_clipboard
+ self.text_input: TextAreaPlus | None = None
+ self.read_only = read_only
+ self.path_completer = path_completer
+ self.member_completer = member_completer
+ self.word_completer = word_completer
+
+ @property
+ def text(self) -> str:
+ """
+ Returns:
+ (str) The contents of the TextEditor.
+ """
+ if self.text_input is None:
+ return ""
+ return self.text_input.text
+
+ @text.setter
+ def text(self, contents: str) -> None:
+ """
+ Args:
+ contents (str): A string (optionally containing newlines) to
+ set the contents of the TextEditor equal to.
+ """
+ if self.text_input is None:
+ return
+ self.text_input.history.checkpoint()
+ self.text_input.replace(
+ contents,
+ start=(0, 0),
+ end=self.text_input.document.end,
+ maintain_selection_offset=False,
+ )
+ self.text_input.move_cursor((0, 0))
+
+ @property
+ def selected_text(self) -> str:
+ """
+ Returns:
+ str: The contents of the TextEditor between the selection
+ anchor and the cursor. Returns an empty string if the
+ selection anchor is not set.
+ """
+ if self.text_input is None:
+ return ""
+ return self.text_input.selected_text
+
+ @property
+ def selection(self) -> Selection:
+ """
+ Returns
+ Selection: The location of the cursor in the TextEditor
+ """
+ if self.text_input is None:
+ return Selection((0, 0), (0, 0))
+ return self.text_input.selection
+
+ @selection.setter
+ def selection(self, selection: Selection) -> None:
+ """
+ Args:
+ selection (Selection): The position (line number, pos)
+ to move the cursor and selection anchor to
+ """
+ if self.text_input is None:
+ return
+ self.text_input.selection = selection
+
+ @property
+ def language(self) -> str | None:
+ """
+ Returns
+ str | None: The tree-sitter short name of the active language
+ """
+ if self.text_input is None:
+ return None
+ return self.text_input.language
+
+ @language.setter
+ def language(self, language: str) -> None:
+ """
+ Args:
+ langage (str | None): The Pygments short name for the new language
+ """
+ if self.text_input is None:
+ return None
+ self.text_input.language = language
+
+ @property
+ def line_count(self) -> int:
+ """
+ Returns the number of lines in the document.
+ """
+ if self.text_input is None:
+ return 0
+ return self.text_input.document.line_count
+
+ def get_line(self, index: int) -> str:
+ """
+ Returns the line with the given index from the document.
+
+ Args:
+ index: The index of the line in the document.
+
+ Returns:
+ The str instance representing the line.
+ """
+ if self.text_input is None:
+ return ""
+ return self.text_input.document.get_line(index=index)
+
+ def get_text_range(self, selection: Selection) -> str:
+ """
+ Get the text between a start and end location.
+
+ Args:
+ selection: The start and end locations
+
+ Returns:
+ The text between start and end.
+ """
+ if self.text_input is None:
+ return ""
+ return self.text_input.get_text_range(*selection)
+
+ def insert_text_at_selection(self, text: str) -> None:
+ """
+ Inserts text at the current cursor position; if there is a selection anchor,
+ first deletes the current selection.
+
+ Args:
+ text (str): The text to be inserted.
+ """
+ if self.text_input is None:
+ return
+ self.text_input.replace(
+ text,
+ *self.text_input.selection,
+ maintain_selection_offset=False,
+ )
+
+ def copy_to_clipboard(self, text: str) -> None:
+ """
+ Sets the editor's internal clipboard, and the system clipboard if enabled, to
+ the value of text
+
+ Args:
+ text (str): The text to place on the clipboard.
+ """
+ if self.text_input is None:
+ self.post_message(TextAreaClipboardError(action="copy"))
+ return
+ self.text_input.clipboard = text
+ if self.use_system_clipboard and self.text_input.system_copy is not None:
+ try:
+ self.text_input.system_copy(text)
+ except Exception:
+ self.post_message(TextAreaClipboardError(action="copy"))
+
+ def pause_blink(self, visible: bool = True) -> None:
+ """
+ Pauses the blink of the cursor
+ """
+ if self.text_input is None:
+ return
+ self.text_input._pause_blink(visible=visible)
+
+ def restart_blink(self) -> None:
+ """
+ Restarts the blink of the cursor
+ """
+ if self.text_input is None:
+ return
+ self.text_input._restart_blink()
+
+ def prepare_query(self, source: str) -> "Query" | None:
+ """
+ Build a Query from source. The Query can be used with self.query_syntax_tree
+
+ Args:
+ source (str): A tree-sitter query. See
+ https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax
+ """
+ if self.text_input is None:
+ return None
+ return self.text_input.document.prepare_query(query=source)
+
+ 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.
+
+ Args:
+ query (Query): The tree-sitter Query to perform.
+ start_point (tuple[int, int] | None): The (row, column byte) to start the
+ query at.
+ end_point (tuple[int, int] | None): The (row, column byte) to end the
+ query at.
+
+ Returns:
+ A dict mapping captured node names to lists of Nodes with that name
+ """
+ if self.text_input is None:
+ return {}
+ return self.text_input.document.query_syntax_tree(
+ query=query, start_point=start_point, end_point=end_point
+ )
+
+ @property
+ def syntax_tree(self) -> "Tree" | None:
+ """
+ Returns the document's syntax tree.
+ """
+ if self.text_input is None:
+ return None
+ if isinstance(self.text_input.document, SyntaxAwareDocument):
+ return self.text_input.document._syntax_tree
+ else:
+ return None
+
+ @property
+ def parser(self) -> "Parser" | None:
+ if self.text_input is None:
+ return None
+ if isinstance(self.text_input.document, SyntaxAwareDocument):
+ return self.text_input.document._parser
+ else:
+ return None
+
+ def compose(self) -> ComposeResult:
+ self.text_container = TextContainer()
+ self.text_input = TextAreaPlus(
+ language=self._language, text=self._initial_text, read_only=self.read_only
+ )
+ self.completion_list = CompletionList()
+ self.footer = FooterContainer(classes="hide")
+ self.footer_label = Label("", id="textarea__save_open_input_label")
+ with self.text_container:
+ yield self.text_input
+ yield self.completion_list
+ with self.footer:
+ yield self.footer_label
+
+ def on_mount(self) -> None:
+ # delay setting the reactive until the widget mounts so we can be sure that
+ # self.text_input exists so watch_theme can do its thing.
+ self.theme = self._theme
+
+ def on_focus(self) -> None:
+ if self.text_input is not None:
+ self.text_input.focus()
+
+ def on_click(self) -> None:
+ if self.text_input is not None:
+ self.text_input.focus()
+
+ @on(TextAreaHideCompletionList)
+ def hide_completion_list(self, event: TextAreaHideCompletionList) -> None:
+ event.stop()
+ assert self.text_input is not None
+ self.completion_list.is_open = False
+ self.text_input.completer_active = None
+
+ @on(TextAreaPlus.SelectionChanged)
+ def update_completion_list_offset(
+ self, event: TextAreaPlus.SelectionChanged
+ ) -> None:
+ event.stop()
+ assert self.text_input is not None
+ region_x, region_y, _, _ = self.text_input.region
+ self.completion_list.cursor_offset = self.text_input.cursor_screen_offset - (
+ region_x,
+ region_y,
+ )
+
+ @on(TextAreaPlus.Changed)
+ def check_for_find_updates(self, event: TextAreaPlus.Changed) -> None:
+ event.stop()
+ try:
+ find_input = self.footer.query_one(FindInput)
+ except Exception:
+ return
+ self._update_find_label(value=find_input.value)
+
+ @on(TextAreaPlus.ShowCompletionList)
+ def update_completers_and_completion_list_offset(
+ self, event: TextAreaPlus.ShowCompletionList
+ ) -> None:
+ event.stop()
+ assert self.text_input is not None
+ region_x, region_y, _, _ = self.text_input.region
+ self.completion_list.cursor_offset = self.text_input.cursor_screen_offset - (
+ region_x,
+ region_y,
+ )
+ if self.text_input.completer_active == "path":
+ self.completion_list.show_completions(event.prefix, self.path_completer)
+ elif self.text_input.completer_active == "member":
+ self.completion_list.show_completions(event.prefix, self.member_completer)
+ elif self.text_input.completer_active == "word":
+ self.completion_list.show_completions(event.prefix, self.word_completer)
+
+ @on(TextAreaPlus.CompletionListKey)
+ def forward_keypress_to_completion_list(
+ self, event: TextAreaPlus.CompletionListKey
+ ) -> None:
+ event.stop()
+ self.completion_list.process_keypress(event.key)
+
+ @on(OptionList.OptionSelected)
+ def insert_completion(self, event: OptionList.OptionSelected) -> None:
+ event.stop()
+ assert self.text_input is not None
+ value = getattr(event.option, "value", None) or str(event.option.prompt)
+ self.text_input.replace_current_word(value)
+ self.completion_list.is_open = False
+ self.text_input.completer_active = None
+
+ @on(CancellableInput.Cancelled)
+ def clear_footer(self) -> None:
+ self._clear_footer_input()
+ if self.text_input is not None:
+ self.text_input.focus()
+
+ @on(Input.Changed)
+ def update_validation_label(self, message: Input.Changed) -> None:
+ if message.input.id is None:
+ return
+ label = self.footer_label
+ if message.input.id in (
+ "textarea__save_input",
+ "textarea__open_input",
+ "textarea__gotoline_input",
+ ):
+ message.stop()
+ if message.validation_result and not message.validation_result.is_valid:
+ label.add_class("validation-error")
+ label.update(";".join(message.validation_result.failure_descriptions))
+ elif (
+ message.validation_result
+ and message.validation_result.is_valid
+ and message.input.id in ("textarea__save_input", "textarea__open_input")
+ ):
+ action = "Saving to" if "save" in message.input.id else "Opening"
+ p = Path(message.input.value).expanduser().resolve()
+ with suppress(ValueError):
+ p = Path("~") / p.relative_to(Path.home())
+ label.remove_class("validation-error")
+ label.update(f"{action} {p}")
+ else:
+ label.remove_class("validation-error")
+ label.update("")
+ elif message.input.id in ("textarea__find_input"):
+ message.stop()
+ self._find_next_after_cursor(value=message.value)
+
+ @on(Input.Submitted, "#textarea__save_input")
+ def save_file(self, message: Input.Submitted) -> None:
+ """
+ Handle the submit event for the Save and Open modals.
+ """
+ message.stop()
+ expanded_path = Path(message.input.value).expanduser()
+ try:
+ expanded_path.parent.mkdir(parents=True, exist_ok=True)
+ with open(expanded_path, "w") as f:
+ f.write(self.text)
+ except OSError as e:
+ self.app.push_screen(
+ ErrorModal(
+ title="Save File Error",
+ header=("There was an error when attempting to save your file:"),
+ error=e,
+ )
+ )
+ else:
+ self.post_message(TextAreaSaved(path=expanded_path))
+ self._clear_footer_input()
+ if self.text_input is not None:
+ self.text_input.focus()
+
+ @on(Input.Submitted, "#textarea__open_input")
+ def open_file(self, message: Input.Submitted) -> None:
+ message.stop()
+ expanded_path = Path(message.input.value).expanduser()
+ try:
+ with open(expanded_path, "r") as f:
+ contents = f.read()
+ except OSError as e:
+ self.app.push_screen(
+ ErrorModal(
+ title="Open File Error",
+ header=("There was an error when attempting to open your file:"),
+ error=e,
+ )
+ )
+ else:
+ self.text = contents
+ self._clear_footer_input()
+ if self.text_input is not None:
+ self.text_input.focus()
+
+ @on(Input.Submitted, "#textarea__gotoline_input")
+ def goto_line(self, message: Input.Submitted) -> None:
+ message.stop()
+ assert self.text_input is not None
+ try:
+ new_line = int(message.value) - 1
+ except (ValueError, TypeError):
+ return
+ self.text_input.move_cursor((new_line, 0), select=False)
+ self._clear_footer_input()
+ self.text_input.focus()
+
+ @on(Input.Submitted, "#textarea__find_input")
+ def find_next(self, message: Input.Submitted) -> None:
+ message.stop()
+ message.input.checkpoint() # type: ignore
+ self.selection = Selection(start=self.selection.end, end=self.selection.end)
+ self._find_next_after_cursor(value=message.value)
+
+ def watch_theme(self, theme: str) -> None:
+ if self.text_input is None:
+ self.app.notify(
+ message=(
+ "Could not load the selected theme in the TextArea, because "
+ "it has not yet loaded. Please try again."
+ ),
+ severity="warning",
+ )
+ return
+
+ if theme in self.text_input.available_themes:
+ self.text_input.theme = theme
+ else:
+ css_vars = self.app.get_css_variables()
+ theme_obj = self.app.get_theme(theme_name=theme)
+ if theme_obj is None:
+ self.post_message(TextAreaThemeError(theme=theme))
+ return
+ textarea_theme = text_area_theme_from_app_theme(theme, theme_obj, css_vars)
+ self.text_input.register_theme(textarea_theme)
+ self.text_input.theme = theme
+
+ def action_save(self) -> None:
+ self._clear_footer_input()
+ self._mount_footer_path_input("save")
+
+ def action_load(self) -> None:
+ self._clear_footer_input()
+ self._mount_footer_path_input("open")
+
+ def action_find(self, prepopulate_from_history: bool = False) -> None:
+ try:
+ find_input = self.footer.query_one(FindInput)
+ except Exception:
+ pass
+ else:
+ find_input.focus()
+ return
+ self._clear_footer_input()
+ if prepopulate_from_history and self._find_history:
+ value = self._find_history[-1]
+ else:
+ value = ""
+ find_input = FindInput(
+ value=value,
+ history=self._find_history,
+ classes="textarea--footer-input",
+ )
+ self._mount_footer_input(input_widget=find_input)
+
+ def action_goto_line(self) -> None:
+ try:
+ goto_input = self.footer.query_one(GotoLineInput)
+ except Exception:
+ pass
+ else:
+ goto_input.focus()
+ return
+ self._clear_footer_input()
+ goto_input = GotoLineInput(
+ max_line_number=self.text_input.document.line_count
+ if self.text_input is not None
+ else 10000,
+ current_line=self.selection.end[0] + 1,
+ min_line_number=1,
+ id="textarea__gotoline_input",
+ classes="textarea--footer-input",
+ )
+ self._mount_footer_input(input_widget=goto_input)
+
+ def _clear_footer_input(self) -> None:
+ try:
+ self.footer.query_one(Input).remove()
+ except Exception:
+ pass
+ try:
+ self.footer_label.update("")
+ except Exception:
+ pass
+ self.footer.add_class("hide")
+
+ def _mount_footer_input(self, input_widget: Input) -> None:
+ self.footer.remove_class("hide")
+ try:
+ self.footer.mount(input_widget)
+ except DuplicateIds:
+ return
+ else:
+ input_widget.focus()
+
+ def _mount_footer_path_input(self, name: str) -> None:
+ if name == "open":
+ file_okay, dir_okay, must_exist = True, False, True
+ else:
+ file_okay, dir_okay, must_exist = True, False, False
+
+ path_input = PathInput(
+ id=f"textarea__{name}_input",
+ placeholder=f"{name.capitalize()}: Enter file path OR press ESC to cancel",
+ file_okay=file_okay,
+ dir_okay=dir_okay,
+ must_exist=must_exist,
+ classes="textarea--footer-input",
+ )
+ self._mount_footer_input(input_widget=path_input)
+
+ def _find_next_after_cursor(self, value: str) -> None:
+ assert self.text_input is not None
+ label = self.footer_label
+ if not value:
+ label.update("")
+ return
+ cursor = self.selection.start
+ lines = self.text_input.document.lines
+ # first search text after the cursor
+ for i, line in enumerate(lines[cursor[0] :]):
+ pos = line.find(value, cursor[1] if i == 0 else None)
+ if pos >= 0:
+ self.selection = Selection(
+ start=(cursor[0] + i, pos),
+ end=(cursor[0] + i, pos + cell_len(value)),
+ )
+ break
+ # search text from beginning, including line with cursor
+ else:
+ for i, line in enumerate(lines[: cursor[0] + 1]):
+ pos = line.find(value)
+ if pos >= 0:
+ self.selection = Selection(
+ start=(i, pos), end=(i, pos + cell_len(value))
+ )
+ break
+ self.text_input.scroll_cursor_visible(animate=True)
+ self._update_find_label(value=value)
+
+ def _update_find_label(self, value: str) -> None:
+ label = self.footer_label
+ n_matches = self.text.count(value)
+ if n_matches > 1:
+ label.update(f"{n_matches} found; Enter for next; ESC to close")
+ elif n_matches > 0:
+ label.update(f"{n_matches} found")
+ else:
+ label.update("No results.")
diff --git a/stubs/pyperclip/__init__.pyi b/stubs/pyperclip/__init__.pyi
new file mode 100644
index 0000000..93a3f09
--- /dev/null
+++ b/stubs/pyperclip/__init__.pyi
@@ -0,0 +1,7 @@
+from typing import Callable, Tuple
+
+def copy(s: str) -> None: ...
+def paste() -> str: ...
+def determine_clipboard() -> Tuple[Callable[[str], None], Callable[[], str]]: ...
+
+class PyperclipException(Exception): ...
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..d7bb3c4
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,9 @@
+from pathlib import Path
+
+import pytest
+
+
+@pytest.fixture
+def data_dir() -> Path:
+ here = Path(__file__)
+ return here.parent / "data"
diff --git a/tests/data/test_open/empty.py b/tests/data/test_open/empty.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/data/test_open/foo.py b/tests/data/test_open/foo.py
new file mode 100644
index 0000000..c13cc94
--- /dev/null
+++ b/tests/data/test_open/foo.py
@@ -0,0 +1,2 @@
+def foo(bar: str, baz: int) -> None:
+ return
diff --git a/tests/data/test_validator/bar/.gitkeep b/tests/data/test_validator/bar/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/data/test_validator/foo/baz.txt b/tests/data/test_validator/foo/baz.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional_tests/__init__.py b/tests/functional_tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional_tests/conftest.py b/tests/functional_tests/conftest.py
new file mode 100644
index 0000000..a3be044
--- /dev/null
+++ b/tests/functional_tests/conftest.py
@@ -0,0 +1,62 @@
+from typing import Type, Union
+from unittest.mock import MagicMock
+
+import pytest
+from textual.app import App, ComposeResult
+from textual.driver import Driver
+from textual.types import CSSPathType
+from textual_textarea.text_editor import TextEditor
+
+
+class TextEditorApp(App, inherit_bindings=False):
+ def __init__(
+ self,
+ driver_class: Union[Type[Driver], None] = None,
+ css_path: Union[CSSPathType, None] = None,
+ watch_css: bool = False,
+ language: Union[str, None] = None,
+ use_system_clipboard: bool = True,
+ ):
+ self.language = language
+ self.use_system_clipboard = use_system_clipboard
+ super().__init__(driver_class, css_path, watch_css)
+
+ def compose(self) -> ComposeResult:
+ self.editor = TextEditor(
+ language=self.language,
+ use_system_clipboard=self.use_system_clipboard,
+ id="ta",
+ )
+ yield self.editor
+
+ def on_mount(self) -> None:
+ self.editor.focus()
+
+
+@pytest.fixture
+def app() -> App:
+ app = TextEditorApp(language="python")
+ return app
+
+
+@pytest.fixture(
+ params=[False, True],
+ ids=["no_sys_clipboard", "default"],
+)
+def app_all_clipboards(request: pytest.FixtureRequest) -> App:
+ app = TextEditorApp(use_system_clipboard=request.param)
+ return app
+
+
+@pytest.fixture(autouse=True)
+def mock_pyperclip(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
+ mock = MagicMock()
+ mock.determine_clipboard.return_value = mock.copy, mock.paste
+
+ def set_paste(x: str) -> None:
+ mock.paste.return_value = x
+
+ mock.copy.side_effect = set_paste
+ monkeypatch.setattr("textual_textarea.text_editor.pyperclip", mock)
+
+ return mock
diff --git a/tests/functional_tests/test_autocomplete.py b/tests/functional_tests/test_autocomplete.py
new file mode 100644
index 0000000..1065de8
--- /dev/null
+++ b/tests/functional_tests/test_autocomplete.py
@@ -0,0 +1,279 @@
+from __future__ import annotations
+
+from pathlib import Path
+from time import monotonic
+from typing import Callable
+from unittest.mock import MagicMock
+
+import pytest
+from textual.app import App
+from textual.message import Message
+from textual.widgets.text_area import Selection
+from textual_textarea import TextEditor
+
+
+@pytest.fixture
+def word_completer() -> Callable[[str], list[tuple[str, str]]]:
+ def _completer(prefix: str) -> list[tuple[str, str]]:
+ words = [
+ "satisfy",
+ "season",
+ "second",
+ "seldom",
+ "select",
+ "self",
+ "separate",
+ "set",
+ "space",
+ "super",
+ ]
+ return [(w, w) for w in words if w.startswith(prefix)]
+
+ return _completer
+
+
+@pytest.fixture
+def word_completer_with_types() -> Callable[[str], list[tuple[tuple[str, str], str]]]:
+ def _completer(prefix: str) -> list[tuple[tuple[str, str], str]]:
+ words = [
+ "satisfy",
+ "season",
+ "second",
+ "seldom",
+ "select",
+ "self",
+ "separate",
+ "set",
+ "space",
+ "super",
+ ]
+ return [((w, "word"), w) for w in words if w.startswith(prefix)]
+
+ return _completer
+
+
+@pytest.fixture
+def member_completer() -> Callable[[str], list[tuple[str, str]]]:
+ mock = MagicMock()
+ mock.return_value = [("completion", "completion")]
+ return mock
+
+
+@pytest.mark.asyncio
+async def test_autocomplete(
+ app: App, word_completer: Callable[[str], list[tuple[str, str]]]
+) -> None:
+ messages: list[Message] = []
+ async with app.run_test(message_hook=messages.append) as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.word_completer = word_completer
+ ta.focus()
+ while ta.word_completer is None:
+ await pilot.pause()
+
+ start_time = monotonic()
+ await pilot.press("s")
+ while ta.completion_list.is_open is False:
+ if monotonic() - start_time > 10:
+ print("MESSAGES:")
+ print("\n".join([str(m) for m in messages]))
+ break
+ await pilot.pause()
+ assert ta.text_input
+ assert ta.text_input.completer_active == "word"
+ assert ta.completion_list.is_open is True
+ assert ta.completion_list.option_count == 10
+ first_offset = ta.completion_list.styles.offset
+
+ await pilot.press("e")
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active == "word"
+ assert ta.completion_list.is_open is True
+ assert ta.completion_list.option_count == 7
+ assert ta.completion_list.styles.offset == first_offset
+
+ await pilot.press("z") # sez, no matches
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active is None
+ assert ta.completion_list.is_open is False
+
+ # backspace when the list is not open doesn't re-open it
+ await pilot.press("backspace")
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active is None
+ assert ta.completion_list.is_open is False
+
+ await pilot.press("l") # sel
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active == "word"
+ assert ta.completion_list.is_open is True
+ assert ta.completion_list.option_count == 3
+ assert ta.completion_list.styles.offset == first_offset
+
+ await pilot.press("backspace") # se
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active == "word"
+ assert ta.completion_list.is_open is True
+ assert ta.completion_list.option_count == 7
+ assert ta.completion_list.styles.offset == first_offset
+
+ await pilot.press("enter")
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active is None
+ assert ta.completion_list.is_open is False
+ assert ta.text == "season"
+ assert ta.selection.end[1] == 6
+
+
+@pytest.mark.asyncio
+async def test_autocomplete_with_types(
+ app: App,
+ word_completer_with_types: Callable[[str], list[tuple[tuple[str, str], str]]],
+) -> None:
+ messages: list[Message] = []
+ word_completer = word_completer_with_types
+ async with app.run_test(message_hook=messages.append) as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.word_completer = word_completer
+ ta.focus()
+ while ta.word_completer is None:
+ await pilot.pause()
+
+ start_time = monotonic()
+ await pilot.press("s")
+ while ta.completion_list.is_open is False:
+ if monotonic() - start_time > 10:
+ print("MESSAGES:")
+ print("\n".join([str(m) for m in messages]))
+ break
+ await pilot.pause()
+ assert ta.text_input
+ assert ta.text_input.completer_active == "word"
+ assert ta.completion_list.is_open is True
+ assert ta.completion_list.option_count == 10
+ first_offset = ta.completion_list.styles.offset
+
+ await pilot.press("e")
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active == "word"
+ assert ta.completion_list.is_open is True
+ assert ta.completion_list.option_count == 7
+ assert ta.completion_list.styles.offset == first_offset
+
+ await pilot.press("z") # sez, no matches
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active is None
+ assert ta.completion_list.is_open is False
+
+ # backspace when the list is not open doesn't re-open it
+ await pilot.press("backspace")
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active is None
+ assert ta.completion_list.is_open is False
+
+ await pilot.press("l") # sel
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active == "word"
+ assert ta.completion_list.is_open is True
+ assert ta.completion_list.option_count == 3
+ assert ta.completion_list.styles.offset == first_offset
+
+ await pilot.press("backspace") # se
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active == "word"
+ assert ta.completion_list.is_open is True
+ assert ta.completion_list.option_count == 7
+ assert ta.completion_list.styles.offset == first_offset
+
+ await pilot.press("enter")
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+ assert ta.text_input.completer_active is None
+ assert ta.completion_list.is_open is False
+ assert ta.text == "season"
+ assert ta.selection.end[1] == 6
+
+
+@pytest.mark.asyncio
+async def test_autocomplete_paths(app: App, data_dir: Path) -> None:
+ messages: list[Message] = []
+ async with app.run_test(message_hook=messages.append) as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.focus()
+ test_path = str(data_dir / "test_validator")
+ ta.text = test_path
+ await pilot.pause()
+ ta.selection = Selection((0, len(test_path)), (0, len(test_path)))
+
+ start_time = monotonic()
+ await pilot.press("slash")
+ while ta.completion_list.is_open is False:
+ if monotonic() - start_time > 10:
+ print("MESSAGES:")
+ print("\n".join([str(m) for m in messages]))
+ break
+ await pilot.pause()
+ assert ta.text_input
+ assert ta.text_input.completer_active == "path"
+ assert ta.completion_list.is_open is True
+ assert ta.completion_list.option_count == 2
+
+
+@pytest.mark.parametrize(
+ "text,keys,expected_prefix",
+ [
+ ("foo bar", ["full_stop"], "bar."),
+ ("foo 'bar'", ["full_stop"], "'bar'."),
+ ("foo `bar`", ["full_stop"], "`bar`."),
+ ('foo "bar"', ["full_stop"], '"bar".'),
+ ("foo bar", ["colon"], "bar:"),
+ ("foo bar", ["colon", "colon"], "bar::"),
+ ('foo "bar"', ["colon", "colon"], '"bar"::'),
+ ("foo bar", ["full_stop", "quotation_mark"], 'bar."'),
+ ('foo "bar"', ["full_stop", "quotation_mark"], '"bar"."'),
+ ],
+)
+@pytest.mark.asyncio
+async def test_autocomplete_members(
+ app: App,
+ member_completer: MagicMock,
+ text: str,
+ keys: list[str],
+ expected_prefix: str,
+) -> None:
+ messages: list[Message] = []
+ async with app.run_test(message_hook=messages.append) as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.member_completer = member_completer
+ ta.focus()
+ while ta.member_completer is None:
+ await pilot.pause()
+ ta.text = text
+ ta.selection = Selection((0, len(text)), (0, len(text)))
+ await pilot.pause()
+ for key in keys:
+ await pilot.press(key)
+
+ start_time = monotonic()
+ while ta.completion_list.is_open is False:
+ if monotonic() - start_time > 10:
+ print("MESSAGES:")
+ print("\n".join([str(m) for m in messages]))
+ break
+ await pilot.pause()
+
+ member_completer.assert_called_with(expected_prefix)
+ assert ta.text_input is not None
+ assert ta.text_input.completer_active == "member"
+ assert ta.completion_list.is_open is True
diff --git a/tests/functional_tests/test_comments.py b/tests/functional_tests/test_comments.py
new file mode 100644
index 0000000..75d96f3
--- /dev/null
+++ b/tests/functional_tests/test_comments.py
@@ -0,0 +1,29 @@
+import pytest
+from textual.app import App
+from textual.widgets.text_area import Selection
+from textual_textarea import TextEditor
+
+
+@pytest.mark.parametrize(
+ "language,expected_marker",
+ [
+ ("python", "# "),
+ ("sql", "-- "),
+ # ("mysql", "# "),
+ # ("c", "// "),
+ ],
+)
+@pytest.mark.asyncio
+async def test_comments(app: App, language: str, expected_marker: str) -> None:
+ async with app.run_test() as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.language = language
+ original_text = "foo bar baz"
+ ta.text = original_text
+ ta.selection = Selection((0, 0), (0, 0))
+
+ await pilot.press("ctrl+underscore") # alias for ctrl+/
+ assert ta.text == f"{expected_marker}{original_text}"
+
+ await pilot.press("ctrl+underscore") # alias for ctrl+/
+ assert ta.text == f"{original_text}"
diff --git a/tests/functional_tests/test_find.py b/tests/functional_tests/test_find.py
new file mode 100644
index 0000000..10454a3
--- /dev/null
+++ b/tests/functional_tests/test_find.py
@@ -0,0 +1,143 @@
+import pytest
+from textual.app import App
+from textual_textarea import TextEditor
+from textual_textarea.find_input import FindInput
+
+
+@pytest.mark.asyncio
+async def test_find(app: App) -> None:
+ async with app.run_test() as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.text = "foo bar\n" * 50
+ await pilot.pause()
+ assert ta.selection.start == ta.selection.end == (0, 0)
+ await pilot.press("ctrl+f")
+
+ find_input = app.query_one(FindInput)
+ assert find_input
+ assert find_input.has_focus
+ assert "Find" in find_input.placeholder
+
+ await pilot.press("b")
+ assert find_input.has_focus
+ assert ta.selection.start == (0, 4)
+ assert ta.selection.end == (0, 5)
+
+ await pilot.press("a")
+ assert find_input.has_focus
+ assert ta.selection.start == (0, 4)
+ assert ta.selection.end == (0, 6)
+
+ await pilot.press("enter")
+ assert find_input.has_focus
+ assert ta.selection.start == (1, 4)
+ assert ta.selection.end == (1, 6)
+
+ await pilot.press("escape")
+ assert ta.text_input
+ assert ta.text_input.has_focus
+ assert ta.selection.start == (1, 4)
+ assert ta.selection.end == (1, 6)
+
+ await pilot.press("ctrl+f")
+
+ find_input = app.query_one(FindInput)
+ await pilot.press("f")
+ assert find_input.has_focus
+ assert ta.selection.start == (2, 0)
+ assert ta.selection.end == (2, 1)
+
+
+@pytest.mark.asyncio
+async def test_find_history(app: App) -> None:
+ async with app.run_test() as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.text = "foo bar\n" * 50
+ await pilot.pause()
+
+ # add an item to the history by pressing enter
+ await pilot.press("ctrl+f")
+ await pilot.press("a")
+ await pilot.press("enter")
+ await pilot.press("escape")
+
+ # re-open the find input and navigate the one-item
+ # history
+ await pilot.press("ctrl+f")
+ find_input = app.query_one(FindInput)
+ assert find_input.value == ""
+ await pilot.press("up")
+ assert find_input.value == "a"
+ await pilot.press("down")
+ assert find_input.value == ""
+ await pilot.press("up")
+ await pilot.press("up")
+ assert find_input.value == "a"
+ await pilot.press("down")
+ assert find_input.value == ""
+
+ # add an item to the history by closing the find input
+ await pilot.press("b")
+ await pilot.press("escape")
+
+ # navigate the two-item history
+ await pilot.press("ctrl+f")
+ find_input = app.query_one(FindInput)
+ assert find_input.value == ""
+ await pilot.press("up")
+ assert find_input.value == "b"
+ await pilot.press("down")
+ assert find_input.value == ""
+ await pilot.press("up")
+ assert find_input.value == "b"
+ await pilot.press("up")
+ assert find_input.value == "a"
+ await pilot.press("up")
+ assert find_input.value == "a"
+ await pilot.press("down")
+ assert find_input.value == "b"
+ await pilot.press("down")
+ assert find_input.value == ""
+
+
+@pytest.mark.asyncio
+async def test_find_with_f3(app: App) -> None:
+ async with app.run_test() as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.text = "foo bar\n" * 50
+ await pilot.pause()
+ assert ta.selection.start == ta.selection.end == (0, 0)
+
+ # pressing f3 with no history brings up an empty find box
+ await pilot.press("f3")
+ find_input = app.query_one(FindInput)
+ assert find_input
+ assert find_input.has_focus
+ assert find_input.value == ""
+
+ await pilot.press("b")
+ assert find_input.has_focus
+ assert ta.selection.start == (0, 4)
+ assert ta.selection.end == (0, 5)
+
+ # pressing f3 from the find input finds the next match
+ await pilot.press("f3")
+ assert find_input.has_focus
+ assert ta.selection.start == (1, 4)
+ assert ta.selection.end == (1, 5)
+
+ # close the find input and navigate up one line
+ await pilot.press("escape")
+ await pilot.press("up")
+
+ # pressing f3 with history prepopulates the find input
+ await pilot.press("f3")
+ find_input = app.query_one(FindInput)
+ assert find_input.value == "b"
+ assert ta.selection.start == (1, 4)
+ assert ta.selection.end == (1, 5)
+
+ # pressing again advances to the next match
+ await pilot.press("f3")
+ assert ta.selection.start == (2, 4)
+ assert ta.selection.end == (2, 5)
diff --git a/tests/functional_tests/test_goto.py b/tests/functional_tests/test_goto.py
new file mode 100644
index 0000000..8add0bc
--- /dev/null
+++ b/tests/functional_tests/test_goto.py
@@ -0,0 +1,36 @@
+import pytest
+from textual.app import App
+from textual_textarea import TextEditor
+from textual_textarea.goto_input import GotoLineInput
+
+
+@pytest.mark.asyncio
+async def test_goto_line(app: App) -> None:
+ async with app.run_test() as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.text = "\n" * 50
+ await pilot.pause()
+ assert ta.selection.start == ta.selection.end == (0, 0)
+ await pilot.press("ctrl+g")
+
+ goto_input = app.query_one(GotoLineInput)
+ assert goto_input
+ assert goto_input.has_focus
+ assert "51" in goto_input.placeholder
+
+ await pilot.press("1")
+ await pilot.press("2")
+ await pilot.press("enter")
+
+ assert ta.text_input
+ assert ta.text_input.has_focus
+ assert ta.selection.start == ta.selection.end == (11, 0)
+
+ # ensure pressing ctrl+g twice doesn't crash
+
+ await pilot.press("ctrl+g")
+ goto_input = app.query_one(GotoLineInput)
+ assert goto_input.has_focus
+ await pilot.press("2")
+
+ await pilot.press("ctrl+g")
diff --git a/tests/functional_tests/test_open.py b/tests/functional_tests/test_open.py
new file mode 100644
index 0000000..2484756
--- /dev/null
+++ b/tests/functional_tests/test_open.py
@@ -0,0 +1,70 @@
+from pathlib import Path
+from typing import List
+
+import pytest
+from textual.app import App
+from textual.message import Message
+from textual.widgets import Input
+from textual_textarea import TextAreaSaved, TextEditor
+
+
+@pytest.mark.parametrize("filename", ["foo.py", "empty.py"])
+@pytest.mark.asyncio
+async def test_open(data_dir: Path, app: App, filename: str) -> None:
+ p = data_dir / "test_open" / filename
+ with open(p, "r") as f:
+ contents = f.read()
+
+ async with app.run_test() as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ assert ta.text == ""
+ starting_text = "123"
+ for key in starting_text:
+ await pilot.press(key)
+ assert ta.text == starting_text
+
+ await pilot.press("ctrl+o")
+ open_input = ta.query_one(Input)
+ assert open_input.id and "open" in open_input.id
+ assert open_input.has_focus
+
+ for key in str(p):
+ await pilot.press(key)
+ await pilot.press("enter")
+
+ assert ta.text == contents
+ assert ta.text_input is not None
+ assert ta.text_input.has_focus
+
+ # make sure the end of the buffer is formatted properly.
+ # these previously caused a crash.
+ await pilot.press("ctrl+end")
+ assert ta.selection.end[1] >= 0
+ await pilot.press("enter")
+
+
+@pytest.mark.asyncio
+async def test_save(app: App, tmp_path: Path) -> None:
+ TEXT = "select\n 1 as a,\n 2 as b,\n 'c' as c"
+ p = tmp_path / "text.sql"
+ print(p)
+ messages: List[Message] = []
+ async with app.run_test(message_hook=messages.append) as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.text = TEXT
+
+ await pilot.press("ctrl+s")
+ save_input = ta.query_one(Input)
+ assert save_input.id and "save" in save_input.id
+ assert save_input.has_focus
+
+ save_input.value = str(p)
+ await pilot.press("enter")
+ await pilot.pause()
+ assert len(messages) > 1
+ assert Input.Submitted in [msg.__class__ for msg in messages]
+ assert TextAreaSaved in [msg.__class__ for msg in messages]
+
+ with open(p, "r") as f:
+ saved_text = f.read()
+ assert saved_text == TEXT
diff --git a/tests/functional_tests/test_textarea.py b/tests/functional_tests/test_textarea.py
new file mode 100644
index 0000000..951b0bd
--- /dev/null
+++ b/tests/functional_tests/test_textarea.py
@@ -0,0 +1,462 @@
+from __future__ import annotations
+
+from typing import List
+
+import pytest
+from textual.app import App
+from textual.widgets.text_area import Selection
+from textual_textarea import TextEditor
+
+
+@pytest.mark.parametrize(
+ "keys,text,selection,expected_text,expected_selection",
+ [
+ (
+ ["ctrl+a"],
+ "select\n foo",
+ Selection(start=(1, 2), end=(1, 2)),
+ "select\n foo",
+ Selection(start=(0, 0), end=(1, 4)),
+ ),
+ (
+ ["ctrl+shift+right"],
+ "select\n foo",
+ Selection(start=(0, 0), end=(0, 0)),
+ "select\n foo",
+ Selection(start=(0, 0), end=(0, 6)),
+ ),
+ (
+ ["right"],
+ "select\n foo",
+ Selection(start=(0, 0), end=(0, 6)),
+ "select\n foo",
+ Selection(start=(1, 0), end=(1, 0)),
+ ),
+ (
+ ["a"],
+ "select\n foo",
+ Selection(start=(1, 4), end=(1, 4)),
+ "select\n fooa",
+ Selection(start=(1, 5), end=(1, 5)),
+ ),
+ (
+ ["a"],
+ "select\n foo",
+ Selection(start=(1, 0), end=(1, 4)),
+ "select\na",
+ Selection(start=(1, 1), end=(1, 1)),
+ ),
+ (
+ ["enter"],
+ "a\na",
+ Selection(start=(1, 0), end=(1, 0)),
+ "a\n\na",
+ Selection(start=(2, 0), end=(2, 0)),
+ ),
+ (
+ ["enter"],
+ "a\na",
+ Selection(start=(1, 1), end=(1, 1)),
+ "a\na\n",
+ Selection(start=(2, 0), end=(2, 0)),
+ ),
+ (
+ ["enter", "b"],
+ "a()",
+ Selection(start=(0, 2), end=(0, 2)),
+ "a(\n b\n)",
+ Selection(start=(1, 5), end=(1, 5)),
+ ),
+ (
+ ["enter", "b"],
+ " a()",
+ Selection(start=(0, 3), end=(0, 3)),
+ " a(\n b\n )",
+ Selection(start=(1, 5), end=(1, 5)),
+ ),
+ (
+ ["delete"],
+ "0\n1\n2\n3",
+ Selection(start=(2, 1), end=(2, 1)),
+ "0\n1\n23",
+ Selection(start=(2, 1), end=(2, 1)),
+ ),
+ (
+ ["shift+delete"],
+ "0\n1\n2\n3",
+ Selection(start=(2, 1), end=(2, 1)),
+ "0\n1\n3",
+ Selection(start=(2, 0), end=(2, 0)),
+ ),
+ (
+ ["shift+delete"],
+ "0\n1\n2\n3",
+ Selection(start=(2, 0), end=(2, 1)),
+ "0\n1\n\n3",
+ Selection(start=(2, 0), end=(2, 0)),
+ ),
+ (
+ ["shift+delete"],
+ "0\n1\n2\n3",
+ Selection(start=(3, 1), end=(3, 1)),
+ "0\n1\n2",
+ Selection(start=(2, 1), end=(2, 1)),
+ ),
+ (
+ ["shift+delete"],
+ "foo",
+ Selection(start=(3, 1), end=(3, 1)),
+ "",
+ Selection(start=(0, 0), end=(0, 0)),
+ ),
+ (
+ ["ctrl+home"],
+ "foo\nbar",
+ Selection(start=(1, 2), end=(1, 2)),
+ "foo\nbar",
+ Selection(start=(0, 0), end=(0, 0)),
+ ),
+ (
+ ["ctrl+end"],
+ "foo\nbar",
+ Selection(start=(0, 1), end=(0, 1)),
+ "foo\nbar",
+ Selection(start=(1, 3), end=(1, 3)),
+ ),
+ (
+ ["("],
+ "foo",
+ Selection(start=(0, 3), end=(0, 3)),
+ "foo()",
+ Selection(start=(0, 4), end=(0, 4)),
+ ),
+ (
+ ["("],
+ "foo",
+ Selection(start=(0, 2), end=(0, 2)),
+ "fo(o",
+ Selection(start=(0, 3), end=(0, 3)),
+ ),
+ (
+ ["("],
+ "foo.",
+ Selection(start=(0, 3), end=(0, 3)),
+ "foo().",
+ Selection(start=(0, 4), end=(0, 4)),
+ ),
+ (
+ ["("],
+ "foo-",
+ Selection(start=(0, 3), end=(0, 3)),
+ "foo(-",
+ Selection(start=(0, 4), end=(0, 4)),
+ ),
+ (
+ ["'"],
+ "foo",
+ Selection(start=(0, 3), end=(0, 3)),
+ "foo'",
+ Selection(start=(0, 4), end=(0, 4)),
+ ),
+ (
+ ["'"],
+ "ba r",
+ Selection(start=(0, 3), end=(0, 3)),
+ "ba '' r",
+ Selection(start=(0, 4), end=(0, 4)),
+ ),
+ (
+ ["'"],
+ "foo-",
+ Selection(start=(0, 3), end=(0, 3)),
+ "foo'-",
+ Selection(start=(0, 4), end=(0, 4)),
+ ),
+ (
+ ["'"],
+ "fo--",
+ Selection(start=(0, 3), end=(0, 3)),
+ "fo-'-",
+ Selection(start=(0, 4), end=(0, 4)),
+ ),
+ (
+ ["'"],
+ "fo-.",
+ Selection(start=(0, 3), end=(0, 3)),
+ "fo-''.",
+ Selection(start=(0, 4), end=(0, 4)),
+ ),
+ (
+ ["'"],
+ "fo()",
+ Selection(start=(0, 3), end=(0, 3)),
+ "fo('')",
+ Selection(start=(0, 4), end=(0, 4)),
+ ),
+ (
+ ["tab"],
+ "bar",
+ Selection(start=(0, 1), end=(0, 1)),
+ "b ar",
+ Selection(start=(0, 4), end=(0, 4)),
+ ),
+ (
+ ["tab"],
+ "bar",
+ Selection(start=(0, 0), end=(0, 0)),
+ " bar",
+ Selection(start=(0, 4), end=(0, 4)),
+ ),
+ (
+ ["shift+tab"],
+ "bar",
+ Selection(start=(0, 0), end=(0, 0)),
+ "bar",
+ Selection(start=(0, 0), end=(0, 0)),
+ ),
+ (
+ ["shift+tab"],
+ " bar",
+ Selection(start=(0, 7), end=(0, 7)),
+ "bar",
+ Selection(start=(0, 3), end=(0, 3)),
+ ),
+ (
+ ["tab"],
+ "bar\n baz",
+ Selection(start=(0, 2), end=(1, 1)),
+ " bar\n baz",
+ Selection(start=(0, 6), end=(1, 4)),
+ ),
+ (
+ ["tab"],
+ "bar\n baz",
+ Selection(start=(0, 0), end=(1, 1)),
+ " bar\n baz",
+ Selection(start=(0, 0), end=(1, 4)),
+ ),
+ (
+ ["shift+tab"],
+ " bar\n baz",
+ Selection(start=(0, 0), end=(1, 1)),
+ "bar\nbaz",
+ Selection(start=(0, 0), end=(1, 0)),
+ ),
+ ],
+)
+@pytest.mark.asyncio
+async def test_keys(
+ app: App,
+ keys: List[str],
+ text: str,
+ selection: Selection,
+ expected_text: str,
+ expected_selection: Selection,
+) -> None:
+ async with app.run_test() as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.text = text
+ ta.selection = selection
+
+ for key in keys:
+ await pilot.press(key)
+
+ assert ta.text == expected_text
+ assert ta.selection == expected_selection
+
+
+@pytest.mark.parametrize(
+ "starting_selection,expected_clipboard,expected_paste_loc",
+ [
+ (Selection((0, 5), (1, 5)), "56789\n01234", (1, 5)),
+ (Selection((0, 0), (1, 0)), "0123456789\n", (1, 0)),
+ ],
+)
+@pytest.mark.asyncio
+async def test_copy_paste(
+ app_all_clipboards: App,
+ starting_selection: Selection,
+ expected_clipboard: str,
+ expected_paste_loc: tuple[int, int],
+) -> None:
+ original_text = "0123456789\n0123456789\n0123456789"
+
+ def eq(a: str, b: str) -> bool:
+ return a.replace("\r\n", "\n") == b.replace("\r\n", "\n")
+
+ async with app_all_clipboards.run_test() as pilot:
+ ta = app_all_clipboards.query_one("#ta", expect_type=TextEditor)
+ while ta.text_input is None:
+ await pilot.pause(0.1)
+ ti = ta.text_input
+ assert ti is not None
+ ta.text = original_text
+ ta.selection = starting_selection
+
+ await pilot.press("ctrl+c")
+ await pilot.pause()
+ assert eq(ti.clipboard, expected_clipboard)
+ assert ta.selection == starting_selection
+ assert ta.text == original_text
+
+ await pilot.press("ctrl+u")
+ await pilot.pause()
+ assert eq(ti.clipboard, expected_clipboard)
+ assert ta.selection == Selection(starting_selection.end, starting_selection.end)
+ assert ta.text == original_text
+
+ await pilot.press("ctrl+a")
+ assert ta.selection == Selection(
+ (0, 0),
+ (len(original_text.splitlines()) - 1, len(original_text.splitlines()[-1])),
+ )
+ assert eq(ti.clipboard, expected_clipboard)
+ assert ta.text == original_text
+
+ await pilot.press("ctrl+u")
+ await pilot.pause()
+ assert ta.selection == Selection(expected_paste_loc, expected_paste_loc)
+ assert eq(ti.clipboard, expected_clipboard)
+ assert ta.text == expected_clipboard
+
+ await pilot.press("ctrl+a")
+ await pilot.press("ctrl+x")
+ await pilot.pause()
+ assert ta.selection == Selection((0, 0), (0, 0))
+ assert eq(ti.clipboard, expected_clipboard)
+ assert ta.text == ""
+
+ await pilot.press("ctrl+v")
+ await pilot.pause()
+ assert eq(ti.clipboard, expected_clipboard)
+ assert ta.text == expected_clipboard
+ assert ta.selection == Selection(expected_paste_loc, expected_paste_loc)
+
+
+@pytest.mark.asyncio
+async def test_undo_redo(app: App) -> None:
+ async with app.run_test() as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ti = ta.text_input
+ assert ti
+ assert ti.has_focus
+
+ for char in "foo":
+ await pilot.press(char)
+ await pilot.pause(0.6)
+
+ await pilot.press("enter")
+ for char in "bar":
+ await pilot.press(char)
+ await pilot.pause(0.6)
+
+ await pilot.press("ctrl+z")
+ assert ta.text == "foo\n"
+ assert ta.selection == Selection((1, 0), (1, 0))
+
+ await pilot.press("ctrl+z")
+ assert ta.text == "foo"
+ assert ta.selection == Selection((0, 3), (0, 3))
+
+ await pilot.press("ctrl+z")
+ assert ta.text == ""
+ assert ta.selection == Selection((0, 0), (0, 0))
+
+ await pilot.press("ctrl+y")
+ assert ta.text == "foo"
+ assert ta.selection == Selection((0, 3), (0, 3))
+
+ await pilot.press("z")
+ assert ta.text == "fooz"
+
+
+@pytest.mark.parametrize(
+ "start_text,insert_text,selection,expected_text",
+ [
+ (
+ "select ",
+ '"main"."drivers"."driverId"',
+ Selection((0, 7), (0, 7)),
+ 'select "main"."drivers"."driverId"',
+ ),
+ (
+ "select , foo",
+ '"main"."drivers"."driverId"',
+ Selection((0, 7), (0, 7)),
+ 'select "main"."drivers"."driverId", foo',
+ ),
+ (
+ "aaa\naaa\naaa\naaa",
+ "bb",
+ Selection((2, 2), (2, 2)),
+ "aaa\naaa\naabba\naaa",
+ ),
+ (
+ "aaa\naaa\naaa\naaa",
+ "bb",
+ Selection((2, 2), (1, 1)),
+ "aaa\nabba\naaa",
+ ),
+ (
+ "01234",
+ "\nabc\n",
+ Selection((0, 2), (0, 2)),
+ "01\nabc\n234",
+ ),
+ ],
+)
+@pytest.mark.asyncio
+async def test_insert_text(
+ app: App,
+ start_text: str,
+ insert_text: str,
+ selection: Selection,
+ expected_text: str,
+) -> None:
+ async with app.run_test() as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.text = start_text
+ ta.selection = selection
+ await pilot.pause()
+
+ ta.insert_text_at_selection(insert_text)
+ await pilot.pause()
+
+ assert ta.text == expected_text
+
+
+@pytest.mark.asyncio
+async def test_toggle_comment(app: App) -> None:
+ async with app.run_test() as pilot:
+ ta = app.query_one("#ta", expect_type=TextEditor)
+ ta.text = "one\ntwo\n\nthree"
+ ta.selection = Selection((0, 0), (0, 0))
+ await pilot.pause()
+
+ await pilot.press("ctrl+underscore")
+ assert ta.text == "# one\ntwo\n\nthree"
+
+ await pilot.press("down")
+ await pilot.press("ctrl+underscore")
+ assert ta.text == "# one\n# two\n\nthree"
+
+ await pilot.press("ctrl+a")
+ await pilot.press("ctrl+underscore")
+ assert ta.text == "# # one\n# # two\n\n# three"
+
+ await pilot.press("ctrl+underscore")
+ assert ta.text == "# one\n# two\n\nthree"
+
+ await pilot.press("up")
+ await pilot.press("up")
+ await pilot.press("ctrl+underscore")
+ assert ta.text == "# one\ntwo\n\nthree"
+
+ await pilot.press("shift+down")
+ await pilot.press("shift+down")
+ await pilot.press("ctrl+underscore")
+ assert ta.text == "# one\n# two\n\n# three"
+
+ await pilot.press("ctrl+a")
+ await pilot.press("ctrl+underscore")
+ assert ta.text == "one\ntwo\n\nthree"
diff --git a/tests/unit_tests/test_path_validator.py b/tests/unit_tests/test_path_validator.py
new file mode 100644
index 0000000..1f652de
--- /dev/null
+++ b/tests/unit_tests/test_path_validator.py
@@ -0,0 +1,84 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+from textual_textarea.path_input import PathValidator, path_completer
+
+
+@pytest.mark.parametrize(
+ "relpath,expected_matches",
+ [
+ ("", ["foo", "bar"]),
+ ("f", ["foo"]),
+ ("fo", ["foo"]),
+ ("foo", ["baz.txt"]),
+ ("foo/", ["baz.txt"]),
+ ("b", ["bar"]),
+ ("c", []),
+ ],
+)
+def test_path_completer(
+ data_dir: Path,
+ relpath: str,
+ expected_matches: list[str],
+) -> None:
+ test_path = data_dir / "test_validator" / relpath
+ test_dir = test_path if test_path.is_dir() else test_path.parent
+ prefix = str(test_path)
+ print(prefix)
+ matches = path_completer(prefix)
+ assert sorted(matches) == sorted(
+ [(str(test_dir / m), str(test_dir / m)) for m in expected_matches]
+ )
+
+
+@pytest.mark.parametrize(
+ "relpath,dir_okay,file_okay,must_exist,expected_result",
+ [
+ ("foo", True, True, True, True),
+ ("foo", True, True, False, True),
+ ("foo", True, False, True, True),
+ ("foo", True, False, False, True),
+ ("foo", False, True, True, False),
+ ("foo", False, True, False, False),
+ ("foo", False, False, True, False),
+ ("foo", False, False, False, False),
+ ("bar", True, True, True, True),
+ ("bar", True, True, False, True),
+ ("bar", True, False, True, True),
+ ("bar", True, False, False, True),
+ ("bar", False, True, True, False),
+ ("bar", False, True, False, False),
+ ("bar", False, False, True, False),
+ ("bar", False, False, False, False),
+ ("baz", True, True, True, False),
+ ("baz", True, True, False, True),
+ ("baz", True, False, True, False),
+ ("baz", True, False, False, True),
+ ("baz", False, True, True, False),
+ ("baz", False, True, False, True),
+ ("baz", False, False, True, False),
+ ("baz", False, False, False, True),
+ ("foo/baz.txt", True, True, True, True),
+ ("foo/baz.txt", True, True, False, True),
+ ("foo/baz.txt", True, False, True, False),
+ ("foo/baz.txt", True, False, False, False),
+ ("foo/baz.txt", False, True, True, True),
+ ("foo/baz.txt", False, True, False, True),
+ ("foo/baz.txt", False, False, True, False),
+ ("foo/baz.txt", False, False, False, False),
+ ],
+)
+def test_path_validator(
+ data_dir: Path,
+ relpath: str,
+ dir_okay: bool,
+ file_okay: bool,
+ must_exist: bool,
+ expected_result: bool,
+) -> None:
+ p = data_dir / "test_validator" / relpath
+ validator = PathValidator(dir_okay, file_okay, must_exist)
+ result = validator.validate(str(p))
+ assert result.is_valid == expected_result
diff --git a/textarea.svg b/textarea.svg
new file mode 100644
index 0000000..2d79719
--- /dev/null
+++ b/textarea.svg
@@ -0,0 +1,157 @@
+