-
Notifications
You must be signed in to change notification settings - Fork 455
/
code_frame.ml
279 lines (262 loc) · 9.31 KB
/
code_frame.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
let digits_count n =
let rec loop n base count =
if n >= base then loop n (base * 10) (count + 1) else count
in
loop (abs n) 1 0
let seek_2_lines_before src (pos : Lexing.position) =
let original_line = pos.pos_lnum in
let rec loop current_line current_char =
if current_line + 2 >= original_line then (current_char, current_line)
else
loop
(if src.[current_char] = '\n' then current_line + 1 else current_line)
(current_char + 1)
in
loop 1 0
let seek_2_lines_after src (pos : Lexing.position) =
let original_line = pos.pos_lnum in
let rec loop current_line current_char =
if current_char = String.length src then (current_char, current_line)
else
match src.[current_char] with
| '\n' when current_line = original_line + 2 ->
(current_char, current_line)
| '\n' -> loop (current_line + 1) (current_char + 1)
| _ -> loop current_line (current_char + 1)
in
loop original_line pos.pos_cnum
let leading_space_count str =
let rec loop i count =
if i = String.length str then count
else if str.[i] != ' ' then count
else loop (i + 1) (count + 1)
in
loop 0 0
let break_long_line max_width line =
let rec loop pos accum =
if pos = String.length line then accum
else
let chunk_length = min max_width (String.length line - pos) in
let chunk = String.sub line pos chunk_length in
loop (pos + chunk_length) (chunk :: accum)
in
loop 0 [] |> List.rev
let filter_mapi f l =
let rec loop f l i accum =
match l with
| [] -> accum
| head :: rest ->
let accum =
match f i head with
| None -> accum
| Some result -> result :: accum
in
loop f rest (i + 1) accum
in
loop f l 0 [] |> List.rev
(* Spiritual equivalent of
https://github.com/ocaml/ocaml/blob/414bdec9ae387129b8102cc6bf3c0b6ae173eeb9/utils/misc.ml#L601
*)
module Color = struct
type color =
| Dim
(* | Filename *)
| Err
| Warn
| NoColor
let dim = "\x1b[2m"
(* let filename = "\x1b[46m" *)
let err = "\x1b[1;31m"
let warn = "\x1b[1;33m"
let reset = "\x1b[0m"
external isatty : out_channel -> bool = "caml_sys_isatty"
(* reasonable heuristic on whether colors should be enabled *)
let should_enable_color () =
let term = try Sys.getenv "TERM" with Not_found -> "" in
term <> "dumb" && term <> "" && isatty stderr
let color_enabled = ref true
let setup =
let first = ref true in
(* initialize only once *)
fun o ->
if !first then (
first := false;
color_enabled :=
match o with
| Some Misc.Color.Always -> true
| Some Auto -> should_enable_color ()
| Some Never -> false
| None -> should_enable_color ());
()
end
let setup = Color.setup
type gutter = Number of int | Elided
type highlighted_string = {s: string; start: int; end_: int}
type line = {gutter: gutter; content: highlighted_string list}
(*
Features:
- display a line gutter
- break long line into multiple for terminal display
- peek 2 lines before & after for context
- center snippet when it's heavily indented
- ellide intermediate lines when the reported range is huge
*)
let print ~is_warning ~src ~(start_pos : Lexing.position)
~(end_pos : Lexing.position) =
let indent = 2 in
let highlight_line_start_line = start_pos.pos_lnum in
let highlight_line_end_line = end_pos.pos_lnum in
let start_line_line_offset, first_shown_line =
seek_2_lines_before src start_pos
in
let end_line_line_end_offset, last_shown_line =
seek_2_lines_after src end_pos
in
let more_than_5_highlighted_lines =
highlight_line_end_line - highlight_line_start_line + 1 > 5
in
let max_line_digits_count = digits_count last_shown_line in
(* TODO: change this back to a fixed 100? *)
(* 3 for separator + the 2 spaces around it *)
let line_width = 78 - max_line_digits_count - indent - 3 in
let lines =
String.sub src start_line_line_offset
(end_line_line_end_offset - start_line_line_offset)
|> String.split_on_char '\n'
|> filter_mapi (fun i line ->
let line_number = i + first_shown_line in
if more_than_5_highlighted_lines then
if line_number = highlight_line_start_line + 2 then
Some (Elided, line)
else if
line_number > highlight_line_start_line + 2
&& line_number < highlight_line_end_line - 1
then None
else Some (Number line_number, line)
else Some (Number line_number, line))
in
let leading_space_to_cut =
lines
|> List.fold_left
(fun current_max (_, line) ->
let leading_spaces = leading_space_count line in
if String.length line = leading_spaces then
(* the line's nothing but spaces. Doesn't count *)
current_max
else min leading_spaces current_max)
99999
in
let separator = if leading_space_to_cut = 0 then "│" else "┆" in
let stripped_lines =
lines
|> List.map (fun (gutter, line) ->
let new_content =
if String.length line <= leading_space_to_cut then
[{s = ""; start = 0; end_ = 0}]
else
String.sub line leading_space_to_cut
(String.length line - leading_space_to_cut)
|> break_long_line line_width
|> List.mapi (fun i line ->
match gutter with
| Elided -> {s = line; start = 0; end_ = 0}
| Number line_number ->
let highlight_line_start_offset =
start_pos.pos_cnum - start_pos.pos_bol
in
let highlight_line_end_offset =
end_pos.pos_cnum - end_pos.pos_bol
in
let start =
if i = 0 && line_number = highlight_line_start_line
then
highlight_line_start_offset - leading_space_to_cut
else 0
in
let end_ =
if line_number < highlight_line_start_line then 0
else if
line_number = highlight_line_start_line
&& line_number = highlight_line_end_line
then highlight_line_end_offset - leading_space_to_cut
else if line_number = highlight_line_start_line then
String.length line
else if
line_number > highlight_line_start_line
&& line_number < highlight_line_end_line
then String.length line
else if line_number = highlight_line_end_line then
highlight_line_end_offset - leading_space_to_cut
else 0
in
{s = line; start; end_})
in
{gutter; content = new_content})
in
let buf = Buffer.create 100 in
let open Color in
let add_ch =
let last_color = ref NoColor in
fun color ch ->
if (not !Color.color_enabled) || !last_color = color then
Buffer.add_char buf ch
else
let ansi =
match (!last_color, color) with
| NoColor, Dim -> dim
(* | NoColor, Filename -> filename *)
| NoColor, Err -> err
| NoColor, Warn -> warn
| _, NoColor -> reset
| _, Dim -> reset ^ dim
(* | _, Filename -> reset ^ filename *)
| _, Err -> reset ^ err
| _, Warn -> reset ^ warn
in
Buffer.add_string buf ansi;
Buffer.add_char buf ch;
last_color := color
in
let draw_gutter color s =
for _i = 1 to max_line_digits_count + indent - String.length s do
add_ch NoColor ' '
done;
s |> String.iter (add_ch color);
add_ch NoColor ' ';
separator |> String.iter (add_ch Dim);
add_ch NoColor ' '
in
stripped_lines
|> List.iter (fun {gutter; content} ->
match gutter with
| Elided ->
draw_gutter Dim ".";
add_ch Dim '.';
add_ch Dim '.';
add_ch Dim '.';
add_ch NoColor '\n'
| Number line_number ->
content
|> List.iteri (fun i line ->
let gutter_content =
if i = 0 then string_of_int line_number else ""
in
let gutter_color =
if
i = 0
&& line_number >= highlight_line_start_line
&& line_number <= highlight_line_end_line
then if is_warning then Warn else Err
else NoColor
in
draw_gutter gutter_color gutter_content;
line.s
|> String.iteri (fun ii ch ->
let c =
if ii >= line.start && ii < line.end_ then
if is_warning then Warn else Err
else NoColor
in
add_ch c ch);
add_ch NoColor '\n'));
Buffer.contents buf