1313import parsecache
1414from parsecache import FunMatcher
1515import 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
1875class 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
86143class 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
98155def 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 )))
0 commit comments