We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
CSS error reporting sometimes off by one
See https://github.com/Textualize/textual/pull/3582#issuecomment-1787507687.

Running the app at the bottom produces the error below, where the error reporting is off by one.
See the line above the panel and the code lines in the code snippet printed.

```
 Error in stylesheet:
 /Users/davep/develop/python/textual-upstream/sandbox/foo.py, CSSErrorApp.CSS:1:4
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│   1 │                                                                                                                                                                                                                      │
│ ❱ 2 │   :                                                                                                                                                                                                                  │
│   3                                                                                                                                                                                                                        │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
 • Expected one of 'comment line', 'comment start', 'selector start', 'selector start class', 'selector start id', 'selector start universal', 'variable name', or 'whitespace'.                                              
 • Did you forget a semicolon at the end of a line?      
```

```py
from textual.app import App, ComposeResult
from textual.widgets import Label

class CSSErrorApp(App[None]):

    CSS = """
    :
    """

    def compose(self) -> ComposeResult:
        yield Label()

if __name__ == "__main__":
    CSSErrorApp().run()
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/textual/css/tokenizer.py`
Content:
```
1 from __future__ import annotations
2 
3 import re
4 from typing import TYPE_CHECKING, NamedTuple
5 
6 import rich.repr
7 from rich.console import Group, RenderableType
8 from rich.highlighter import ReprHighlighter
9 from rich.padding import Padding
10 from rich.panel import Panel
11 from rich.syntax import Syntax
12 from rich.text import Text
13 
14 from ..suggestions import get_suggestion
15 from ._error_tools import friendly_list
16 from .constants import VALID_PSEUDO_CLASSES
17 
18 if TYPE_CHECKING:
19     from .types import CSSLocation
20 
21 
22 class TokenError(Exception):
23     """Error raised when the CSS cannot be tokenized (syntax error)."""
24 
25     def __init__(
26         self,
27         read_from: CSSLocation,
28         code: str,
29         start: tuple[int, int],
30         message: str,
31         end: tuple[int, int] | None = None,
32     ) -> None:
33         """
34         Args:
35             read_from: The location where the CSS was read from.
36             code: The code being parsed.
37             start: Line number of the error.
38             message: A message associated with the error.
39             end: End location of token, or None if not known.
40         """
41 
42         self.read_from = read_from
43         self.code = code
44         self.start = start
45         self.end = end or start
46         super().__init__(message)
47 
48     def _get_snippet(self) -> Panel:
49         """Get a short snippet of code around a given line number.
50 
51         Returns:
52             A renderable.
53         """
54         line_no = self.start[0]
55         # TODO: Highlight column number
56         syntax = Syntax(
57             self.code,
58             lexer="scss",
59             theme="ansi_light",
60             line_numbers=True,
61             indent_guides=True,
62             line_range=(max(0, line_no - 2), line_no + 2),
63             highlight_lines={line_no + 1},
64         )
65         syntax.stylize_range("reverse bold", self.start, self.end)
66         return Panel(syntax, border_style="red")
67 
68     def __rich__(self) -> RenderableType:
69         highlighter = ReprHighlighter()
70         errors: list[RenderableType] = []
71 
72         message = str(self)
73         errors.append(Text(" Error in stylesheet:", style="bold red"))
74 
75         line_no, col_no = self.start
76 
77         path, widget_variable = self.read_from
78         if widget_variable:
79             css_location = f" {path}, {widget_variable}:{line_no}:{col_no}"
80         else:
81             css_location = f" {path}:{line_no}:{col_no}"
82         errors.append(highlighter(css_location))
83         errors.append(self._get_snippet())
84 
85         final_message = "\n".join(
86             f"• {message_part.strip()}" for message_part in message.split(";")
87         )
88         errors.append(
89             Padding(
90                 highlighter(
91                     Text(final_message, "red"),
92                 ),
93                 pad=(0, 1),
94             )
95         )
96 
97         return Group(*errors)
98 
99 
100 class EOFError(TokenError):
101     pass
102 
103 
104 class Expect:
105     def __init__(self, **tokens: str) -> None:
106         self.names = list(tokens.keys())
107         self.regexes = list(tokens.values())
108         self._regex = re.compile(
109             "("
110             + "|".join(f"(?P<{name}>{regex})" for name, regex in tokens.items())
111             + ")"
112         )
113         self.match = self._regex.match
114         self.search = self._regex.search
115         self._expect_eof = False
116 
117     def expect_eof(self, eof: bool) -> Expect:
118         self._expect_eof = eof
119         return self
120 
121     def __rich_repr__(self) -> rich.repr.Result:
122         yield from zip(self.names, self.regexes)
123 
124 
125 class ReferencedBy(NamedTuple):
126     name: str
127     location: tuple[int, int]
128     length: int
129     code: str
130 
131 
132 @rich.repr.auto
133 class Token(NamedTuple):
134     name: str
135     value: str
136     read_from: CSSLocation
137     code: str
138     location: tuple[int, int]
139     referenced_by: ReferencedBy | None = None
140 
141     @property
142     def start(self) -> tuple[int, int]:
143         """Start line and column (1 indexed)."""
144         line, offset = self.location
145         return (line + 1, offset)
146 
147     @property
148     def end(self) -> tuple[int, int]:
149         """End line and column (1 indexed)."""
150         line, offset = self.location
151         return (line + 1, offset + len(self.value))
152 
153     def with_reference(self, by: ReferencedBy | None) -> "Token":
154         """Return a copy of the Token, with reference information attached.
155         This is used for variable substitution, where a variable reference
156         can refer to tokens which were defined elsewhere. With the additional
157         ReferencedBy data attached, we can track where the token we are referring
158         to is used.
159         """
160         return Token(
161             name=self.name,
162             value=self.value,
163             read_from=self.read_from,
164             code=self.code,
165             location=self.location,
166             referenced_by=by,
167         )
168 
169     def __str__(self) -> str:
170         return self.value
171 
172     def __rich_repr__(self) -> rich.repr.Result:
173         yield "name", self.name
174         yield "value", self.value
175         yield (
176             "read_from",
177             self.read_from[0] if not self.read_from[1] else self.read_from,
178         )
179         yield "code", self.code if len(self.code) < 40 else self.code[:40] + "..."
180         yield "location", self.location
181         yield "referenced_by", self.referenced_by, None
182 
183 
184 class Tokenizer:
185     def __init__(self, text: str, read_from: CSSLocation = ("", "")) -> None:
186         self.read_from = read_from
187         self.code = text
188         self.lines = text.splitlines(keepends=True)
189         self.line_no = 0
190         self.col_no = 0
191 
192     def get_token(self, expect: Expect) -> Token:
193         line_no = self.line_no
194         col_no = self.col_no
195         if line_no >= len(self.lines):
196             if expect._expect_eof:
197                 return Token(
198                     "eof",
199                     "",
200                     self.read_from,
201                     self.code,
202                     (line_no + 1, col_no + 1),
203                     None,
204                 )
205             else:
206                 raise EOFError(
207                     self.read_from,
208                     self.code,
209                     (line_no + 1, col_no + 1),
210                     "Unexpected end of file",
211                 )
212         line = self.lines[line_no]
213         match = expect.match(line, col_no)
214         if match is None:
215             expected = friendly_list(" ".join(name.split("_")) for name in expect.names)
216             message = f"Expected one of {expected}.; Did you forget a semicolon at the end of a line?"
217             raise TokenError(
218                 self.read_from,
219                 self.code,
220                 (line_no, col_no),
221                 message,
222             )
223         iter_groups = iter(match.groups())
224 
225         next(iter_groups)
226 
227         for name, value in zip(expect.names, iter_groups):
228             if value is not None:
229                 break
230         else:
231             # For MyPy's benefit
232             raise AssertionError("can't reach here")
233 
234         token = Token(
235             name,
236             value,
237             self.read_from,
238             self.code,
239             (line_no, col_no),
240             referenced_by=None,
241         )
242 
243         if (
244             token.name == "pseudo_class"
245             and token.value.strip(":") not in VALID_PSEUDO_CLASSES
246         ):
247             pseudo_class = token.value.strip(":")
248             suggestion = get_suggestion(pseudo_class, list(VALID_PSEUDO_CLASSES))
249             all_valid = f"must be one of {friendly_list(VALID_PSEUDO_CLASSES)}"
250             if suggestion:
251                 raise TokenError(
252                     self.read_from,
253                     self.code,
254                     (line_no, col_no),
255                     f"unknown pseudo-class {pseudo_class!r}; did you mean {suggestion!r}?; {all_valid}",
256                 )
257             else:
258                 raise TokenError(
259                     self.read_from,
260                     self.code,
261                     (line_no, col_no),
262                     f"unknown pseudo-class {pseudo_class!r}; {all_valid}",
263                 )
264 
265         col_no += len(value)
266         if col_no >= len(line):
267             line_no += 1
268             col_no = 0
269         self.line_no = line_no
270         self.col_no = col_no
271         return token
272 
273     def skip_to(self, expect: Expect) -> Token:
274         line_no = self.line_no
275         col_no = self.col_no
276 
277         while True:
278             if line_no >= len(self.lines):
279                 raise EOFError(
280                     self.read_from,
281                     self.code,
282                     (line_no, col_no),
283                     "Unexpected end of file",
284                 )
285             line = self.lines[line_no]
286             match = expect.search(line, col_no)
287 
288             if match is None:
289                 line_no += 1
290                 col_no = 0
291             else:
292                 self.line_no = line_no
293                 self.col_no = match.span(0)[0]
294                 return self.get_token(expect)
295
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/textual/css/tokenizer.py b/src/textual/css/tokenizer.py
--- a/src/textual/css/tokenizer.py
+++ b/src/textual/css/tokenizer.py
@@ -34,9 +34,9 @@
         Args:
             read_from: The location where the CSS was read from.
             code: The code being parsed.
-            start: Line number of the error.
+            start: Line and column number of the error (1-indexed).
             message: A message associated with the error.
-            end: End location of token, or None if not known.
+            end: End location of token (1-indexed), or None if not known.
         """
 
         self.read_from = read_from
@@ -60,9 +60,13 @@
             line_numbers=True,
             indent_guides=True,
             line_range=(max(0, line_no - 2), line_no + 2),
-            highlight_lines={line_no + 1},
+            highlight_lines={line_no},
+        )
+        syntax.stylize_range(
+            "reverse bold",
+            (self.start[0], self.start[1] - 1),
+            (self.end[0], self.end[1] - 1),
         )
-        syntax.stylize_range("reverse bold", self.start, self.end)
         return Panel(syntax, border_style="red")
 
     def __rich__(self) -> RenderableType:
@@ -136,19 +140,20 @@
     read_from: CSSLocation
     code: str
     location: tuple[int, int]
+    """Token starting location, 0-indexed."""
     referenced_by: ReferencedBy | None = None
 
     @property
     def start(self) -> tuple[int, int]:
-        """Start line and column (1 indexed)."""
+        """Start line and column (1-indexed)."""
         line, offset = self.location
-        return (line + 1, offset)
+        return (line + 1, offset + 1)
 
     @property
     def end(self) -> tuple[int, int]:
-        """End line and column (1 indexed)."""
+        """End line and column (1-indexed)."""
         line, offset = self.location
-        return (line + 1, offset + len(self.value))
+        return (line + 1, offset + len(self.value) + 1)
 
     def with_reference(self, by: ReferencedBy | None) -> "Token":
         """Return a copy of the Token, with reference information attached.
@@ -199,7 +204,7 @@
                     "",
                     self.read_from,
                     self.code,
-                    (line_no + 1, col_no + 1),
+                    (line_no, col_no),
                     None,
                 )
             else:
@@ -217,7 +222,7 @@
             raise TokenError(
                 self.read_from,
                 self.code,
-                (line_no, col_no),
+                (line_no + 1, col_no + 1),
                 message,
             )
         iter_groups = iter(match.groups())
@@ -251,14 +256,14 @@
                 raise TokenError(
                     self.read_from,
                     self.code,
-                    (line_no, col_no),
+                    (line_no + 1, col_no + 1),
                     f"unknown pseudo-class {pseudo_class!r}; did you mean {suggestion!r}?; {all_valid}",
                 )
             else:
                 raise TokenError(
                     self.read_from,
                     self.code,
-                    (line_no, col_no),
+                    (line_no + 1, col_no + 1),
                     f"unknown pseudo-class {pseudo_class!r}; {all_valid}",
                 )

{"golden_diff": "diff --git a/src/textual/css/tokenizer.py b/src/textual/css/tokenizer.py\n--- a/src/textual/css/tokenizer.py\n+++ b/src/textual/css/tokenizer.py\n@@ -34,9 +34,9 @@\n         Args:\n             read_from: The location where the CSS was read from.\n             code: The code being parsed.\n-            start: Line number of the error.\n+            start: Line and column number of the error (1-indexed).\n             message: A message associated with the error.\n-            end: End location of token, or None if not known.\n+            end: End location of token (1-indexed), or None if not known.\n         \"\"\"\n \n         self.read_from = read_from\n@@ -60,9 +60,13 @@\n             line_numbers=True,\n             indent_guides=True,\n             line_range=(max(0, line_no - 2), line_no + 2),\n-            highlight_lines={line_no + 1},\n+            highlight_lines={line_no},\n+        )\n+        syntax.stylize_range(\n+            \"reverse bold\",\n+            (self.start[0], self.start[1] - 1),\n+            (self.end[0], self.end[1] - 1),\n         )\n-        syntax.stylize_range(\"reverse bold\", self.start, self.end)\n         return Panel(syntax, border_style=\"red\")\n \n     def __rich__(self) -> RenderableType:\n@@ -136,19 +140,20 @@\n     read_from: CSSLocation\n     code: str\n     location: tuple[int, int]\n+    \"\"\"Token starting location, 0-indexed.\"\"\"\n     referenced_by: ReferencedBy | None = None\n \n     @property\n     def start(self) -> tuple[int, int]:\n-        \"\"\"Start line and column (1 indexed).\"\"\"\n+        \"\"\"Start line and column (1-indexed).\"\"\"\n         line, offset = self.location\n-        return (line + 1, offset)\n+        return (line + 1, offset + 1)\n \n     @property\n     def end(self) -> tuple[int, int]:\n-        \"\"\"End line and column (1 indexed).\"\"\"\n+        \"\"\"End line and column (1-indexed).\"\"\"\n         line, offset = self.location\n-        return (line + 1, offset + len(self.value))\n+        return (line + 1, offset + len(self.value) + 1)\n \n     def with_reference(self, by: ReferencedBy | None) -> \"Token\":\n         \"\"\"Return a copy of the Token, with reference information attached.\n@@ -199,7 +204,7 @@\n                     \"\",\n                     self.read_from,\n                     self.code,\n-                    (line_no + 1, col_no + 1),\n+                    (line_no, col_no),\n                     None,\n                 )\n             else:\n@@ -217,7 +222,7 @@\n             raise TokenError(\n                 self.read_from,\n                 self.code,\n-                (line_no, col_no),\n+                (line_no + 1, col_no + 1),\n                 message,\n             )\n         iter_groups = iter(match.groups())\n@@ -251,14 +256,14 @@\n                 raise TokenError(\n                     self.read_from,\n                     self.code,\n-                    (line_no, col_no),\n+                    (line_no + 1, col_no + 1),\n                     f\"unknown pseudo-class {pseudo_class!r}; did you mean {suggestion!r}?; {all_valid}\",\n                 )\n             else:\n                 raise TokenError(\n                     self.read_from,\n                     self.code,\n-                    (line_no, col_no),\n+                    (line_no + 1, col_no + 1),\n                     f\"unknown pseudo-class {pseudo_class!r}; {all_valid}\",\n                 )\n", "issue": "CSS error reporting sometimes off by one\nSee https://github.com/Textualize/textual/pull/3582#issuecomment-1787507687.\r\n\r\nRunning the app at the bottom produces the error below, where the error reporting is off by one.\r\nSee the line above the panel and the code lines in the code snippet printed.\r\n\r\n```\r\n Error in stylesheet:\r\n /Users/davep/develop/python/textual-upstream/sandbox/foo.py, CSSErrorApp.CSS:1:4\r\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\r\n\u2502   1 \u2502                                                                                                                                                                                                                      \u2502\r\n\u2502 \u2771 2 \u2502   :                                                                                                                                                                                                                  \u2502\r\n\u2502   3                                                                                                                                                                                                                        \u2502\r\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\r\n \u2022 Expected one of 'comment line', 'comment start', 'selector start', 'selector start class', 'selector start id', 'selector start universal', 'variable name', or 'whitespace'.                                              \r\n \u2022 Did you forget a semicolon at the end of a line?      \r\n```\r\n\r\n```py\r\nfrom textual.app import App, ComposeResult\r\nfrom textual.widgets import Label\r\n\r\nclass CSSErrorApp(App[None]):\r\n\r\n    CSS = \"\"\"\r\n    :\r\n    \"\"\"\r\n\r\n    def compose(self) -> ComposeResult:\r\n        yield Label()\r\n\r\nif __name__ == \"__main__\":\r\n    CSSErrorApp().run()\r\n```\n", "before_files": [{"content": "from __future__ import annotations\n\nimport re\nfrom typing import TYPE_CHECKING, NamedTuple\n\nimport rich.repr\nfrom rich.console import Group, RenderableType\nfrom rich.highlighter import ReprHighlighter\nfrom rich.padding import Padding\nfrom rich.panel import Panel\nfrom rich.syntax import Syntax\nfrom rich.text import Text\n\nfrom ..suggestions import get_suggestion\nfrom ._error_tools import friendly_list\nfrom .constants import VALID_PSEUDO_CLASSES\n\nif TYPE_CHECKING:\n    from .types import CSSLocation\n\n\nclass TokenError(Exception):\n    \"\"\"Error raised when the CSS cannot be tokenized (syntax error).\"\"\"\n\n    def __init__(\n        self,\n        read_from: CSSLocation,\n        code: str,\n        start: tuple[int, int],\n        message: str,\n        end: tuple[int, int] | None = None,\n    ) -> None:\n        \"\"\"\n        Args:\n            read_from: The location where the CSS was read from.\n            code: The code being parsed.\n            start: Line number of the error.\n            message: A message associated with the error.\n            end: End location of token, or None if not known.\n        \"\"\"\n\n        self.read_from = read_from\n        self.code = code\n        self.start = start\n        self.end = end or start\n        super().__init__(message)\n\n    def _get_snippet(self) -> Panel:\n        \"\"\"Get a short snippet of code around a given line number.\n\n        Returns:\n            A renderable.\n        \"\"\"\n        line_no = self.start[0]\n        # TODO: Highlight column number\n        syntax = Syntax(\n            self.code,\n            lexer=\"scss\",\n            theme=\"ansi_light\",\n            line_numbers=True,\n            indent_guides=True,\n            line_range=(max(0, line_no - 2), line_no + 2),\n            highlight_lines={line_no + 1},\n        )\n        syntax.stylize_range(\"reverse bold\", self.start, self.end)\n        return Panel(syntax, border_style=\"red\")\n\n    def __rich__(self) -> RenderableType:\n        highlighter = ReprHighlighter()\n        errors: list[RenderableType] = []\n\n        message = str(self)\n        errors.append(Text(\" Error in stylesheet:\", style=\"bold red\"))\n\n        line_no, col_no = self.start\n\n        path, widget_variable = self.read_from\n        if widget_variable:\n            css_location = f\" {path}, {widget_variable}:{line_no}:{col_no}\"\n        else:\n            css_location = f\" {path}:{line_no}:{col_no}\"\n        errors.append(highlighter(css_location))\n        errors.append(self._get_snippet())\n\n        final_message = \"\\n\".join(\n            f\"\u2022 {message_part.strip()}\" for message_part in message.split(\";\")\n        )\n        errors.append(\n            Padding(\n                highlighter(\n                    Text(final_message, \"red\"),\n                ),\n                pad=(0, 1),\n            )\n        )\n\n        return Group(*errors)\n\n\nclass EOFError(TokenError):\n    pass\n\n\nclass Expect:\n    def __init__(self, **tokens: str) -> None:\n        self.names = list(tokens.keys())\n        self.regexes = list(tokens.values())\n        self._regex = re.compile(\n            \"(\"\n            + \"|\".join(f\"(?P<{name}>{regex})\" for name, regex in tokens.items())\n            + \")\"\n        )\n        self.match = self._regex.match\n        self.search = self._regex.search\n        self._expect_eof = False\n\n    def expect_eof(self, eof: bool) -> Expect:\n        self._expect_eof = eof\n        return self\n\n    def __rich_repr__(self) -> rich.repr.Result:\n        yield from zip(self.names, self.regexes)\n\n\nclass ReferencedBy(NamedTuple):\n    name: str\n    location: tuple[int, int]\n    length: int\n    code: str\n\n\n@rich.repr.auto\nclass Token(NamedTuple):\n    name: str\n    value: str\n    read_from: CSSLocation\n    code: str\n    location: tuple[int, int]\n    referenced_by: ReferencedBy | None = None\n\n    @property\n    def start(self) -> tuple[int, int]:\n        \"\"\"Start line and column (1 indexed).\"\"\"\n        line, offset = self.location\n        return (line + 1, offset)\n\n    @property\n    def end(self) -> tuple[int, int]:\n        \"\"\"End line and column (1 indexed).\"\"\"\n        line, offset = self.location\n        return (line + 1, offset + len(self.value))\n\n    def with_reference(self, by: ReferencedBy | None) -> \"Token\":\n        \"\"\"Return a copy of the Token, with reference information attached.\n        This is used for variable substitution, where a variable reference\n        can refer to tokens which were defined elsewhere. With the additional\n        ReferencedBy data attached, we can track where the token we are referring\n        to is used.\n        \"\"\"\n        return Token(\n            name=self.name,\n            value=self.value,\n            read_from=self.read_from,\n            code=self.code,\n            location=self.location,\n            referenced_by=by,\n        )\n\n    def __str__(self) -> str:\n        return self.value\n\n    def __rich_repr__(self) -> rich.repr.Result:\n        yield \"name\", self.name\n        yield \"value\", self.value\n        yield (\n            \"read_from\",\n            self.read_from[0] if not self.read_from[1] else self.read_from,\n        )\n        yield \"code\", self.code if len(self.code) < 40 else self.code[:40] + \"...\"\n        yield \"location\", self.location\n        yield \"referenced_by\", self.referenced_by, None\n\n\nclass Tokenizer:\n    def __init__(self, text: str, read_from: CSSLocation = (\"\", \"\")) -> None:\n        self.read_from = read_from\n        self.code = text\n        self.lines = text.splitlines(keepends=True)\n        self.line_no = 0\n        self.col_no = 0\n\n    def get_token(self, expect: Expect) -> Token:\n        line_no = self.line_no\n        col_no = self.col_no\n        if line_no >= len(self.lines):\n            if expect._expect_eof:\n                return Token(\n                    \"eof\",\n                    \"\",\n                    self.read_from,\n                    self.code,\n                    (line_no + 1, col_no + 1),\n                    None,\n                )\n            else:\n                raise EOFError(\n                    self.read_from,\n                    self.code,\n                    (line_no + 1, col_no + 1),\n                    \"Unexpected end of file\",\n                )\n        line = self.lines[line_no]\n        match = expect.match(line, col_no)\n        if match is None:\n            expected = friendly_list(\" \".join(name.split(\"_\")) for name in expect.names)\n            message = f\"Expected one of {expected}.; Did you forget a semicolon at the end of a line?\"\n            raise TokenError(\n                self.read_from,\n                self.code,\n                (line_no, col_no),\n                message,\n            )\n        iter_groups = iter(match.groups())\n\n        next(iter_groups)\n\n        for name, value in zip(expect.names, iter_groups):\n            if value is not None:\n                break\n        else:\n            # For MyPy's benefit\n            raise AssertionError(\"can't reach here\")\n\n        token = Token(\n            name,\n            value,\n            self.read_from,\n            self.code,\n            (line_no, col_no),\n            referenced_by=None,\n        )\n\n        if (\n            token.name == \"pseudo_class\"\n            and token.value.strip(\":\") not in VALID_PSEUDO_CLASSES\n        ):\n            pseudo_class = token.value.strip(\":\")\n            suggestion = get_suggestion(pseudo_class, list(VALID_PSEUDO_CLASSES))\n            all_valid = f\"must be one of {friendly_list(VALID_PSEUDO_CLASSES)}\"\n            if suggestion:\n                raise TokenError(\n                    self.read_from,\n                    self.code,\n                    (line_no, col_no),\n                    f\"unknown pseudo-class {pseudo_class!r}; did you mean {suggestion!r}?; {all_valid}\",\n                )\n            else:\n                raise TokenError(\n                    self.read_from,\n                    self.code,\n                    (line_no, col_no),\n                    f\"unknown pseudo-class {pseudo_class!r}; {all_valid}\",\n                )\n\n        col_no += len(value)\n        if col_no >= len(line):\n            line_no += 1\n            col_no = 0\n        self.line_no = line_no\n        self.col_no = col_no\n        return token\n\n    def skip_to(self, expect: Expect) -> Token:\n        line_no = self.line_no\n        col_no = self.col_no\n\n        while True:\n            if line_no >= len(self.lines):\n                raise EOFError(\n                    self.read_from,\n                    self.code,\n                    (line_no, col_no),\n                    \"Unexpected end of file\",\n                )\n            line = self.lines[line_no]\n            match = expect.search(line, col_no)\n\n            if match is None:\n                line_no += 1\n                col_no = 0\n            else:\n                self.line_no = line_no\n                self.col_no = match.span(0)[0]\n                return self.get_token(expect)\n", "path": "src/textual/css/tokenizer.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport re\nfrom typing import TYPE_CHECKING, NamedTuple\n\nimport rich.repr\nfrom rich.console import Group, RenderableType\nfrom rich.highlighter import ReprHighlighter\nfrom rich.padding import Padding\nfrom rich.panel import Panel\nfrom rich.syntax import Syntax\nfrom rich.text import Text\n\nfrom ..suggestions import get_suggestion\nfrom ._error_tools import friendly_list\nfrom .constants import VALID_PSEUDO_CLASSES\n\nif TYPE_CHECKING:\n    from .types import CSSLocation\n\n\nclass TokenError(Exception):\n    \"\"\"Error raised when the CSS cannot be tokenized (syntax error).\"\"\"\n\n    def __init__(\n        self,\n        read_from: CSSLocation,\n        code: str,\n        start: tuple[int, int],\n        message: str,\n        end: tuple[int, int] | None = None,\n    ) -> None:\n        \"\"\"\n        Args:\n            read_from: The location where the CSS was read from.\n            code: The code being parsed.\n            start: Line and column number of the error (1-indexed).\n            message: A message associated with the error.\n            end: End location of token (1-indexed), or None if not known.\n        \"\"\"\n\n        self.read_from = read_from\n        self.code = code\n        self.start = start\n        self.end = end or start\n        super().__init__(message)\n\n    def _get_snippet(self) -> Panel:\n        \"\"\"Get a short snippet of code around a given line number.\n\n        Returns:\n            A renderable.\n        \"\"\"\n        line_no = self.start[0]\n        # TODO: Highlight column number\n        syntax = Syntax(\n            self.code,\n            lexer=\"scss\",\n            theme=\"ansi_light\",\n            line_numbers=True,\n            indent_guides=True,\n            line_range=(max(0, line_no - 2), line_no + 2),\n            highlight_lines={line_no},\n        )\n        syntax.stylize_range(\n            \"reverse bold\",\n            (self.start[0], self.start[1] - 1),\n            (self.end[0], self.end[1] - 1),\n        )\n        return Panel(syntax, border_style=\"red\")\n\n    def __rich__(self) -> RenderableType:\n        highlighter = ReprHighlighter()\n        errors: list[RenderableType] = []\n\n        message = str(self)\n        errors.append(Text(\" Error in stylesheet:\", style=\"bold red\"))\n\n        line_no, col_no = self.start\n\n        path, widget_variable = self.read_from\n        if widget_variable:\n            css_location = f\" {path}, {widget_variable}:{line_no}:{col_no}\"\n        else:\n            css_location = f\" {path}:{line_no}:{col_no}\"\n        errors.append(highlighter(css_location))\n        errors.append(self._get_snippet())\n\n        final_message = \"\\n\".join(\n            f\"\u2022 {message_part.strip()}\" for message_part in message.split(\";\")\n        )\n        errors.append(\n            Padding(\n                highlighter(\n                    Text(final_message, \"red\"),\n                ),\n                pad=(0, 1),\n            )\n        )\n\n        return Group(*errors)\n\n\nclass EOFError(TokenError):\n    pass\n\n\nclass Expect:\n    def __init__(self, **tokens: str) -> None:\n        self.names = list(tokens.keys())\n        self.regexes = list(tokens.values())\n        self._regex = re.compile(\n            \"(\"\n            + \"|\".join(f\"(?P<{name}>{regex})\" for name, regex in tokens.items())\n            + \")\"\n        )\n        self.match = self._regex.match\n        self.search = self._regex.search\n        self._expect_eof = False\n\n    def expect_eof(self, eof: bool) -> Expect:\n        self._expect_eof = eof\n        return self\n\n    def __rich_repr__(self) -> rich.repr.Result:\n        yield from zip(self.names, self.regexes)\n\n\nclass ReferencedBy(NamedTuple):\n    name: str\n    location: tuple[int, int]\n    length: int\n    code: str\n\n\n@rich.repr.auto\nclass Token(NamedTuple):\n    name: str\n    value: str\n    read_from: CSSLocation\n    code: str\n    location: tuple[int, int]\n    \"\"\"Token starting location, 0-indexed.\"\"\"\n    referenced_by: ReferencedBy | None = None\n\n    @property\n    def start(self) -> tuple[int, int]:\n        \"\"\"Start line and column (1-indexed).\"\"\"\n        line, offset = self.location\n        return (line + 1, offset + 1)\n\n    @property\n    def end(self) -> tuple[int, int]:\n        \"\"\"End line and column (1-indexed).\"\"\"\n        line, offset = self.location\n        return (line + 1, offset + len(self.value) + 1)\n\n    def with_reference(self, by: ReferencedBy | None) -> \"Token\":\n        \"\"\"Return a copy of the Token, with reference information attached.\n        This is used for variable substitution, where a variable reference\n        can refer to tokens which were defined elsewhere. With the additional\n        ReferencedBy data attached, we can track where the token we are referring\n        to is used.\n        \"\"\"\n        return Token(\n            name=self.name,\n            value=self.value,\n            read_from=self.read_from,\n            code=self.code,\n            location=self.location,\n            referenced_by=by,\n        )\n\n    def __str__(self) -> str:\n        return self.value\n\n    def __rich_repr__(self) -> rich.repr.Result:\n        yield \"name\", self.name\n        yield \"value\", self.value\n        yield (\n            \"read_from\",\n            self.read_from[0] if not self.read_from[1] else self.read_from,\n        )\n        yield \"code\", self.code if len(self.code) < 40 else self.code[:40] + \"...\"\n        yield \"location\", self.location\n        yield \"referenced_by\", self.referenced_by, None\n\n\nclass Tokenizer:\n    def __init__(self, text: str, read_from: CSSLocation = (\"\", \"\")) -> None:\n        self.read_from = read_from\n        self.code = text\n        self.lines = text.splitlines(keepends=True)\n        self.line_no = 0\n        self.col_no = 0\n\n    def get_token(self, expect: Expect) -> Token:\n        line_no = self.line_no\n        col_no = self.col_no\n        if line_no >= len(self.lines):\n            if expect._expect_eof:\n                return Token(\n                    \"eof\",\n                    \"\",\n                    self.read_from,\n                    self.code,\n                    (line_no, col_no),\n                    None,\n                )\n            else:\n                raise EOFError(\n                    self.read_from,\n                    self.code,\n                    (line_no + 1, col_no + 1),\n                    \"Unexpected end of file\",\n                )\n        line = self.lines[line_no]\n        match = expect.match(line, col_no)\n        if match is None:\n            expected = friendly_list(\" \".join(name.split(\"_\")) for name in expect.names)\n            message = f\"Expected one of {expected}.; Did you forget a semicolon at the end of a line?\"\n            raise TokenError(\n                self.read_from,\n                self.code,\n                (line_no + 1, col_no + 1),\n                message,\n            )\n        iter_groups = iter(match.groups())\n\n        next(iter_groups)\n\n        for name, value in zip(expect.names, iter_groups):\n            if value is not None:\n                break\n        else:\n            # For MyPy's benefit\n            raise AssertionError(\"can't reach here\")\n\n        token = Token(\n            name,\n            value,\n            self.read_from,\n            self.code,\n            (line_no, col_no),\n            referenced_by=None,\n        )\n\n        if (\n            token.name == \"pseudo_class\"\n            and token.value.strip(\":\") not in VALID_PSEUDO_CLASSES\n        ):\n            pseudo_class = token.value.strip(\":\")\n            suggestion = get_suggestion(pseudo_class, list(VALID_PSEUDO_CLASSES))\n            all_valid = f\"must be one of {friendly_list(VALID_PSEUDO_CLASSES)}\"\n            if suggestion:\n                raise TokenError(\n                    self.read_from,\n                    self.code,\n                    (line_no + 1, col_no + 1),\n                    f\"unknown pseudo-class {pseudo_class!r}; did you mean {suggestion!r}?; {all_valid}\",\n                )\n            else:\n                raise TokenError(\n                    self.read_from,\n                    self.code,\n                    (line_no + 1, col_no + 1),\n                    f\"unknown pseudo-class {pseudo_class!r}; {all_valid}\",\n                )\n\n        col_no += len(value)\n        if col_no >= len(line):\n            line_no += 1\n            col_no = 0\n        self.line_no = line_no\n        self.col_no = col_no\n        return token\n\n    def skip_to(self, expect: Expect) -> Token:\n        line_no = self.line_no\n        col_no = self.col_no\n\n        while True:\n            if line_no >= len(self.lines):\n                raise EOFError(\n                    self.read_from,\n                    self.code,\n                    (line_no, col_no),\n                    \"Unexpected end of file\",\n                )\n            line = self.lines[line_no]\n            match = expect.search(line, col_no)\n\n            if match is None:\n                line_no += 1\n                col_no = 0\n            else:\n                self.line_no = line_no\n                self.col_no = match.span(0)[0]\n                return self.get_token(expect)\n", "path": "src/textual/css/tokenizer.py"}]}