Skip to content

Commit 665dca9

Browse files
authored
Merge pull request #3618 from davep/command-palette-worker-nuke
Ensure that the command palette doesn't kill *all* workers when stoping command gathering
2 parents efbb655 + 9cacf8c commit 665dca9

File tree

3 files changed

+64
-6
lines changed

3 files changed

+64
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2121
- Fixed `OptionList` event leakage from `CommandPalette` to `App`.
2222
- Fixed crash in `LoadingIndicator` https://github.com/Textualize/textual/pull/3498
2323
- Fixed crash when `Tabs` appeared as a descendant of `TabbedContent` in the DOM https://github.com/Textualize/textual/pull/3602
24+
- Fixed the command palette cancelling other workers https://github.com/Textualize/textual/issues/3615
2425

2526
### Added
2627

src/textual/command.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ def _on_click(self, event: Click) -> None:
515515
method of dismissing the palette.
516516
"""
517517
if self.get_widget_at(event.screen_x, event.screen_y)[0] is self:
518-
self.workers.cancel_all()
518+
self._cancel_gather_commands()
519519
self.dismiss()
520520

521521
def on_mount(self, _: Mount) -> None:
@@ -774,7 +774,10 @@ def _refresh_command_list(
774774
_NO_MATCHES: Final[str] = "--no-matches"
775775
"""The ID to give the disabled option that shows there were no matches."""
776776

777-
@work(exclusive=True)
777+
_GATHER_COMMANDS_GROUP: Final[str] = "--textual-command-palette-gather-commands"
778+
"""The group name of the command gathering worker."""
779+
780+
@work(exclusive=True, group=_GATHER_COMMANDS_GROUP)
778781
async def _gather_commands(self, search_value: str) -> None:
779782
"""Gather up all of the commands that match the search value.
780783
@@ -895,6 +898,10 @@ async def _gather_commands(self, search_value: str) -> None:
895898
if command_list.option_count == 0 and not worker.is_cancelled:
896899
self._start_no_matches_countdown()
897900

901+
def _cancel_gather_commands(self) -> None:
902+
"""Cancel any operation that is gather commands."""
903+
self.workers.cancel_group(self, self._GATHER_COMMANDS_GROUP)
904+
898905
@on(Input.Changed)
899906
def _input(self, event: Input.Changed) -> None:
900907
"""React to input in the command palette.
@@ -903,7 +910,7 @@ def _input(self, event: Input.Changed) -> None:
903910
event: The input event.
904911
"""
905912
event.stop()
906-
self.workers.cancel_all()
913+
self._cancel_gather_commands()
907914
self._stop_no_matches_countdown()
908915

909916
search_value = event.value.strip()
@@ -921,7 +928,7 @@ def _select_command(self, event: OptionList.OptionSelected) -> None:
921928
event: The option selection event.
922929
"""
923930
event.stop()
924-
self.workers.cancel_all()
931+
self._cancel_gather_commands()
925932
input = self.query_one(CommandInput)
926933
with self.prevent(Input.Changed):
927934
assert isinstance(event.option, Command)
@@ -958,7 +965,7 @@ def _select_or_command(
958965
if self._selected_command is not None:
959966
# ...we should return it to the parent screen and let it
960967
# decide what to do with it (hopefully it'll run it).
961-
self.workers.cancel_all()
968+
self._cancel_gather_commands()
962969
self.dismiss(self._selected_command.command)
963970

964971
@on(OptionList.OptionHighlighted)
@@ -971,7 +978,7 @@ def _action_escape(self) -> None:
971978
if self._list_visible:
972979
self._list_visible = False
973980
else:
974-
self.workers.cancel_all()
981+
self._cancel_gather_commands()
975982
self.dismiss()
976983

977984
def _action_command_list(self, action: str) -> None:
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Tests for https://github.com/Textualize/textual/issues/3615"""
2+
3+
from asyncio import sleep
4+
5+
from textual import work
6+
from textual.app import App
7+
from textual.command import Hit, Hits, Provider
8+
9+
10+
class SimpleSource(Provider):
11+
async def search(self, query: str) -> Hits:
12+
def goes_nowhere_does_nothing() -> None:
13+
pass
14+
15+
for _ in range(100):
16+
yield Hit(1, query, goes_nowhere_does_nothing, query)
17+
18+
19+
class CommandPaletteNoWorkerApp(App[None]):
20+
COMMANDS = {SimpleSource}
21+
22+
23+
async def test_no_command_palette_worker_droppings() -> None:
24+
"""The command palette should not leave any workers behind.."""
25+
async with CommandPaletteNoWorkerApp().run_test() as pilot:
26+
assert len(pilot.app.workers) == 0
27+
pilot.app.action_command_palette()
28+
await pilot.press("a", "enter")
29+
assert len(pilot.app.workers) == 0
30+
31+
32+
class CommandPaletteWithWorkerApp(App[None]):
33+
COMMANDS = {SimpleSource}
34+
35+
def on_mount(self) -> None:
36+
self.innocent_worker()
37+
38+
@work
39+
async def innocent_worker(self) -> None:
40+
while True:
41+
await sleep(1)
42+
43+
44+
async def test_innocent_worker_is_untouched() -> None:
45+
"""Using the command palette should not halt other workers."""
46+
async with CommandPaletteWithWorkerApp().run_test() as pilot:
47+
assert len(pilot.app.workers) > 0
48+
pilot.app.action_command_palette()
49+
await pilot.press("a", "enter")
50+
assert len(pilot.app.workers) > 0

0 commit comments

Comments
 (0)