Merge: doc: fixed some typos and other misc. corrections
[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
51 # Extract a given line excluding the line-terminators characters.
52 #
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
60 line_end += 1
61 end
62 return string.substring(line_start, line_end-line_start+1)
63 end
64 end
65
66 # A location inside a source file
67 class Location
68 super Comparable
69 redef type OTHER: Location
70
71 # The associated source-file
72 var file: nullable SourceFile
73
74 # The starting line number (starting from 1)
75 #
76 # If `line_start==0` then the whole file is considered
77 var line_start: Int
78
79 # The stopping line number (starting from 1)
80 var line_end: Int
81
82 # Start of this location on `line_start`
83 #
84 # A `column_start` of 1 means the first column or character.
85 #
86 # If `column_start == 0` this location concerns the whole line.
87 #
88 # Require: `column_start >= 0`
89 var column_start: Int
90
91 # End of this location on `line_end`
92 var column_end: Int
93
94 # Builds a location instance from its string representation.
95 #
96 # Examples:
97 #
98 # ~~~
99 # var loc = new Location.from_string("location.nit:82,2--105,8")
100 # assert loc.to_s == "location.nit:82,2--105,8"
101 #
102 # loc = new Location.from_string("location.nit")
103 # assert loc.to_s == "location.nit"
104 #
105 # loc = new Location.from_string("location.nit:82,2")
106 # assert loc.to_s == "location.nit:82,2--0,0"
107 #
108 # loc = new Location.from_string("location.nit:82--105")
109 # assert loc.to_s == "location.nit:82,0--105,0"
110 #
111 # loc = new Location.from_string("location.nit:82,2--105")
112 # assert loc.to_s == "location.nit:82,2--105,0"
113 #
114 # loc = new Location.from_string("location.nit:82--105,8")
115 # assert loc.to_s == "location.nit:82,0--105,8"
116 # ~~~
117 init from_string(string: String) is
118 nosuper
119 do
120 self.line_start = 0
121 self.line_end = 0
122 self.column_start = 0
123 self.column_end = 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))
128 # split position
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
137 end
138 else
139 self.line_start = pos.first.to_i
140 end
141 # split end position
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
148 else
149 self.line_end = self.line_start
150 self.column_end = pos2[0].to_i
151 end
152 else
153 self.line_end = pos[1].to_i
154 end
155 end
156
157 # Initialize a location corresponding to an opaque file.
158 #
159 # The path is used as is and is not open nor read.
160 init opaque_file(path: String)
161 do
162 var source = new SourceFile.from_string(path, "")
163 init(source, 0, 0, 0, 0)
164 end
165
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
168
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
171
172 # The verbatim associated text in the source-file
173 fun text: String
174 do
175 var res = self.text_cache
176 if res != null then return res
177 var l = self
178 var pstart = self.pstart
179 var pend = self.pend
180 res = l.file.string.substring(pstart, pend-pstart+1)
181 self.text_cache = res
182 return res
183 end
184
185 private var text_cache: nullable String = null
186
187 redef fun ==(other: nullable Object): Bool do
188 if other == null then return false
189 if not other isa Location then return false
190
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
196
197 return true
198 end
199
200 # Is `self` included (or equals) to `loc`?
201 fun located_in(loc: nullable Location): Bool do
202 if loc == null then return false
203
204 if line_start < loc.line_start then return false
205 if line_start > loc.line_end then return false
206
207 if line_end > loc.line_end then return false
208
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
212 end
213
214 if line_end == loc.line_end and column_end > loc.column_end then return false
215
216 return true
217 end
218
219 redef fun to_s: String do
220 var file_part = ""
221 if file != null then
222 file_part = file.filename
223 end
224
225 if line_start <= 0 then return file_part
226
227 if file != null and file.filename.length > 0 then file_part += ":"
228
229 if line_start == line_end then
230 if column_start == column_end then
231 return "{file_part}{line_start},{column_start}"
232 else
233 return "{file_part}{line_start},{column_start}--{column_end}"
234 end
235 else
236 return "{file_part}{line_start},{column_start}--{line_end},{column_end}"
237 end
238 end
239
240 # Return a location message according to an observer.
241 #
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)
247 else
248 relative = new Location(self.file, self.line_start, self.line_end, self.column_start, self.column_end)
249 end
250 return relative.to_s
251 end
252
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
257
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
261
262 return column_end < other.column_end
263 end
264
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;"`
267 # * `"0;31"` for red
268 # * `"1;31"` for bright red
269 # * `"0;32"` for green
270 fun colored_line(color: String): String
271 do
272 var esc = 27.code_point
273 var def = "{esc}[0m"
274 var col = "{esc}[{color}m"
275
276 var l = self
277 var i = l.line_start
278 if i <= 0 then return ""
279
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
284 line_end += 1
285 end
286 var lstart
287 if l.column_start > 0 then
288 lstart = string.substring(line_start, l.column_start - 1)
289 else
290 lstart = ""
291 end
292 var cend
293 if i != l.line_end then
294 cend = line_end - line_start + 1
295 else
296 cend = l.column_end
297 end
298 var lmid
299 var lend
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)
303 else
304 lmid = ""
305 lend = ""
306 end
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
310 indent.add '\t'
311 else
312 indent.add ' '
313 end
314 end
315 return "\t{lstart}{col}{lmid}{def}{lend}\n\t{indent}^"
316 end
317 end