Skip to content

Commit 3abc8ee

Browse files
Text area fixes (#4157)
* Initial undo related machinery added to TextArea * Initial undo implementation * Basic undo and redo * Some more fleshing out of undo and redo * Skeleton code for managing TextArea history * Initial implementation of undo & redo checkpointing in TextArea * Increase checkpoint characters * Restoring the selection in the TextArea and then restoring it on undo * Adding docstrings to undo_batch and redo_batch in the TextArea * Batching edits of the same type * Batching edits of the same type * Keeping edits containing newlines in their own batch * Checking for newline characters in insertion or replacement during undo checkpoint creation. Updating docstrings in history.py * Fix mypy warning * Performance improvement * Add history checkpoint on cursor movement * Fixing merge conflict in Edit class * Fixing error in merge conflict resolution * Remove unused test file * Remove unused test file * Initial testing of undo and redo * Testing for undo redo * Updating lockfile * Add an extra test * Fix: setting the `text` property programmatically should invalidate the edit history * Improving docstrings * Rename EditHistory.reset() to EditHistory.clear() * Add docstring to an exception * Add a pause after focus/blur in a test * Forcing CI colour * Update focus checkpoint test * Try to force color in pytest by setting --color=yes in PYTEST_ADDOPTS in env var on Github Actions * Add extra assertion in a test * Toggle text_area has focus to trigger checkpoint in history * Apply grammar/wording suggestions from code review Co-authored-by: Rodrigo Girão Serrão <[email protected]> * Making max checkpoints configurable in TextArea history * Improve a docstring * Update changelog * Spelling fixes * More spelling fixes * Americanize spelling of tab_behaviour (->tab_behavior) * Update CHANGELOG regarding `tab_behaviour`->`tab_behavior` * Various fixes * Various fixes and improvements * Updating tests to account for themes always being non-None * Update CHANGELOG. * Add TextArea.read_only to reactive attr table in TextArea docs * Update TextArea docs regarding new features * Cleaning up some typing issues * Add actions for undo and redo * Fix a typo * Fix wording in docs/widgets/text_area.md Co-authored-by: Rodrigo Girão Serrão <[email protected]> * Re-add return type hint * PR feedback and fixing typos * Mark breaking change in CHANGELOG * Add undo/redo to docstring * Add note on undo/redo bindings --------- Co-authored-by: Rodrigo Girão Serrão <[email protected]>
1 parent b13a215 commit 3abc8ee

File tree

5 files changed

+95
-93
lines changed

5 files changed

+95
-93
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1616

1717
### Changed
1818

19-
- Renamed `TextArea.tab_behaviour` to `TextArea.tab_behavior` https://github.com/Textualize/textual/pull/4124
19+
- Breaking change: Renamed `TextArea.tab_behaviour` to `TextArea.tab_behavior` https://github.com/Textualize/textual/pull/4124
20+
- `TextArea.theme` now defaults to `"css"` instead of None, and is no longer optional https://github.com/Textualize/textual/pull/4157
2021

2122
## [0.50.1] - 2024-02-09
2223

docs/widgets/text_area.md

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
21
# TextArea
32

4-
!!! tip
3+
!!! tip
54

65
Added in version 0.38.0. Soft wrapping added in version 0.48.0.
76

@@ -68,7 +67,6 @@ text_area.language = "markdown"
6867
!!! note
6968
More built-in languages will be added in the future. For now, you can [add your own](#adding-support-for-custom-languages).
7069

71-
7270
### Reading content from `TextArea`
7371

7472
There are a number of ways to retrieve content from the `TextArea`:
@@ -136,7 +134,7 @@ There are a number of additional utility methods available for interacting with
136134

137135
##### Location information
138136

139-
A number of properties exist on `TextArea` which give information about the current cursor location.
137+
Many properties exist on `TextArea` which give information about the current cursor location.
140138
These properties begin with `cursor_at_`, and return booleans.
141139
For example, [`cursor_at_start_of_line`][textual.widgets._text_area.TextArea.cursor_at_start_of_line] tells us if the cursor is at a start of line.
142140

@@ -175,12 +173,14 @@ the cursor, selection, gutter, and more.
175173

176174
#### Default theme
177175

178-
The default `TextArea` theme is called `css`.
179-
This a theme which takes values entirely from CSS.
176+
The default `TextArea` theme is called `css`, which takes it's values entirely from CSS.
180177
This means that the default appearance of the widget fits nicely into a standard Textual application,
181178
and looks right on both dark and light mode.
182179

183-
More complex applications such as code editors will likely want to use pre-defined themes such as `monokai`.
180+
When using the `css` theme, you can make use of [component classes][textual.widgets.TextArea.COMPONENT_CLASSES] to style elements of the `TextArea`.
181+
For example, the CSS code `TextArea .text-area--cursor { background: green; }` will make the cursor `green`.
182+
183+
More complex applications such as code editors may want to use pre-defined themes such as `monokai`.
184184
This involves using a `TextAreaTheme` object, which we cover in detail below.
185185
This allows full customization of the `TextArea`, including syntax highlighting, at the code level.
186186

@@ -190,15 +190,15 @@ The initial theme of the `TextArea` is determined by the `theme` parameter.
190190

191191
```python
192192
# Create a TextArea with the 'dracula' theme.
193-
yield TextArea("print(123)", language="python", theme="dracula")
193+
yield TextArea.code_editor("print(123)", language="python", theme="dracula")
194194
```
195195

196196
You can check which themes are available using the [`available_themes`][textual.widgets._text_area.TextArea.available_themes] property.
197197

198198
```python
199199
>>> text_area = TextArea()
200200
>>> print(text_area.available_themes)
201-
{'dracula', 'github_light', 'monokai', 'vscode_dark'}
201+
{'css', 'dracula', 'github_light', 'monokai', 'vscode_dark'}
202202
```
203203

204204
After creating a `TextArea`, you can change the theme by setting the [`theme`][textual.widgets._text_area.TextArea.theme]
@@ -212,7 +212,12 @@ On setting this attribute the `TextArea` will immediately refresh to display the
212212

213213
#### Custom themes
214214

215-
Using custom (non-builtin) themes is two-step process:
215+
!!! note
216+
217+
Custom themes are only relevant for people who are looking to customize syntax highlighting.
218+
If you're only editing plain text, and wish to recolor aspects of the `TextArea`, you should use the [provided component classes][textual.widgets.TextArea.COMPONENT_CLASSES].
219+
220+
Using custom (non-builtin) themes is a two-step process:
216221

217222
1. Create an instance of [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme].
218223
2. Register it using [`TextArea.register_theme`][textual.widgets._text_area.TextArea.register_theme].
@@ -297,6 +302,27 @@ The character(s) inserted when you press tab is controlled by setting the `inden
297302

298303
If `indent_type == "spaces"`, pressing ++tab++ will insert up to `indent_width` spaces in order to align with the next tab stop.
299304

305+
### Undo and redo
306+
307+
`TextArea` offers `undo` and `redo` methods.
308+
By default, `undo` is bound to <kbd>Ctrl</kbd>+<kbd>Z</kbd> and `redo` to <kbd>Ctrl</kbd>+<kbd>Y</kbd>.
309+
310+
The `TextArea` uses a heuristic to place _checkpoints_ after certain types of edit.
311+
When you call `undo`, all of the edits between now and the most recent checkpoint are reverted.
312+
You can manually add a checkpoint by calling the [`TextArea.history.checkpoint()`][textual.widgets.text_area.EditHistory.checkpoint] instance method.
313+
314+
The undo and redo history uses a stack-based system, where a single item on the stack represents a single checkpoint.
315+
In memory-constrained environments, you may wish to reduce the maximum number of checkpoints that can exist.
316+
You can do this by passing the `max_checkpoints` argument to the `TextArea` constructor.
317+
318+
### Read-only mode
319+
320+
`TextArea.read_only` is a boolean reactive attribute which, if `True`, will prevent users from modifying content in the `TextArea`.
321+
322+
While `read_only=True`, you can still modify the content programmatically.
323+
324+
While this mode is active, the `TextArea` receives the `-read-only` CSS class, which you can use to supply custom styles for read-only mode.
325+
300326
### Line separators
301327

302328
When content is loaded into `TextArea`, the content is scanned from beginning to end
@@ -459,7 +485,6 @@ If you notice some highlights are missing after registering a language, the issu
459485
The names assigned in tree-sitter highlight queries are often reused across multiple languages.
460486
For example, `@string` is used in many languages to highlight strings.
461487

462-
463488
#### Navigation and wrapping information
464489

465490
If you're building functionality on top of `TextArea`, it may be useful to inspect the `navigator` and `wrapped_document` attributes.
@@ -473,14 +498,15 @@ A detailed view of these classes is out of scope, but do note that a lot of the
473498

474499
| Name | Type | Default | Description |
475500
|------------------------|--------------------------|---------------|------------------------------------------------------------------|
476-
| `language` | `str | None` | `None` | The language to use for syntax highlighting. |
477-
| `theme` | `str | None` | `TextAreaTheme.default()` | The theme to use for syntax highlighting. |
501+
| `language` | `str | None` | `None` | The language to use for syntax highlighting. |
502+
| `theme` | `str` | `"css"` | The theme to use. |
478503
| `selection` | `Selection` | `Selection()` | The current selection. |
479504
| `show_line_numbers` | `bool` | `False` | Show or hide line numbers. |
480505
| `indent_width` | `int` | `4` | The number of spaces to indent and width of tabs. |
481506
| `match_cursor_bracket` | `bool` | `True` | Enable/disable highlighting matching brackets under cursor. |
482507
| `cursor_blink` | `bool` | `True` | Enable/disable blinking of the cursor when the widget has focus. |
483508
| `soft_wrap` | `bool` | `True` | Enable/disable soft wrapping. |
509+
| `read_only` | `bool` | `False` | Enable/disable read-only mode. |
484510

485511
## Messages
486512

@@ -496,7 +522,6 @@ The `TextArea` widget defines the following bindings:
496522
show_root_heading: false
497523
show_root_toc_entry: false
498524

499-
500525
## Component classes
501526

502527
The `TextArea` defines component classes that can style various aspects of the widget.
@@ -513,11 +538,11 @@ Styles from the `theme` attribute take priority.
513538
- [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme] - theming the `TextArea`
514539
- [`DocumentNavigator`][textual.widgets.text_area.DocumentNavigator] - guides cursor movement
515540
- [`WrappedDocument`][textual.widgets.text_area.WrappedDocument] - manages wrapping the document
541+
- [`EditHistory`][textual.widgets.text_area.EditHistory] - manages the undo stack
516542
- The tree-sitter documentation [website](https://tree-sitter.github.io/tree-sitter/).
517543
- The tree-sitter Python bindings [repository](https://github.com/tree-sitter/py-tree-sitter).
518544
- `py-tree-sitter-languages` [repository](https://github.com/grantjenks/py-tree-sitter-languages) (provides binary wheels for a large variety of tree-sitter languages).
519545

520-
521546
## Additional notes
522547

523548
- To remove the outline effect when the `TextArea` is focused, you can set `border: none; padding: 0;` in your CSS.

src/textual/_text_area_theme.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,6 @@ def apply_css(self, text_area: TextArea) -> None:
119119
if self.cursor_line_gutter_style is None:
120120
self.cursor_line_gutter_style = get_style("text-area--cursor-gutter")
121121

122-
if self.cursor_line_gutter_style is None and self.cursor_line_style is not None:
123-
self.cursor_line_gutter_style = self.cursor_line_style.copy()
124-
125122
if self.bracket_matching_style is None:
126123
matching_bracket_style = get_style("text-area--matching-bracket")
127124
if matching_bracket_style:
@@ -182,15 +179,6 @@ def builtin_themes(cls) -> list[TextAreaTheme]:
182179
"""
183180
return list(_BUILTIN_THEMES.values())
184181

185-
@classmethod
186-
def default(cls) -> TextAreaTheme:
187-
"""Get the default syntax theme.
188-
189-
Returns:
190-
The default TextAreaTheme (probably "css").
191-
"""
192-
return _CSS_THEME
193-
194182

195183
_MONOKAI = TextAreaTheme(
196184
name="monokai",
@@ -388,6 +376,3 @@ def default(cls) -> TextAreaTheme:
388376
"vscode_dark": _DARK_VS,
389377
"github_light": _GITHUB_LIGHT,
390378
}
391-
392-
DEFAULT_THEME = TextAreaTheme.get_builtin_theme("basic")
393-
"""The default TextAreaTheme used by Textual."""

0 commit comments

Comments
 (0)