diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1643a05850..84b2740a7d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,18 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+
## Unreleased
+- Fixed `Button` not rendering correctly with console markup https://github.com/Textualize/textual/issues/4328
+
+
+## [0.54.0] - 2023-03-26
+
### Fixed
- Fixed a crash in `TextArea` when undoing an edit to a selection the selection was made backwards https://github.com/Textualize/textual/issues/4301
- Fixed issue with flickering scrollbars https://github.com/Textualize/textual/pull/4315
+- Fixed issue where narrow TextArea would repeatedly wrap due to scrollbar appearing/disappearing https://github.com/Textualize/textual/pull/4334
- Fix progress bar ETA not updating when setting `total` reactive https://github.com/Textualize/textual/pull/4316
-- Fixed `Button` not rendering correctly with console markup https://github.com/Textualize/textual/issues/4328
+- Exceptions inside `Widget.compose` or workers weren't bubbling up in tests https://github.com/Textualize/textual/issues/4282
### Changed
- ProgressBar won't show ETA until there is at least one second of samples https://github.com/Textualize/textual/pull/4316
+- `Input` waits until an edit has been made, after entry to the widget, before offering a suggestion https://github.com/Textualize/textual/pull/4335
+
+### Added
+
+- Added `Document.start` and `end` location properties for convenience https://github.com/Textualize/textual/pull/4267
## [0.53.1] - 2023-03-18
@@ -1800,6 +1812,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling
+[0.54.0]: https://github.com/Textualize/textual/compare/v0.53.1...v0.54.0
[0.53.1]: https://github.com/Textualize/textual/compare/v0.53.0...v0.53.1
[0.53.0]: https://github.com/Textualize/textual/compare/v0.52.1...v0.53.0
[0.52.1]: https://github.com/Textualize/textual/compare/v0.52.0...v0.52.1
diff --git a/docs/events/unmount.md b/docs/events/unmount.md
new file mode 100644
index 0000000000..8e17c76924
--- /dev/null
+++ b/docs/events/unmount.md
@@ -0,0 +1,3 @@
+::: textual.events.Unmount
+ options:
+ heading_level: 1
diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md
index c0a95c265a..9e24442d30 100644
--- a/docs/widgets/text_area.md
+++ b/docs/widgets/text_area.md
@@ -84,6 +84,10 @@ This method is the programmatic equivalent of selecting some text and then pasti
Some other convenient methods are available, such as [`insert`][textual.widgets._text_area.TextArea.insert], [`delete`][textual.widgets._text_area.TextArea.delete], and [`clear`][textual.widgets._text_area.TextArea.clear].
+!!! tip
+ The `TextArea.document.end` property returns the location at the end of the
+ document, which might be convenient when editing programmatically.
+
### Working with the cursor
#### Moving the cursor
diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml
index 1ba90e4ee6..7dd0863b43 100644
--- a/mkdocs-nav.yml
+++ b/mkdocs-nav.yml
@@ -69,6 +69,7 @@ nav:
- "events/screen_resume.md"
- "events/screen_suspend.md"
- "events/show.md"
+ - "events/unmount.md"
- Styles:
- "styles/index.md"
- "styles/align.md"
diff --git a/pyproject.toml b/pyproject.toml
index 5d8ebf16de..630cc27599 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
-version = "0.53.1"
+version = "0.54.0"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
diff --git a/src/textual/app.py b/src/textual/app.py
index aaf799a66c..15811232c9 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -3006,11 +3006,18 @@ async def _broker_event(
return False
else:
event.stop()
- if isinstance(action, (str, tuple)):
+ if isinstance(action, str) or (isinstance(action, tuple) and len(action) == 2):
await self.run_action(action, default_namespace=default_namespace) # type: ignore[arg-type]
elif callable(action):
await action()
else:
+ if isinstance(action, tuple) and self.debug:
+ # It's a tuple and made it this far, which means it'll be a
+ # malformed action. This is a no-op, but let's log that
+ # anyway.
+ log.warning(
+ f"Can't parse @{event_name} action from style meta; check your console markup syntax"
+ )
return False
return True
diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py
index 3a4e5729b2..ca850285b5 100644
--- a/src/textual/document/_document.py
+++ b/src/textual/document/_document.py
@@ -171,6 +171,17 @@ def prepare_query(self, query: str) -> Query | None:
def line_count(self) -> int:
"""Returns the number of lines in the document."""
+ @property
+ @abstractmethod
+ def start(self) -> Location:
+ """Returns the location of the start of the document (0, 0)."""
+ return (0, 0)
+
+ @property
+ @abstractmethod
+ def end(self) -> Location:
+ """Returns the location of the end of the document."""
+
@overload
def __getitem__(self, line_index: int) -> str: ...
@@ -331,6 +342,17 @@ def line_count(self) -> int:
"""Returns the number of lines in the document."""
return len(self._lines)
+ @property
+ def start(self) -> Location:
+ """Returns the location of the start of the document (0, 0)."""
+ return super().start
+
+ @property
+ def end(self) -> Location:
+ """Returns the location of the end of the document."""
+ last_line = self._lines[-1]
+ return (self.line_count, len(last_line))
+
def get_index_from_location(self, location: Location) -> int:
"""Given a location, returns the index from the document's text.
diff --git a/src/textual/events.py b/src/textual/events.py
index 79c7c2366a..7e5c158bc5 100644
--- a/src/textual/events.py
+++ b/src/textual/events.py
@@ -143,7 +143,7 @@ class Mount(Event, bubble=False, verbose=False):
class Unmount(Event, bubble=False, verbose=False):
- """Sent when a widget is unmounted and may not longer receive messages.
+ """Sent when a widget is unmounted and may no longer receive messages.
- [ ] Bubbles
- [ ] Verbose
diff --git a/src/textual/widget.py b/src/textual/widget.py
index aac76e8778..4bfaabdee0 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -3618,10 +3618,8 @@ async def _compose(self) -> None:
raise TypeError(
f"{self!r} compose() method returned an invalid result; {error}"
) from error
- except Exception:
- from rich.traceback import Traceback
-
- self.app.panic(Traceback())
+ except Exception as error:
+ self.app._handle_exception(error)
else:
self._extend_compose(widgets)
await self.mount_composed_widgets(widgets)
diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py
index 0d3149f2d3..0b3ae57ee1 100644
--- a/src/textual/widgets/_input.py
+++ b/src/textual/widgets/_input.py
@@ -519,6 +519,7 @@ def _on_focus(self, _: Focus) -> None:
if self.cursor_blink:
self._blink_timer.resume()
self.app.cursor_position = self.cursor_screen_offset
+ self._suggestion = ""
async def _on_key(self, event: events.Key) -> None:
self._cursor_visible = True
diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py
index c37c7770e0..849e291bd4 100644
--- a/src/textual/widgets/_markdown.py
+++ b/src/textual/widgets/_markdown.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import re
from pathlib import Path, PurePath
from typing import Callable, Iterable, Optional
@@ -149,7 +150,12 @@ def build_from_token(self, token: Token) -> None:
if token.children:
for child in token.children:
if child.type == "text":
- content.append(child.content, style_stack[-1])
+ content.append(
+ # Ensure repeating spaces and/or tabs get squashed
+ # down to a single space.
+ re.sub(r"\s+", " ", child.content),
+ style_stack[-1],
+ )
if child.type == "hardbreak":
content.append("\n")
if child.type == "softbreak":
diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py
index 501eef8f40..4ad2023aad 100644
--- a/src/textual/widgets/_text_area.py
+++ b/src/textual/widgets/_text_area.py
@@ -688,7 +688,8 @@ def _watch_indent_width(self) -> None:
self.scroll_cursor_visible()
def _watch_show_vertical_scrollbar(self) -> None:
- self._rewrap_and_refresh_virtual_size()
+ if self.wrap_width:
+ self._rewrap_and_refresh_virtual_size()
self.scroll_cursor_visible()
def _watch_theme(self, theme: str) -> None:
@@ -2025,10 +2026,7 @@ def clear(self) -> EditResult:
Returns:
An EditResult relating to the deletion of all content.
"""
- document = self.document
- last_line = document[-1]
- document_end = (document.line_count, len(last_line))
- return self.delete((0, 0), document_end, maintain_selection_offset=False)
+ return self.delete((0, 0), self.document.end, maintain_selection_offset=False)
def _delete_via_keyboard(
self,
diff --git a/src/textual/worker.py b/src/textual/worker.py
index 7719cd4dca..f7f10b60ae 100644
--- a/src/textual/worker.py
+++ b/src/textual/worker.py
@@ -375,7 +375,8 @@ async def _run(self, app: App) -> None:
app.log.worker(Traceback())
if self.exit_on_error:
- app._fatal_error()
+ worker_failed = WorkerFailed(self._error)
+ app._handle_exception(worker_failed)
else:
self.state = WorkerState.SUCCESS
app.log.worker(self)
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index 4754af204b..29c658bf55 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -20792,137 +20792,137 @@
font-weight: 700;
}
- .terminal-2073605363-matrix {
+ .terminal-2577839347-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-2073605363-title {
+ .terminal-2577839347-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-2073605363-r1 { fill: #1e1e1e }
- .terminal-2073605363-r2 { fill: #0178d4 }
- .terminal-2073605363-r3 { fill: #c5c8c6 }
- .terminal-2073605363-r4 { fill: #e2e2e2 }
- .terminal-2073605363-r5 { fill: #1e1e1e;font-style: italic; }
- .terminal-2073605363-r6 { fill: #ff0000;font-style: italic; }
- .terminal-2073605363-r7 { fill: #121212 }
- .terminal-2073605363-r8 { fill: #e1e1e1 }
+ .terminal-2577839347-r1 { fill: #1e1e1e }
+ .terminal-2577839347-r2 { fill: #0178d4 }
+ .terminal-2577839347-r3 { fill: #c5c8c6 }
+ .terminal-2577839347-r4 { fill: #e2e2e2 }
+ .terminal-2577839347-r5 { fill: #1e1e1e;font-style: italic; }
+ .terminal-2577839347-r6 { fill: #ff0000;font-style: italic; }
+ .terminal-2577839347-r7 { fill: #121212 }
+ .terminal-2577839347-r8 { fill: #e1e1e1 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- FruitsApp
+ FruitsApp
-
-
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- ▊strawberry▎
- ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- ▊straw▎
- ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- ▊p▎
- ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- ▊b▎
- ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- ▊a▎
- ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-
-
-
-
-
+
+
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ ▊strawberry▎
+ ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ ▊straw▎
+ ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ ▊p▎
+ ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ ▊b▎
+ ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ ▊a▎
+ ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+
+
+
+
+
@@ -23812,6 +23812,174 @@
'''
# ---
+# name: test_markdown_space_squashing
+ '''
+
+
+ '''
+# ---
# name: test_markdown_theme_switching
'''