Skip to content

Commit 3204e32

Browse files
committed
Merge branch 'main' into queue-optimize
2 parents 5914f57 + 8e7fb1a commit 3204e32

File tree

11 files changed

+459
-32
lines changed

11 files changed

+459
-32
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1717
### Added
1818

1919
- Added `bar_renderable` to `ProgressBar` widget https://github.com/Textualize/textual/pull/5963
20-
20+
- Added `OptionList.set_options` https://github.com/Textualize/textual/pull/6048
21+
- Added `TextArea.suggestion` https://github.com/Textualize/textual/pull/6048
22+
- Added `TextArea.placeholder` https://github.com/Textualize/textual/pull/6048
2123

2224
### Changed
2325

src/textual/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2768,6 +2768,7 @@ def push_screen(
27682768

27692769
next_screen._push_result_callback(message_pump, callback, future)
27702770
self._load_screen_css(next_screen)
2771+
next_screen._update_auto_focus()
27712772
self._screen_stack.append(next_screen)
27722773
next_screen.post_message(events.ScreenResume())
27732774
self.log.system(f"{self.screen} is current (PUSHED)")

src/textual/highlight.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class HighlightTheme:
2929
Token.Keyword.Namespace: "$text-error",
3030
Token.Keyword.Type: "bold",
3131
Token.Literal.Number: "$text-warning",
32+
Token.Literal.String.Backtick: "$text 60%",
3233
Token.Literal.String: "$text-success 90%",
3334
Token.Literal.String.Doc: "$text-success 80% italic",
3435
Token.Literal.String.Double: "$text-success 90%",

src/textual/screen.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,23 +1346,27 @@ def _on_screen_resume(self) -> None:
13461346
self.app._refresh_notifications()
13471347
size = self.app.size
13481348

1349-
# Only auto-focus when the app has focus (textual-web only)
1349+
self._update_auto_focus()
1350+
1351+
if self.is_attached:
1352+
self._compositor_refresh()
1353+
self.app.stylesheet.update(self)
1354+
self._refresh_layout(size)
1355+
self.refresh()
1356+
1357+
def _update_auto_focus(self) -> None:
1358+
"""Update auto focus."""
13501359
if self.app.app_focus:
13511360
auto_focus = (
13521361
self.app.AUTO_FOCUS if self.AUTO_FOCUS is None else self.AUTO_FOCUS
13531362
)
13541363
if auto_focus and self.focused is None:
13551364
for widget in self.query(auto_focus):
13561365
if widget.focusable:
1366+
widget.has_focus = True
13571367
self.set_focus(widget)
13581368
break
13591369

1360-
if self.is_attached:
1361-
self._compositor_refresh()
1362-
self.app.stylesheet.update(self)
1363-
self._refresh_layout(size)
1364-
self.refresh()
1365-
13661370
def _on_screen_suspend(self) -> None:
13671371
"""Screen has suspended."""
13681372
if self.app.SUSPENDED_SCREEN_CLASS:

src/textual/widgets/_option_list.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,15 +324,37 @@ def clear_options(self) -> Self:
324324
self._option_to_index.clear()
325325
self.highlighted = None
326326
self.refresh()
327-
self.scroll_to(0, 0, animate=False)
327+
self.scroll_y = 0
328328
self._update_lines()
329329
return self
330330

331+
def set_options(self, options: Iterable[OptionListContent]) -> Self:
332+
"""Set options, potentially clearing existing options.
333+
334+
Args:
335+
options: Options to set.
336+
337+
Returns:
338+
The `OptionList` instance.
339+
"""
340+
self._options.clear()
341+
self._line_cache.clear()
342+
self._option_render_cache.clear()
343+
self._id_to_option.clear()
344+
self._option_to_index.clear()
345+
self.highlighted = None
346+
self.scroll_y = 0
347+
self.add_options(options)
348+
return self
349+
331350
def add_options(self, new_options: Iterable[OptionListContent]) -> Self:
332351
"""Add new options.
333352
334353
Args:
335354
new_options: Content of new options.
355+
356+
Returns:
357+
The `OptionList` instance.
336358
"""
337359

338360
new_options = list(new_options)

src/textual/widgets/_static.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ def __init__(
4646
)
4747
self.expand = expand
4848
self.shrink = shrink
49-
self._content = content
50-
self._visual: Visual | None = None
49+
self.__content = content
50+
self.__visual: Visual | None = None
5151

5252
@property
5353
def visual(self) -> Visual:
@@ -58,19 +58,19 @@ def visual(self) -> Visual:
5858
update with a string, then the visual will be a [Content][textual.content.Content] instance.
5959
6060
"""
61-
if self._visual is None:
62-
self._visual = visualize(self, self._content, markup=self._render_markup)
63-
return self._visual
61+
if self.__visual is None:
62+
self.__visual = visualize(self, self.__content, markup=self._render_markup)
63+
return self.__visual
6464

6565
@property
6666
def content(self) -> VisualType:
6767
"""The original content set in the constructor."""
68-
return self._content
68+
return self.__content
6969

7070
@content.setter
7171
def content(self, content: VisualType) -> None:
72-
self._content = content
73-
self._visual = visualize(self, content, markup=self._render_markup)
72+
self.__content = content
73+
self.__visual = visualize(self, content, markup=self._render_markup)
7474
self.clear_cached_dimensions()
7575
self.refresh(layout=True)
7676

@@ -90,6 +90,6 @@ def update(self, content: VisualType = "", *, layout: bool = True) -> None:
9090
layout: Also perform a layout operation (set to `False` if you are certain the size won't change).
9191
"""
9292

93-
self._content = content
94-
self._visual = visualize(self, content, markup=self._render_markup)
93+
self.__content = content
94+
self.__visual = visualize(self, content, markup=self._render_markup)
9595
self.refresh(layout=layout)

src/textual/widgets/_text_area.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from textual._tree_sitter import TREE_SITTER, get_language
1919
from textual.cache import LRUCache
2020
from textual.color import Color
21+
from textual.content import Content
2122
from textual.document._document import (
2223
Document,
2324
DocumentBase,
@@ -36,6 +37,7 @@
3637
from textual.document._wrapped_document import WrappedDocument
3738
from textual.expand_tabs import expand_tabs_inline, expand_text_tabs_from_widths
3839
from textual.screen import Screen
40+
from textual.style import Style as ContentStyle
3941

4042
if TYPE_CHECKING:
4143
from tree_sitter import Language, Query
@@ -143,6 +145,14 @@ class TextArea(ScrollView):
143145
background: $foreground 30%;
144146
}
145147
148+
& .text-area--suggestion {
149+
color: $text-muted;
150+
}
151+
152+
& .text-area--placeholder {
153+
color: $text 40%;
154+
}
155+
146156
&:focus {
147157
border: tall $border;
148158
}
@@ -183,6 +193,8 @@ class TextArea(ScrollView):
183193
"text-area--cursor-line",
184194
"text-area--selection",
185195
"text-area--matching-bracket",
196+
"text-area--suggestion",
197+
"text-area--placeholder",
186198
}
187199
"""
188200
`TextArea` offers some component classes which can be used to style aspects of the widget.
@@ -197,6 +209,8 @@ class TextArea(ScrollView):
197209
| `text-area--cursor-line` | Target the line the cursor is on. |
198210
| `text-area--selection` | Target the current selection. |
199211
| `text-area--matching-bracket` | Target matching brackets. |
212+
| `text-area--suggestion` | Target the text set in the `suggestion` reactive. |
213+
| `text-area--placeholder` | Target the placeholder text. |
200214
"""
201215

202216
BINDINGS = [
@@ -392,6 +406,12 @@ class TextArea(ScrollView):
392406
"""Indicates where the cursor is in the blink cycle. If it's currently
393407
not visible due to blinking, this is False."""
394408

409+
suggestion: Reactive[str] = reactive("")
410+
"""A suggestion for auto-complete (pressing right will insert it)."""
411+
412+
placeholder: Reactive[str | Content] = reactive("")
413+
"""Text to show when the text area has no content."""
414+
395415
@dataclass
396416
class Changed(Message):
397417
"""Posted when the content inside the TextArea changes.
@@ -443,6 +463,7 @@ def __init__(
443463
tooltip: RenderableType | None = None,
444464
compact: bool = False,
445465
highlight_cursor_line: bool = True,
466+
placeholder: str | Content = "",
446467
) -> None:
447468
"""Construct a new `TextArea`.
448469
@@ -464,6 +485,7 @@ def __init__(
464485
tooltip: Optional tooltip.
465486
compact: Enable compact style (without borders).
466487
highlight_cursor_line: Highlight the line under the cursor.
488+
placeholder: Text to display when there is not content.
467489
"""
468490
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
469491

@@ -524,6 +546,7 @@ def __init__(
524546
self.set_reactive(TextArea.show_line_numbers, show_line_numbers)
525547
self.set_reactive(TextArea.line_number_start, line_number_start)
526548
self.set_reactive(TextArea.highlight_cursor_line, highlight_cursor_line)
549+
self.set_reactive(TextArea.placeholder, placeholder)
527550

528551
self._line_cache: LRUCache[tuple, Strip] = LRUCache(1024)
529552

@@ -565,6 +588,7 @@ def code_editor(
565588
tooltip: RenderableType | None = None,
566589
compact: bool = False,
567590
highlight_cursor_line: bool = True,
591+
placeholder: str | Content = "",
568592
) -> TextArea:
569593
"""Construct a new `TextArea` with sensible defaults for editing code.
570594
@@ -607,6 +631,7 @@ def code_editor(
607631
tooltip=tooltip,
608632
compact=compact,
609633
highlight_cursor_line=highlight_cursor_line,
634+
placeholder=placeholder,
610635
)
611636

612637
@staticmethod
@@ -632,6 +657,7 @@ def _get_builtin_highlight_query(language_name: str) -> str:
632657

633658
def notify_style_update(self) -> None:
634659
self._line_cache.clear()
660+
super().notify_style_update()
635661

636662
def check_consume_key(self, key: str, character: str | None = None) -> bool:
637663
"""Check if the widget may consume the given key.
@@ -1173,6 +1199,25 @@ def render_line(self, y: int) -> Strip:
11731199
Returns:
11741200
A rendered line.
11751201
"""
1202+
if y == 0 and not self.text and self.placeholder:
1203+
style = self.get_visual_style("text-area--placeholder")
1204+
content = (
1205+
Content(self.placeholder)
1206+
if isinstance(self.placeholder, str)
1207+
else self.placeholder
1208+
)
1209+
content = content.stylize(style)
1210+
if self._draw_cursor:
1211+
theme = self._theme
1212+
cursor_style = theme.cursor_style if theme else None
1213+
if cursor_style:
1214+
content = content.stylize(
1215+
ContentStyle.from_rich_style(cursor_style), 0, 1
1216+
)
1217+
return Strip(
1218+
content.render_segments(self.visual_style), content.cell_length
1219+
)
1220+
11761221
scroll_x, scroll_y = self.scroll_offset
11771222
absolute_y = scroll_y + y
11781223
selection = self.selection
@@ -1201,6 +1246,7 @@ def render_line(self, y: int) -> Strip:
12011246
self.show_line_numbers,
12021247
self.read_only,
12031248
self.show_cursor,
1249+
self.suggestion,
12041250
)
12051251
if (cached_line := self._line_cache.get(cache_key)) is not None:
12061252
return cached_line
@@ -1332,6 +1378,16 @@ def _render_line(self, y: int) -> Strip:
13321378
cursor_column + 1,
13331379
)
13341380

1381+
if self.suggestion and self.has_focus:
1382+
suggestion_style = self.get_component_rich_style(
1383+
"text-area--suggestion"
1384+
)
1385+
line = Text.assemble(
1386+
line[:cursor_column],
1387+
(self.suggestion, suggestion_style),
1388+
line[cursor_column:],
1389+
)
1390+
13351391
if draw_cursor:
13361392
cursor_style = theme.cursor_style if theme else None
13371393
if cursor_style:
@@ -1474,6 +1530,7 @@ def edit(self, edit: Edit) -> EditResult:
14741530
Data relating to the edit that may be useful. The data returned
14751531
may be different depending on the edit performed.
14761532
"""
1533+
self.suggestion = ""
14771534
old_gutter_width = self.gutter_width
14781535
result = edit.do(self)
14791536
self.history.record(edit)
@@ -2030,6 +2087,9 @@ def action_cursor_right(self, select: bool = False) -> None:
20302087
if not self._has_cursor:
20312088
self.scroll_right()
20322089
return
2090+
if self.suggestion:
2091+
self.insert(self.suggestion)
2092+
return
20332093
target = (
20342094
self.get_cursor_right_location()
20352095
if select or self.selection.is_empty

tests/option_list/test_option_list_create.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,13 @@ async def test_options_are_available_soon() -> None:
156156
option = Option("", id="some_id")
157157
option_list = OptionList(option)
158158
assert option_list.get_option("some_id") is option
159+
160+
161+
async def test_set_options():
162+
"""Test set_options method."""
163+
async with OptionListApp().run_test() as pilot:
164+
option_list = pilot.app.query_one(OptionList)
165+
option_list.set_options(["foo", "bar"])
166+
assert option_list.option_count == 2
167+
assert option_list.get_option_at_index(0).prompt == "foo"
168+
assert option_list.get_option_at_index(1).prompt == "bar"

0 commit comments

Comments
 (0)