nitc/location: add named constructor `opaque_file`
[nit.git] / src / location.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2009 Jean-Sebastien Gelinas <calestar@gmail.com>
4 #
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
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
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.
16
17 # Nit source-file and locations in source-file
18 module location
19
20 # A raw text Nit source file
21 class SourceFile
22 # The path of the source
23 var filename: String
24
25 # The content of the source
26 var string: String is noinit
27
28 # The original stream used to initialize `string`
29 var stream: Reader
30
31 init
32 do
33 string = stream.read_all
34 line_starts[0] = 0
35 end
36
37 # Create a new sourcefile using a dummy filename and a given content
38 init from_string(filename: String, string: String) is
39 nosuper
40 do
41 self.filename = filename
42 self.string = string
43 line_starts[0] = 0
44 end
45
46 # Offset of each line start in the content `string`.
47 #
48 # Used for fast access to each line when rendering parts of the `string`.
49 var line_starts = new Array[Int]
50 end
51
52 # A location inside a source file
53 class Location
54 super Comparable
55 redef type OTHER: Location
56
57 # The associated source-file
58 var file: nullable SourceFile
59
60 # The starting line number (starting from 1)
61 #
62 # If `line_start==0` then the whole file is considered
63 var line_start: Int
64
65 # The stopping line number (starting from 1)
66 var line_end: Int
67
68 # Start of this location on `line_start`
69 #
70 # A `column_start` of 1 means the first column or character.
71 #
72 # If `column_start == 0` this location concerns the whole line.
73 #
74 # Require: `column_start >= 0`
75 var column_start: Int
76
77 # End of this location on `line_end`
78 var column_end: Int
79
80 # Builds a location instance from its string representation.
81 #
82 # Examples:
83 #
84 # ~~~
85 # var loc = new Location.from_string("location.nit:82,2--105,8")
86 # assert loc.to_s == "location.nit:82,2--105,8"
87 #
88 # loc = new Location.from_string("location.nit")
89 # assert loc.to_s == "location.nit"
90 #
91 # loc = new Location.from_string("location.nit:82,2")
92 # assert loc.to_s == "location.nit:82,2--0,0"
93 #
94 # loc = new Location.from_string("location.nit:82--105")
95 # assert loc.to_s == "location.nit:82,0--105,0"
96 #
97 # loc = new Location.from_string("location.nit:82,2--105")
98 # assert loc.to_s == "location.nit:82,2--105,0"
99 #
100 # loc = new Location.from_string("location.nit:82--105,8")
101 # assert loc.to_s == "location.nit:82,0--105,8"
102 # ~~~
103 init from_string(string: String) is
104 nosuper
105 do
106 self.line_start = 0
107 self.line_end = 0
108 self.column_start = 0
109 self.column_end = 0
110 # parses the location string and init position vars
111 var parts = string.split_with(":")
112 var filename = parts.shift
113 self.file = new SourceFile(filename, new FileReader.open(filename))
114 # split position
115 if parts.is_empty then return
116 var pos = parts.first.split_with("--")
117 # split start position
118 if pos.first.has(",") then
119 var pos1 = pos.first.split_with(",")
120 self.line_start = pos1[0].to_i
121 if pos1.length > 1 then
122 self.column_start = pos1[1].to_i
123 end
124 else
125 self.line_start = pos.first.to_i
126 end
127 # split end position
128 if pos.length <= 1 then return
129 if pos[1].has(",") then
130 var pos2 = pos[1].split_with(",")
131 if pos2.length > 1 then
132 self.line_end = pos2[0].to_i
133 self.column_end = pos2[1].to_i
134 else
135 self.line_end = self.line_start
136 self.column_end = pos2[0].to_i
137 end
138 else
139 self.line_end = pos[1].to_i
140 end
141 end
142
143 # Initialize a location corresponding to an opaque file.
144 #
145 # The path is used as is and is not open nor read.
146 init opaque_file(path: String)
147 do
148 var source = new SourceFile.from_string(path, "")
149 init(source, 0, 0, 0, 0)
150 end
151
152 # The index in the start character in the source
153 fun pstart: Int do return file.line_starts[line_start-1] + column_start-1
154
155 # The index on the end character in the source
156 fun pend: Int do return file.line_starts[line_end-1] + column_end-1
157
158 # The verbatim associated text in the source-file
159 fun text: String
160 do
161 var res = self.text_cache
162 if res != null then return res
163 var l = self
164 var pstart = self.pstart
165 var pend = self.pend
166 res = l.file.string.substring(pstart, pend-pstart+1)
167 self.text_cache = res
168 return res
169 end
170
171 private var text_cache: nullable String = null
172
173 redef fun ==(other: nullable Object): Bool do
174 if other == null then return false
175 if not other isa Location then return false
176
177 if other.file != file then return false
178 if other.line_start != line_start then return false
179 if other.line_end != line_end then return false
180 if other.column_start != column_start then return false
181 if other.column_end != column_end then return false
182
183 return true
184 end
185
186 # Is `self` included (or equals) to `loc`?
187 fun located_in(loc: nullable Location): Bool do
188 if loc == null then return false
189
190 if line_start < loc.line_start then return false
191 if line_start > loc.line_end then return false
192
193 if line_end > loc.line_end then return false
194
195 if line_start == loc.line_start then
196 if column_start < loc.column_start then return false
197 if column_start > loc.column_end then return false
198 end
199
200 if line_end == loc.line_end and column_end > loc.column_end then return false
201
202 return true
203 end
204
205 redef fun to_s: String do
206 var file_part = ""
207 if file != null then
208 file_part = file.filename
209 end
210
211 if line_start <= 0 then return file_part
212
213 if file != null and file.filename.length > 0 then file_part += ":"
214
215 if line_start == line_end then
216 if column_start == column_end then
217 return "{file_part}{line_start},{column_start}"
218 else
219 return "{file_part}{line_start},{column_start}--{column_end}"
220 end
221 else
222 return "{file_part}{line_start},{column_start}--{line_end},{column_end}"
223 end
224 end
225
226 # Return a location message according to an observer.
227 #
228 # Currently, if both are in the same file, the file information is not present in the result.
229 fun relative_to(loc: nullable Location): String do
230 var relative: Location
231 if loc != null and loc.file == self.file then
232 relative = new Location(null, self.line_start, self.line_end, self.column_start, self.column_end)
233 else
234 relative = new Location(self.file, self.line_start, self.line_end, self.column_start, self.column_end)
235 end
236 return relative.to_s
237 end
238
239 redef fun <(other: OTHER): Bool do
240 if self == other then return false
241 if self.located_in(other) then return true
242 if other.located_in(self) then return false
243
244 if line_start != other.line_start then return line_start < other.line_start
245 if column_start != other.column_start then return column_start < other.column_start
246 if line_end != other.line_end then return line_end < other.line_end
247
248 return column_end < other.column_end
249 end
250
251 # Return the associated line with the location highlighted with color and a caret under the starting position
252 # `color` must be and terminal escape sequence used as `"{escape}[{color}m;"`
253 # * `"0;31"` for red
254 # * `"1;31"` for bright red
255 # * `"0;32"` for green
256 fun colored_line(color: String): String
257 do
258 var esc = 27.code_point
259 var def = "{esc}[0m"
260 var col = "{esc}[{color}m"
261
262 var l = self
263 var i = l.line_start
264 if i <= 0 then return ""
265
266 var line_start = l.file.line_starts[i-1]
267 var line_end = line_start
268 var string = l.file.string
269 while line_end+1 < string.length and string.chars[line_end+1] != '\n' and string.chars[line_end+1] != '\r' do
270 line_end += 1
271 end
272 var lstart
273 if l.column_start > 0 then
274 lstart = string.substring(line_start, l.column_start - 1)
275 else
276 lstart = ""
277 end
278 var cend
279 if i != l.line_end then
280 cend = line_end - line_start + 1
281 else
282 cend = l.column_end
283 end
284 var lmid
285 var lend
286 if line_start + cend <= string.length then
287 lmid = string.substring(line_start + l.column_start - 1, cend - l.column_start + 1)
288 lend = string.substring(line_start + cend, line_end - line_start - cend + 1)
289 else
290 lmid = ""
291 lend = ""
292 end
293 var indent = new FlatBuffer
294 for j in [line_start..line_start+l.column_start-1[ do
295 if string.chars[j] == '\t' then
296 indent.add '\t'
297 else
298 indent.add ' '
299 end
300 end
301 return "\t{lstart}{col}{lmid}{def}{lend}\n\t{indent}^"
302 end
303 end