diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md
index 5747d394ec..0824f4c697 100644
--- a/docs/widgets/text_area.md
+++ b/docs/widgets/text_area.md
@@ -353,6 +353,8 @@ the `show_line_numbers` attribute to `True` or `False`.
Setting this attribute will immediately repaint the `TextArea` to reflect the new value.
+You can also change the start line number (the topmost line number in the gutter) by setting the `line_number_start` reactive attribute.
+
### Extending `TextArea`
Sometimes, you may wish to subclass `TextArea` to add some extra functionality.
@@ -506,6 +508,7 @@ A detailed view of these classes is out of scope, but do note that a lot of the
| `theme` | `str` | `"css"` | The theme to use. |
| `selection` | `Selection` | `Selection()` | The current selection. |
| `show_line_numbers` | `bool` | `False` | Show or hide line numbers. |
+| `line_number_start` | `int` | `1` | The start line number in the gutter. |
| `indent_width` | `int` | `4` | The number of spaces to indent and width of tabs. |
| `match_cursor_bracket` | `bool` | `True` | Enable/disable highlighting matching brackets under cursor. |
| `cursor_blink` | `bool` | `True` | Enable/disable blinking of the cursor when the widget has focus. |
diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py
index 21768f2bb8..7b1c10cb6b 100644
--- a/src/textual/widgets/_text_area.py
+++ b/src/textual/widgets/_text_area.py
@@ -301,6 +301,9 @@ class TextArea(ScrollView):
Changing this value will immediately re-render the `TextArea`."""
+ line_number_start: Reactive[int] = reactive(1, init=False)
+ """The line number the first line should be."""
+
indent_width: Reactive[int] = reactive(4, init=False)
"""The width of tabs or the multiple of spaces to align to on pressing the `tab` key.
@@ -370,6 +373,7 @@ def __init__(
tab_behavior: Literal["focus", "indent"] = "focus",
read_only: bool = False,
show_line_numbers: bool = False,
+ line_number_start: int = 1,
max_checkpoints: int = 50,
name: str | None = None,
id: str | None = None,
@@ -387,6 +391,7 @@ def __init__(
tab_behavior: If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.
read_only: Enable read-only mode. This prevents edits using the keyboard.
show_line_numbers: Show line numbers on the left edge.
+ line_number_start: What line number to start on.
max_checkpoints: The maximum number of undo history checkpoints to retain.
name: The name of the `TextArea` widget.
id: The ID of the widget, used to refer to it from Textual CSS.
@@ -455,6 +460,7 @@ def __init__(
self.set_reactive(TextArea.soft_wrap, soft_wrap)
self.set_reactive(TextArea.read_only, read_only)
self.set_reactive(TextArea.show_line_numbers, show_line_numbers)
+ self.set_reactive(TextArea.line_number_start, line_number_start)
self.tab_behavior = tab_behavior
@@ -475,6 +481,7 @@ def code_editor(
tab_behavior: Literal["focus", "indent"] = "indent",
read_only: bool = False,
show_line_numbers: bool = True,
+ line_number_start: int = 1,
max_checkpoints: int = 50,
name: str | None = None,
id: str | None = None,
@@ -494,6 +501,7 @@ def code_editor(
soft_wrap: Enable soft wrapping.
tab_behavior: If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.
show_line_numbers: Show line numbers on the left edge.
+ line_number_start: What line number to start on.
name: The name of the `TextArea` widget.
id: The ID of the widget, used to refer to it from Textual CSS.
classes: One or more Textual CSS compatible class names separated by spaces.
@@ -508,6 +516,7 @@ def code_editor(
tab_behavior=tab_behavior,
read_only=read_only,
show_line_numbers=show_line_numbers,
+ line_number_start=line_number_start,
max_checkpoints=max_checkpoints,
name=name,
id=id,
@@ -691,6 +700,11 @@ def _watch_show_line_numbers(self) -> None:
self._rewrap_and_refresh_virtual_size()
self.scroll_cursor_visible()
+ def _watch_line_number_start(self) -> None:
+ """The line number gutter max size might change and contributes to virtual size, so recalculate."""
+ self._rewrap_and_refresh_virtual_size()
+ self.scroll_cursor_visible()
+
def _watch_indent_width(self) -> None:
"""Changing width of tabs will change the document display width."""
self._rewrap_and_refresh_virtual_size()
@@ -1142,7 +1156,9 @@ def render_line(self, y: int) -> Strip:
gutter_style = theme.gutter_style
gutter_width_no_margin = gutter_width - 2
- gutter_content = str(line_index + 1) if section_offset == 0 else ""
+ gutter_content = (
+ str(line_index + self.line_number_start) if section_offset == 0 else ""
+ )
gutter = Text(
f"{gutter_content:>{gutter_width_no_margin}} ",
style=gutter_style or "",
@@ -1467,7 +1483,8 @@ def gutter_width(self) -> int:
# The longest number in the gutter plus two extra characters: `│ `.
gutter_margin = 2
gutter_width = (
- len(str(self.document.line_count)) + gutter_margin
+ len(str(self.document.line_count - 1 + self.line_number_start))
+ + gutter_margin
if self.show_line_numbers
else 0
)
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index 693e150753..e17165d8c8 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -45462,6 +45462,103 @@
'''
# ---
+# name: test_text_area_line_number_start
+ '''
+
+
+ '''
+# ---
# name: test_text_area_read_only_cursor_rendering
'''