Repository: Textualize/textual

Issue:
Removing an option from an `OptionList` when the mouse is hovering over the last item causes a crash
Given this code:

```python
from textual import on
from textual.app import App, ComposeResult
from textual.widgets import OptionList

class OptionListRemoveOnDeleteApp(App[None]):

    def compose(self) -> ComposeResult:
        yield OptionList(*[f"This is option {n}" for n in range(20)])

    @on(OptionList.OptionSelected)
    def remove_option(self, event: OptionList.OptionSelected) -> None:
        self.query_one(OptionList).remove_option_at_index(event.option_index)

if __name__ == "__main__":
    OptionListRemoveOnDeleteApp().run()
```

hover the mouse over the last item in the list, then either click on the last item, or hit <kbd>Enter</kbd>.  It will result in a `IndexError: list index out of range`.

At first glance it seems to be to do with the value of `_mouse_hovering_over`, which should possibly be reset to `None` when an option is removed (especially if the option that is hovered over is the option being removed *and* it's the last option); or [the test for drawing of a `mouse_over` line in `render_line` should do a double check that the hover option is still a known option](https://github.com/Textualize/textual/blob/74aa90f5ac3e9cbd5e002cf504241a9b2d257930/src/textual/widgets/_option_list.py#L927-L930).

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aeaecf207..e824df982 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ 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
+
+- Fixed a crash when removing an option from an `OptionList` while the mouse is hovering over the last option https://github.com/Textualize/textual/issues/3270
+
 ## [0.36.0] - 2023-09-05
 
 ### Added
diff --git a/src/textual/widgets/_option_list.py b/src/textual/widgets/_option_list.py
index 9d3280644..74c89fb20 100644
--- a/src/textual/widgets/_option_list.py
+++ b/src/textual/widgets/_option_list.py
@@ -613,6 +613,7 @@ class OptionList(ScrollView, can_focus=True):
         self._refresh_content_tracking(force=True)
         # Force a re-validation of the highlight.
         self.highlighted = self.highlighted
+        self._mouse_hovering_over = None
         self.refresh()
 
     def remove_option(self, option_id: str) -> Self:


Test patch:
diff --git a/tests/option_list/test_option_removal.py b/tests/option_list/test_option_removal.py
index fe64543aa..a26f5af49 100644
--- a/tests/option_list/test_option_removal.py
+++ b/tests/option_list/test_option_removal.py
@@ -5,6 +5,7 @@ from __future__ import annotations
 import pytest
 
 from textual.app import App, ComposeResult
+from textual.geometry import Offset
 from textual.widgets import OptionList
 from textual.widgets.option_list import Option, OptionDoesNotExist
 
@@ -99,3 +100,13 @@ async def test_remove_invalid_index() -> None:
     async with OptionListApp().run_test() as pilot:
         with pytest.raises(OptionDoesNotExist):
             pilot.app.query_one(OptionList).remove_option_at_index(23)
+
+
+async def test_remove_with_hover_on_last_option():
+    """https://github.com/Textualize/textual/issues/3270"""
+    async with OptionListApp().run_test() as pilot:
+        await pilot.hover(OptionList, Offset(1, 1) + Offset(2, 1))
+        option_list = pilot.app.query_one(OptionList)
+        assert option_list._mouse_hovering_over == 1
+        option_list.remove_option_at_index(0)
+        assert option_list._mouse_hovering_over == None