`Selection.extract` IndexError via ordinary mouse selection on built-in widgets (MRE for #6428)
Have you checked closed issues? (https://github.com/Textualize/textual/issues?q=is%3Aissue+is%3Aclosed)
Yes, this is the crash reported in #6428, which was closed for lack of an MRE. Below is a reproduction that uses only built-in widgets and Textual's own mouse-selection machinery.
This could be some AI hallucination stuff, but it has come up a few times for myself and the MRE below crashes every time.
Have you checked against the most recent version of Textual? (https://pypi.org/search/?q=textual)
Yes, reproduced on 8.2.8.
The bug
Selection.extract() clamps end_line but never start_line_index:
end_line = min(len(lines), end_line)
if start_line_index == end_line:
return lines[start_line_index][start_offset:end_offset] # <- unclampedA selection whose start row equals the extracted text's line count takes that path and raises IndexError: list index out of range from inside Screen.get_selected_text().
The MRE example is attached, and its output is below:
Output:
Traceback (most recent call last):
...
File "...\textual\screen.py", line 977, in get_selected_text
and (selected_text_in_widget := widget.get_selection(selection))
File "...\textual\widget.py", line 4232, in get_selection
return selection.extract(text), "\n"
File "...\textual\selection.py", line 60, in extract
return lines[start_line_index][start_offset:end_offset]
IndexError: list index out of rangeHere is an interactive variant (drag across the blank line under "hello" with the mouse, then press c):
from textual.app import App, ComposeResult
from textual.widgets import Static
class MREApp(App):
BINDINGS = [("c", "show_selected", "Show selected text")]
def compose(self) -> ComposeResult:
yield Static("hello\n")
def action_show_selected(self) -> None:
self.notify(repr(self.screen.get_selected_text()))
MREApp().run()Suggested fix
This was Claude's suggested fix - figured it was worth submitting to see if it would help out.
Guard the fast path the same way the multi-line path below it already guards (except IndexError: pass), or clamp the start like the end:
if start_line_index == end_line:
try:
return lines[start_line_index][start_offset:end_offset]
except IndexError:
return ""Textual Diagnostics
Versions
| Name | Value |
|---|---|
| Textual | 8.2.8 |
| Rich | 15.0.0 |
Python
| Name | Value |
|---|---|
| Version | 3.14.7 |
| Implementation | CPython |
| Compiler | MSC v.1944 64 bit (AMD64) |
| Executable | C:\Users\jchampion\AppData\Local\Python\pythoncore-3.14-64\python.exe |
Operating System
| Name | Value |
|---|---|
| System | Windows |
| Release | 11 |
| Version | 10.0.26200 |
Terminal
| Name | Value |
|---|---|
| Terminal Application | Windows Terminal |
| TERM | Not set |
| COLORTERM | Not set |
| FORCE_COLOR | Not set |
| NO_COLOR | Not set |
Rich Console options
| Name | Value |
|---|---|
| size | width=148, height=34 |
| legacy_windows | False |
| min_width | 1 |
| max_width | 148 |
| is_terminal | True |
| encoding | utf-8 |
| max_height | 34 |
| justify | None |
| overflow | None |
| no_wrap | False |
| highlight | None |
| markup | None |
| height | None |
Source: Textualize/textual