Skip to content

Commit 5dbd5af

Browse files
committed
fix highlighting with umlauts
1 parent 2a6e976 commit 5dbd5af

File tree

6 files changed

+89
-9
lines changed

6 files changed

+89
-9
lines changed

python/code/wypp/location.py

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,63 @@
1313
import parsecache
1414
from parsecache import FunMatcher
1515
import paths
16+
import tokenize
17+
import os
18+
19+
@dataclass
20+
class EncodedBytes:
21+
bytes: bytes
22+
encoding: str
23+
def __len__(self):
24+
return len(self.bytes)
25+
def countLeadingSpaces(self) -> int:
26+
return len(self.bytes) - len(self.bytes.lstrip())
27+
def decoded(self) -> str:
28+
return self.bytes.decode(self.encoding, errors='replace')
29+
@overload
30+
def __getitem__(self, key: int) -> int: ...
31+
@overload
32+
def __getitem__(self, key: slice) -> str: ...
33+
def __getitem__(self, key: int | slice) -> int | str:
34+
if isinstance(key, int):
35+
return self.bytes[key]
36+
else:
37+
b = self.bytes[key]
38+
return b.decode(self.encoding, errors='replace')
39+
40+
@dataclass
41+
class EncodedByteLines:
42+
bytes: list[bytes]
43+
encoding: str
44+
45+
_cache: dict[str, EncodedByteLines] = {}
46+
def getline(filename, lineno):
47+
"""
48+
Returns a line of some source file as a bytearray. We use byte arrays because
49+
location offsets are byte offsets.
50+
"""
51+
p = os.path.normpath(os.path.abspath(filename))
52+
if p in _cache:
53+
lines = _cache[p]
54+
else:
55+
with open(filename, 'rb') as f:
56+
byteLines = f.readlines()
57+
i = 0
58+
def nextLine() -> bytes:
59+
nonlocal i
60+
if i < len(byteLines):
61+
x = byteLines[i]
62+
i = i + 1
63+
return x
64+
else:
65+
return b''
66+
encoding, _ = tokenize.detect_encoding(nextLine)
67+
lines = EncodedByteLines(byteLines, encoding)
68+
if 1 <= lineno <= len(lines.bytes):
69+
x = lines.bytes[lineno - 1].rstrip(b'\n')
70+
else:
71+
x = b''
72+
return EncodedBytes(x, encoding)
1673

1774
@dataclass
1875
class Loc:
@@ -38,7 +95,7 @@ def code(self) -> Optional[str]:
3895
case (startLine, startCol, endLine, endCol):
3996
result = []
4097
for lineNo in range(startLine, startLine+1):
41-
line = linecache.getline(self.filename, lineNo).rstrip("\n")
98+
line = getline(self.filename, lineNo)
4299
c1 = startCol if lineNo == startLine else 0
43100
c2 = endCol if lineNo == endLine else len(line)
44101
result.append(line[c1:c2])
@@ -84,27 +141,27 @@ def highlight(s: str, mode: HighlightMode) -> str:
84141

85142
@dataclass
86143
class SourceLine:
87-
line: str # without trailing \n
144+
line: EncodedBytes # without trailing \n
88145
span: Optional[tuple[int, int]] # (inclusive, exclusive)
89146

90-
def highlight(self, mode: HighlightMode | Literal['fromEnv'] = 'fromEnv'):
147+
def highlight(self, mode: HighlightMode | Literal['fromEnv'] = 'fromEnv') -> str:
91148
mode = getHighlightMode(mode)
92149
if self.span:
93150
l = self.line
94151
return l[:self.span[0]] + highlight(l[self.span[0]:self.span[1]], mode) + l[self.span[1]:]
95152
else:
96-
return self.line
153+
return self.line.decoded()
97154

98155
def highlightedLines(loc: Loc) -> list[SourceLine]:
99156
match loc.fullSpan():
100157
case None:
101-
line = linecache.getline(loc.filename, loc.startLine).rstrip("\n")
158+
line = getline(loc.filename, loc.startLine)
102159
return [SourceLine(line, None)]
103160
case (startLine, startCol, endLine, endCol):
104161
result = []
105162
for lineNo in range(startLine, startLine+1):
106-
line = linecache.getline(loc.filename, lineNo).rstrip("\n")
107-
leadingSpaces = len(line) - len(line.lstrip())
163+
line = getline(loc.filename, lineNo)
164+
leadingSpaces = line.countLeadingSpaces()
108165
c1 = startCol if lineNo == startLine else leadingSpaces
109166
c2 = endCol if lineNo == endLine else len(line)
110167
result.append(SourceLine(line, (c1, c2)))
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Traceback (most recent call last):
2+
File "file-test-data/basics/umlaute.py", line 6, in <module>
3+
check(äüßö("1"), 1)
4+
5+
WyppTypeError: "1"
6+
7+
Der Aufruf der Funktion `äüßö` erwartet einen Wert vom Typ `int` als erstes Argument.
8+
Aber der übergebene Wert hat den Typ `str`.
9+
10+
## Datei file-test-data/basics/umlaute.py
11+
## Fehlerhafter Aufruf in Zeile 6:
12+
13+
check(äüßö("1"), 1)
14+
15+
## Typ deklariert in Zeile 3:
16+
17+
def äüßö(x: int) -> int:

python/file-test-data/basics/umlaute.out

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from wypp import *
2+
3+
def äüßö(x: int) -> int:
4+
return x + 1
5+
6+
check(äüßö("1"), 1)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Traceback (most recent call last):
22
File "file-test-data/extras/testImpossible.py", line 3, in <module>
33
impossible()
4-
File "code/wypp/writeYourProgram.py", line 334, in impossible
4+
File "code/wypp/writeYourProgram.py", line 318, in impossible
55
raise errors.ImpossibleError(msg)
66

77
Das Unmögliche ist passiert!
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Traceback (most recent call last):
22
File "file-test-data/extras/testTodo.py", line 3, in <module>
33
todo()
4-
File "code/wypp/writeYourProgram.py", line 328, in todo
4+
File "code/wypp/writeYourProgram.py", line 312, in todo
55
raise errors.TodoError(msg)
66

77
TODO

0 commit comments

Comments
 (0)