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 + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LineNumbersReactive + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎ +  9999  Foo                   + 10000  Bar                   + 10001  Baz                   + 10002   + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎ + + + + + ''' +# --- # name: test_text_area_read_only_cursor_rendering ''' diff --git a/tests/snapshot_tests/snapshot_apps/text_area_line_number_start.py b/tests/snapshot_tests/snapshot_apps/text_area_line_number_start.py new file mode 100644 index 0000000000..d8db335960 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/text_area_line_number_start.py @@ -0,0 +1,25 @@ +from textual.app import App, ComposeResult +from textual.widgets import TextArea + +TEXT = """\ +Foo +Bar +Baz +""" + + +class LineNumbersReactive(App[None]): + START_LINE_NUMBER = 9999 + + def compose(self) -> ComposeResult: + yield TextArea( + TEXT, + soft_wrap=True, + show_line_numbers=True, + line_number_start=self.START_LINE_NUMBER, + ) + + +app = LineNumbersReactive() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 74a4a7b607..c297ca0cff 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -1000,6 +1000,12 @@ def test_text_area_wrapping_and_folding(snap_compare): ) +def test_text_area_line_number_start(snap_compare): + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area_line_number_start.py", terminal_size=(32, 8) + ) + + def test_digits(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "digits.py")