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+ try :
56+ with open (filename , 'rb' ) as f :
57+ byteLines = f .readlines ()
58+ except Exception :
59+ byteLines = []
60+ i = 0
61+ def nextLine () -> bytes :
62+ nonlocal i
63+ if i < len (byteLines ):
64+ x = byteLines [i ]
65+ i = i + 1
66+ return x
67+ else :
68+ return b''
69+ encoding , _ = tokenize .detect_encoding (nextLine )
70+ lines = EncodedByteLines (byteLines , encoding )
71+ if 1 <= lineno <= len (lines .bytes ):
72+ x = lines .bytes [lineno - 1 ].rstrip (b'\n ' )
73+ else :
74+ x = b''
75+ return EncodedBytes (x , encoding )
1676
1777@dataclass
1878class Loc :
@@ -38,7 +98,7 @@ def code(self) -> Optional[str]:
3898 case (startLine , startCol , endLine , endCol ):
3999 result = []
40100 for lineNo in range (startLine , startLine + 1 ):
41- line = linecache . getline (self .filename , lineNo ). rstrip ( " \n " )
101+ line = getline (self .filename , lineNo )
42102 c1 = startCol if lineNo == startLine else 0
43103 c2 = endCol if lineNo == endLine else len (line )
44104 result .append (line [c1 :c2 ])
@@ -84,27 +144,27 @@ def highlight(s: str, mode: HighlightMode) -> str:
84144
85145@dataclass
86146class SourceLine :
87- line : str # without trailing \n
147+ line : EncodedBytes # without trailing \n
88148 span : Optional [tuple [int , int ]] # (inclusive, exclusive)
89149
90- def highlight (self , mode : HighlightMode | Literal ['fromEnv' ] = 'fromEnv' ):
150+ def highlight (self , mode : HighlightMode | Literal ['fromEnv' ] = 'fromEnv' ) -> str :
91151 mode = getHighlightMode (mode )
92152 if self .span :
93153 l = self .line
94154 return l [:self .span [0 ]] + highlight (l [self .span [0 ]:self .span [1 ]], mode ) + l [self .span [1 ]:]
95155 else :
96- return self .line
156+ return self .line . decoded ()
97157
98158def highlightedLines (loc : Loc ) -> list [SourceLine ]:
99159 match loc .fullSpan ():
100160 case None :
101- line = linecache . getline (loc .filename , loc .startLine ). rstrip ( " \n " )
161+ line = getline (loc .filename , loc .startLine )
102162 return [SourceLine (line , None )]
103163 case (startLine , startCol , endLine , endCol ):
104164 result = []
105165 for lineNo in range (startLine , startLine + 1 ):
106- line = linecache . getline (loc .filename , lineNo ). rstrip ( " \n " )
107- leadingSpaces = len ( line ) - len ( line . lstrip () )
166+ line = getline (loc .filename , lineNo )
167+ leadingSpaces = line . countLeadingSpaces ( )
108168 c1 = startCol if lineNo == startLine else leadingSpaces
109169 c2 = endCol if lineNo == endLine else len (line )
110170 result .append (SourceLine (line , (c1 , c2 )))
0 commit comments