1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2009 Jean-Sebastien Gelinas <calestar@gmail.com>
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 # Nit source-file and locations in source-file
20 # A raw text Nit source file
22 # The path of the source
25 # The content of the source
26 var string
: String is noinit
28 # The original stream used to initialize `string`
33 string
= stream
.read_all
37 # Create a new sourcefile using a dummy filename and a given content
38 init from_string
(filename
: String, string
: String) is
41 self.filename
= filename
46 # Offset of each line start in the content `string`.
48 # Used for fast access to each line when rendering parts of the `string`.
49 var line_starts
= new Array[Int]
51 # Extract a given line excluding the line-terminators characters.
53 # `line_number` starts at 1 for the first line.
54 fun get_line
(line_number
: Int): String do
55 if line_number
> line_starts
.length
then return ""
56 var line_start
= line_starts
[line_number-1
]
57 var line_end
= line_start
58 var string
= self.string
59 while line_end
+1 < string
.length
and string
.chars
[line_end
+1] != '\n' and string
.chars
[line_end
+1] != '\r' do
62 return string
.substring
(line_start
, line_end-line_start
+1)
66 # A location inside a source file
69 redef type OTHER: Location
71 # The associated source-file
72 var file
: nullable SourceFile
74 # The starting line number (starting from 1)
76 # If `line_start==0` then the whole file is considered
79 # The stopping line number (starting from 1)
82 # Start of this location on `line_start`
84 # A `column_start` of 1 means the first column or character.
86 # If `column_start == 0` this location concerns the whole line.
88 # Require: `column_start >= 0`
91 # End of this location on `line_end`
94 # Builds a location instance from its string representation.
99 # var loc = new Location.from_string("location.nit:82,2--105,8")
100 # assert loc.to_s == "location.nit:82,2--105,8"
102 # loc = new Location.from_string("location.nit")
103 # assert loc.to_s == "location.nit"
105 # loc = new Location.from_string("location.nit:82,2")
106 # assert loc.to_s == "location.nit:82,2--0,0"
108 # loc = new Location.from_string("location.nit:82--105")
109 # assert loc.to_s == "location.nit:82,0--105,0"
111 # loc = new Location.from_string("location.nit:82,2--105")
112 # assert loc.to_s == "location.nit:82,2--105,0"
114 # loc = new Location.from_string("location.nit:82--105,8")
115 # assert loc.to_s == "location.nit:82,0--105,8"
117 init from_string
(string
: String) is
122 self.column_start
= 0
124 # parses the location string and init position vars
125 var parts
= string
.split_with
(":")
126 var filename
= parts
.shift
127 self.file
= new SourceFile(filename
, new FileReader.open
(filename
))
129 if parts
.is_empty
then return
130 var pos
= parts
.first
.split_with
("--")
131 # split start position
132 if pos
.first
.has
(",") then
133 var pos1
= pos
.first
.split_with
(",")
134 self.line_start
= pos1
[0].to_i
135 if pos1
.length
> 1 then
136 self.column_start
= pos1
[1].to_i
139 self.line_start
= pos
.first
.to_i
142 if pos
.length
<= 1 then return
143 if pos
[1].has
(",") then
144 var pos2
= pos
[1].split_with
(",")
145 if pos2
.length
> 1 then
146 self.line_end
= pos2
[0].to_i
147 self.column_end
= pos2
[1].to_i
149 self.line_end
= self.line_start
150 self.column_end
= pos2
[0].to_i
153 self.line_end
= pos
[1].to_i
157 # Initialize a location corresponding to an opaque file.
159 # The path is used as is and is not open nor read.
160 init opaque_file
(path
: String)
162 var source
= new SourceFile.from_string
(path
, "")
163 init(source
, 0, 0, 0, 0)
166 # The index in the start character in the source
167 fun pstart
: Int do return file
.line_starts
[line_start-1
] + column_start-1
169 # The index on the end character in the source
170 fun pend
: Int do return file
.line_starts
[line_end-1
] + column_end-1
172 # The verbatim associated text in the source-file
175 var res
= self.text_cache
176 if res
!= null then return res
178 var pstart
= self.pstart
180 res
= l
.file
.string
.substring
(pstart
, pend-pstart
+1)
181 self.text_cache
= res
185 private var text_cache
: nullable String = null
187 redef fun ==(other
: nullable Object): Bool do
188 if other
== null then return false
189 if not other
isa Location then return false
191 if other
.file
!= file
then return false
192 if other
.line_start
!= line_start
then return false
193 if other
.line_end
!= line_end
then return false
194 if other
.column_start
!= column_start
then return false
195 if other
.column_end
!= column_end
then return false
200 # Is `self` included (or equals) to `loc`?
201 fun located_in
(loc
: nullable Location): Bool do
202 if loc
== null then return false
204 if line_start
< loc
.line_start
then return false
205 if line_start
> loc
.line_end
then return false
207 if line_end
> loc
.line_end
then return false
209 if line_start
== loc
.line_start
then
210 if column_start
< loc
.column_start
then return false
211 if line_start
== loc
.line_end
and column_start
> loc
.column_end
then return false
214 if line_end
== loc
.line_end
and column_end
> loc
.column_end
then return false
219 redef fun to_s
: String do
222 file_part
= file
.filename
225 if line_start
<= 0 then return file_part
227 if file
!= null and file
.filename
.length
> 0 then file_part
+= ":"
229 if line_start
== line_end
then
230 if column_start
== column_end
then
231 return "{file_part}{line_start},{column_start}"
233 return "{file_part}{line_start},{column_start}--{column_end}"
236 return "{file_part}{line_start},{column_start}--{line_end},{column_end}"
240 # Return a location message according to an observer.
242 # Currently, if both are in the same file, the file information is not present in the result.
243 fun relative_to
(loc
: nullable Location): String do
244 var relative
: Location
245 if loc
!= null and loc
.file
== self.file
then
246 relative
= new Location(null, self.line_start
, self.line_end
, self.column_start
, self.column_end
)
248 relative
= new Location(self.file
, self.line_start
, self.line_end
, self.column_start
, self.column_end
)
253 redef fun <(other
: OTHER): Bool do
254 if self == other
then return false
255 if self.located_in
(other
) then return true
256 if other
.located_in
(self) then return false
258 if line_start
!= other
.line_start
then return line_start
< other
.line_start
259 if column_start
!= other
.column_start
then return column_start
< other
.column_start
260 if line_end
!= other
.line_end
then return line_end
< other
.line_end
262 return column_end
< other
.column_end
265 # Return the associated line with the location highlighted with color and a caret under the starting position
266 # `color` must be and terminal escape sequence used as `"{escape}[{color}m;"`
268 # * `"1;31"` for bright red
269 # * `"0;32"` for green
270 fun colored_line
(color
: String): String
272 var esc
= 27.code_point
274 var col
= "{esc}[{color}m"
278 if i
<= 0 then return ""
280 var line_start
= l
.file
.line_starts
[i-1
]
281 var line_end
= line_start
282 var string
= l
.file
.string
283 while line_end
+1 < string
.length
and string
.chars
[line_end
+1] != '\n' and string
.chars
[line_end
+1] != '\r' do
287 if l
.column_start
> 0 then
288 lstart
= string
.substring
(line_start
, l
.column_start
- 1)
293 if i
!= l
.line_end
then
294 cend
= line_end
- line_start
+ 1
300 if line_start
+ cend
<= string
.length
then
301 lmid
= string
.substring
(line_start
+ l
.column_start
- 1, cend
- l
.column_start
+ 1)
302 lend
= string
.substring
(line_start
+ cend
, line_end
- line_start
- cend
+ 1)
307 var indent
= new FlatBuffer
308 for j
in [line_start
..line_start
+l
.column_start-1
[ do
309 if string
.chars
[j
] == '\t' then
315 return "\t{lstart}{col}{lmid}{def}{lend}\n\t{indent}^"