Repository: Textualize/textual

Issue:
Document.end line number is off by one (1-indexed)
`Document.start` and `Document.end` were introduced in https://github.com/Textualize/textual/pull/4267

`Document.start` maps to `(0, 0)`, but `Document.end` is 1-indexed, with its line number computed from `Document.line_count`. This is confusing, and breaks with the standard from other functions that return or accept a `Location`.

For example, this is the current behavior:

```py
>>> from textual.widgets.text_area import Document
>>> doc = Document("foo\nbar")
>>> doc.end
(2, 3)
```

Expected:
```py
>>> doc.end
(1, 3)
```

# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.56.4 |
| Rich    | 13.7.1 |

## Python

| Name           | Value                                                                            |
|----------------|----------------------------------------------------------------------------------|
| Version        | 3.8.10                                                                           |
| Implementation | CPython                                                                          |
| Compiler       | GCC 9.3.0                                                                        |
| Executable     | /home/tco/.cache/pypoetry/virtualenvs/textual-textarea-mNcIuqFZ-py3.8/bin/python |

## Operating System

| Name    | Value                               |
|---------|-------------------------------------|
| System  | Linux                               |
| Release | 5.15.146.1-microsoft-standard-WSL2  |
| Version | #1 SMP Thu Jan 11 04:09:03 UTC 2024 |

## Terminal

| Name                 | Value            |
|----------------------|------------------|
| Terminal Application | Windows Terminal |
| TERM                 | xterm-256color   |
| COLORTERM            | truecolor        |
| FORCE_COLOR          | *Not set*        |
| NO_COLOR             | *Not set*        |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=120, height=30 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 120                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 30                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd0139a5e..cd62457fa 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 an off-by-one error in the line number of the `Document.end` property https://github.com/Textualize/textual/issues/4426
+
 ## [0.57.0] - 2024-04-19
 
 ### Fixed
diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py
index ca850285b..4a63ff205 100644
--- a/src/textual/document/_document.py
+++ b/src/textual/document/_document.py
@@ -351,7 +351,7 @@ class Document(DocumentBase):
     def end(self) -> Location:
         """Returns the location of the end of the document."""
         last_line = self._lines[-1]
-        return (self.line_count, len(last_line))
+        return (self.line_count - 1, len(last_line))
 
     def get_index_from_location(self, location: Location) -> int:
         """Given a location, returns the index from the document's text.


Test patch:
diff --git a/tests/document/test_document.py b/tests/document/test_document.py
index ad1c82b83..ebf8474c8 100644
--- a/tests/document/test_document.py
+++ b/tests/document/test_document.py
@@ -137,3 +137,16 @@ def test_location_from_index(text):
         len(lines) - 1,
         len(lines[-1]),
     )
+
+
+@pytest.mark.parametrize(
+    "text", [TEXT, TEXT_NEWLINE, TEXT_WINDOWS, TEXT_WINDOWS_NEWLINE]
+)
+def test_document_end(text):
+    """The location is always what we expect."""
+    document = Document(text)
+    expected_line_number = (
+        len(text.splitlines()) if text.endswith("\n") else len(text.splitlines()) - 1
+    )
+    expected_pos = 0 if text.endswith("\n") else (len(text.splitlines()[-1]))
+    assert document.end == (expected_line_number, expected_pos)