Repository: Textualize/textual

Issue:
`ListView` initial index is off by one
Setting the `initial_index` of a `ListView` seems to be off by one, introduced somewhere in v0.48.2:

```python
from textual.app import App, ComposeResult
from textual.widgets import Label, ListItem, ListView


class ExampleApp(App):
    def compose(self) -> ComposeResult:
        list_items = [ListItem(Label(str(i))) for i in range(10)]
        yield ListView(
            *list_items,
            initial_index=9,
        )


if __name__ == "__main__":
    app = ExampleApp()
    app.run()
```

![image](https://github.com/Textualize/textual/assets/101601846/90471154-e20b-4da4-b776-f436a29dfac2)


Discussion:
Thank you for your issue. Give us a little time to review it.

PS. You might want to check the [FAQ](https://textual.textualize.io/FAQ/) if you haven't done so already.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)
After digging a bit deeper into this, the problem seems to be where the `ListView._index` is set in `__init__`:

https://github.com/Textualize/textual/blob/a5cab15cc10143fb49771231c35cbe5665083f9c/src/textual/widgets/_list_view.py#L120-L125

The off-by-one error can be fixed by simply changing where `initial_index - 1`, *however* I think the bigger issue here is that the `self._nodes` aren't actually available yet, meaning this will always just return the default anchor value?

Resolving patch:
diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py
--- a/src/textual/widgets/_list_view.py
+++ b/src/textual/widgets/_list_view.py
@@ -119,9 +119,10 @@ def __init__(
         )
         # Set the index to the given initial index, or the first available index after.
         self._index = _widget_navigation.find_next_enabled(
-            self._nodes,
-            anchor=initial_index - 1 if initial_index is not None else None,
+            children,
+            anchor=initial_index if initial_index is not None else None,
             direction=1,
+            with_anchor=True,
         )
 
     def _on_mount(self, _: Mount) -> None:


Test patch:
diff --git a/tests/listview/test_listview_initial_index.py b/tests/listview/test_listview_initial_index.py
new file mode 100644
--- /dev/null
+++ b/tests/listview/test_listview_initial_index.py
@@ -0,0 +1,42 @@
+import pytest
+
+from textual.app import App, ComposeResult
+from textual.widgets import Label, ListItem, ListView
+
+
+@pytest.mark.parametrize(
+    "initial_index,expected_index",
+    [
+        (0, 1),
+        (1, 1),
+        (2, 4),
+        (3, 4),
+        (4, 4),
+        (5, 5),
+        (6, 7),
+        (7, 7),
+        (8, 1),
+    ],
+)
+async def test_listview_initial_index(initial_index, expected_index) -> None:
+    """Regression test for https://github.com/Textualize/textual/issues/4449"""
+
+    class ListViewDisabledItemsApp(App[None]):
+        def compose(self) -> ComposeResult:
+            yield ListView(
+                ListItem(Label("0"), disabled=True),
+                ListItem(Label("1")),
+                ListItem(Label("2"), disabled=True),
+                ListItem(Label("3"), disabled=True),
+                ListItem(Label("4")),
+                ListItem(Label("5")),
+                ListItem(Label("6"), disabled=True),
+                ListItem(Label("7")),
+                ListItem(Label("8"), disabled=True),
+                initial_index=initial_index,
+            )
+
+    app = ListViewDisabledItemsApp()
+    async with app.run_test() as pilot:
+        list_view = pilot.app.query_one(ListView)
+        assert list_view._index == expected_index
diff --git a/tests/listview/test_listview_navigation.py b/tests/listview/test_listview_navigation.py
--- a/tests/listview/test_listview_navigation.py
+++ b/tests/listview/test_listview_navigation.py
@@ -35,7 +35,6 @@ async def test_keyboard_navigation_with_disabled_items() -> None:
             await pilot.press("up")
 
     assert app.highlighted == [
-        None,
         "1",
         "4",
         "5",