Repository: Textualize/textual

Issue:
`ListView.remove_items()` removes all items
Thanks @jkoelker for reporting this in https://github.com/Textualize/textual/pull/4708#discussion_r1677011195!

> I believe this changes the behavior of this functions to removing all `ListItem`s instead of the ones at the specific `indices`.

https://github.com/Textualize/textual/blob/66f2e0cf45ba4ee20b2a7d150bc8ab5f034d1f54/src/textual/widgets/_list_view.py#L262-L266

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b485020b..c530fcf56 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 ### Added
 
 - `TextArea.line_number_start` reactive attribute https://github.com/Textualize/textual/pull/4471
+- `Widget.remove_children` now accepts an iterable if widgets in addition to a selector https://github.com/Textualize/textual/issues/4735
 - Raise `ValueError` with improved error message when number of cells inserted using `DataTable.add_row` doesn't match the number of columns in the table https://github.com/Textualize/textual/pull/4742
 
 ### Fixed
diff --git a/src/textual/widget.py b/src/textual/widget.py
index fcdbc8aa6..dfac89349 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -3478,22 +3478,33 @@ class Widget(DOMNode):
         await_remove = self.app._prune(self, parent=self._parent)
         return await_remove
 
-    def remove_children(self, selector: str | type[QueryType] = "*") -> AwaitRemove:
+    def remove_children(
+        self, selector: str | type[QueryType] | Iterable[Widget] = "*"
+    ) -> AwaitRemove:
         """Remove the immediate children of this Widget from the DOM.
 
         Args:
-            selector: A CSS selector to specify which direct children to remove.
+            selector: A CSS selector or iterable of widgets to remove.
 
         Returns:
             An awaitable object that waits for the direct children to be removed.
         """
-        if not isinstance(selector, str):
+
+        if callable(selector) and issubclass(selector, Widget):
             selector = selector.__name__
-        parsed_selectors = parse_selectors(selector)
-        children_to_remove = [
-            child for child in self.children if match(parsed_selectors, child)
-        ]
-        await_remove = self.app._prune(*children_to_remove, parent=self._parent)
+
+        children_to_remove: Iterable[Widget]
+
+        if isinstance(selector, str):
+            parsed_selectors = parse_selectors(selector)
+            children_to_remove = [
+                child for child in self.children if match(parsed_selectors, child)
+            ]
+        else:
+            children_to_remove = selector
+        await_remove = self.app._prune(
+            *children_to_remove, parent=cast(DOMNode, self._parent)
+        )
         return await_remove
 
     @asynccontextmanager
diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py
index a8f9e1ed3..a4243cdc8 100644
--- a/src/textual/widgets/_list_view.py
+++ b/src/textual/widgets/_list_view.py
@@ -258,11 +258,8 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False):
             An awaitable object that waits for the direct children to be removed.
         """
         items = self.query("ListItem")
-        items_to_remove = []
-        for index in indices:
-            items_to_remove.append(items[index])
-
-        await_remove = items.remove()
+        items_to_remove = [items[index] for index in indices]
+        await_remove = self.remove_children(items_to_remove)
         return await_remove
 
     def action_select_cursor(self) -> None:


Test patch:
diff --git a/tests/listview/test_listview_remove_items.py b/tests/listview/test_listview_remove_items.py
new file mode 100644
index 000000000..43501cdf1
--- /dev/null
+++ b/tests/listview/test_listview_remove_items.py
@@ -0,0 +1,32 @@
+from textual.app import App, ComposeResult
+from textual.widgets import ListView, ListItem, Label
+
+
+class ListViewApp(App[None]):
+    def compose(self) -> ComposeResult:
+        yield ListView(
+            ListItem(Label("0")),
+            ListItem(Label("1")),
+            ListItem(Label("2")),
+            ListItem(Label("3")),
+            ListItem(Label("4")),
+            ListItem(Label("5")),
+            ListItem(Label("6")),
+            ListItem(Label("7")),
+            ListItem(Label("8")),
+        )
+
+
+async def test_listview_remove_items() -> None:
+    """Regression test for https://github.com/Textualize/textual/issues/4735"""
+    app = ListViewApp()
+    async with app.run_test() as pilot:
+        listview = pilot.app.query_one(ListView)
+        assert len(listview) == 9
+        await listview.remove_items(range(4, 9))
+        assert len(listview) == 4
+
+
+if __name__ == "__main__":
+    app = ListViewApp()
+    app.run()