Skip to content

Commit 952fc53

Browse files
authored
Line numbers (#1986)
2 parents 1eb6f84 + 1d3223a commit 952fc53

17 files changed

Lines changed: 249 additions & 16 deletions

File tree

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/haz3lcore/derived/Measured.re

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,12 @@ let of_segment_inner =
335335
~col=new_indent - origin.col,
336336
);
337337
// add seg to map and reset seg
338-
//TODO: decide if should actually add linebreak here
339-
let map = add_piece_row(origin.row, seg, map);
338+
let map =
339+
add_piece_row(
340+
origin.row,
341+
seg @ [Piece.Secondary(Secondary.mk_newline(Id.mk()))], /* NOTE: These linebreaks don't actually occur in the surface syntax */
342+
map,
343+
);
340344
let map =
341345
size.row == 0 ? map : add_n_empty_piece_rows(size.row - 1, map);
342346
([], new_indent, size, map);
@@ -354,7 +358,11 @@ let of_segment_inner =
354358
? {
355359
let g = DeferredLinebreaks.of_secondary();
356360
add_n_rows(origin, indent, g, map)
357-
|> add_piece_row(origin.row, seg, _)
361+
|> add_piece_row(
362+
origin.row,
363+
seg @ [Piece.Secondary(Secondary.mk_newline(Id.mk()))], /* NOTE: These linebreaks don't actually occur in the surface syntax */
364+
_,
365+
)
358366
|> add_n_empty_piece_rows(g - 1);
359367
}
360368
: map;

src/haz3lcore/lang/Token.re

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ let of_list = Unicode.of_list;
4141
/* Token Recognition Predicates */
4242

4343
/* A. Secondary Notation (Comments, Whitespace, etc.) */
44+
let empty = ""; /* This is invalid for view */
4445
let space = " ";
4546
let linebreak = "\n";
4647
let comment_regexp = regexp("^#[^#\n]*#$"); /* Multiline comments not supported */

src/haz3lcore/tiles/Secondary.re

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ open Language.Secondary;
44
[@deriving (show({with_path: false}), sexp, yojson, eq)]
55
type t = Language.Secondary.t;
66

7+
let mk_empty = id => {
8+
content: Whitespace(Token.empty),
9+
id,
10+
};
11+
712
let mk_space = id => {
813
content: Whitespace(Token.space),
914
id,

src/web/Settings.re

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ module Model = {
1414
explainThis: ExplainThisModel.Settings.t,
1515
assistant: AssistantSettings.t,
1616
sidebar: SidebarModel.Settings.t,
17+
line_numbers: bool,
18+
relative_line_numbers: bool,
1719
};
1820

1921
let init = {
@@ -61,6 +63,8 @@ module Model = {
6163
panel: LanguageDocumentation,
6264
show: true,
6365
},
66+
line_numbers: false,
67+
relative_line_numbers: false,
6468
};
6569

6670
let fix_instructor_mode = settings =>
@@ -122,7 +126,9 @@ module Update = {
122126
| Sidebar(SidebarModel.Settings.action)
123127
| ExplainThis(ExplainThisModel.Settings.action)
124128
| Assistant(AssistantSettings.action)
125-
| FlipAnimations;
129+
| FlipAnimations
130+
| ToggleLineNumbers
131+
| ToggleRelativeLineNumbers;
126132

127133
let can_undo = (action: t) => {
128134
switch (action) {
@@ -346,6 +352,15 @@ module Update = {
346352
...settings, //TODO[Matt]: Make sure instructor mode actually makes prelude read-only
347353
instructor_mode: !settings.instructor_mode,
348354
}
355+
356+
| ToggleLineNumbers => {
357+
...settings,
358+
line_numbers: !settings.line_numbers,
359+
}
360+
| ToggleRelativeLineNumbers => {
361+
...settings,
362+
relative_line_numbers: !settings.relative_line_numbers,
363+
}
349364
}
350365
)
351366
|> Updated.return(~scroll_active=false);

src/web/app/editors/Editors.re

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ module View = {
367367
editors: Model.t,
368368
) =>
369369
switch (editors) {
370+
// Add in the line numbering for Scratch editor
370371
| Scratch(m) =>
371372
ScratchMode.View.view(
372373
~signal=
@@ -381,6 +382,7 @@ module View = {
381382
~inject=a => Update.Scratch(a) |> inject,
382383
m,
383384
)
385+
// Add in the line numbering for Documentation editor
384386
| Documentation(m) =>
385387
ScratchMode.View.view(
386388
~signal=

src/web/app/editors/cell/CellEditor.re

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ module View = {
199199
~caption: option(Node.t)=?,
200200
~result_kind=?,
201201
~locked=false,
202+
~lines=false,
202203
model: Model.t,
203204
) => {
204205
let (footer, overlays) =
@@ -249,6 +250,7 @@ module View = {
249250
: (action => inject(MainEditor(action))),
250251
~selected=selected == Some(MainEditor),
251252
~overlays=overlays(model.editor.editor),
253+
~lines,
252254
~dynamics=EvalResult.Model.dynamics(model.result),
253255
model.editor,
254256
),

src/web/app/editors/code/CodeEditable.re

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ module View = {
270270
~inject: Update.t => Ui_effect.t(unit),
271271
~selected: bool,
272272
~overlays: list(Node.t)=[],
273+
~lines: bool=false,
273274
~dynamics: Language.Dynamics.Map.t,
274275
~expand_selection=?,
275276
model: Model.t,
@@ -438,10 +439,14 @@ module View = {
438439
};
439440
};
440441

442+
let display_line_numbers: bool = lines && globals.settings.line_numbers;
443+
441444
Node.div(
442445
~attrs=[
443446
Attr.classes(
444-
["cell-item", "code-editor"] @ (selected ? ["selected"] : []),
447+
["cell-item", "code-editor"]
448+
@ (selected ? ["selected"] : [])
449+
@ (display_line_numbers ? ["has-line-numbers"] : []),
445450
),
446451
Attr.on_contextmenu(evt =>
447452
switch (Pointer.Event.mk(evt)) {
@@ -459,7 +464,14 @@ module View = {
459464
Attr.on_mousemove(evt => drag_select(Pointer.Event.mk(evt))),
460465
Attr.on_wheel(evt => drag_select(Pointer.Event.mk(evt))),
461466
],
462-
[code_view],
467+
display_line_numbers
468+
? LineNumbers.View.view(
469+
model,
470+
globals.settings.relative_line_numbers,
471+
selected,
472+
)
473+
@ [code_view]
474+
: [code_view],
463475
);
464476
};
465477
};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
open Haz3lcore;
2+
//open Virtual_dom.Vdom;
3+
open Util;
4+
open WebUtil;
5+
6+
/*
7+
Used to display line numbering alongside cells
8+
*/
9+
module Model = CodeWithStatics.Model;
10+
11+
module View = {
12+
let view = (model: Model.t, show_relative_numbers: bool, selected: bool) => {
13+
let {editor: {syntax: {measured, _}, state: {zipper, _}, _}, _}: Model.t = model;
14+
let num_rows = List.length(measured.piece_rows);
15+
let empty_row = row => {
16+
let result = List.nth_opt(List.rev(measured.piece_rows), row);
17+
switch (result) {
18+
| Some(value) =>
19+
switch (value) {
20+
| [] => true
21+
| _ => false
22+
}
23+
| None => true // The row doesn't actually exist, hence it's empty
24+
};
25+
};
26+
let Point.{row, _} = Zipper.Caret.point(measured, zipper);
27+
let cursor_row_index = row;
28+
/*
29+
Recursively builds a list of line numbers for display, skipping empty rows.
30+
31+
Parameters:
32+
- row_index: Current row being processed (0-indexed)
33+
- line_count: The line number to assign to the current non-empty row
34+
35+
Returns: (line_numbers, cursor_line_number) where:
36+
- line_numbers: List where 0 indicates skip this row, non-zero is the display line number
37+
- cursor_line_number: The line number where the cursor is located (0 if not found yet)
38+
*/
39+
let rec processed_line_numbers =
40+
(row_index: int, line_count: int): (list(int), int) =>
41+
if (row_index == num_rows) {
42+
([], 0);
43+
} else {
44+
let is_row_empty = empty_row(row_index);
45+
let (returned_processed_list, returned_cursor_line_number) =
46+
is_row_empty
47+
? processed_line_numbers(row_index + 1, line_count)
48+
: processed_line_numbers(row_index + 1, line_count + 1);
49+
let current_line_number = is_row_empty ? 0 : line_count;
50+
let cursor_line_number =
51+
if (returned_cursor_line_number == 0 && row_index == cursor_row_index) {
52+
line_count;
53+
} else {
54+
returned_cursor_line_number;
55+
};
56+
(
57+
[current_line_number] @ returned_processed_list,
58+
cursor_line_number,
59+
);
60+
};
61+
let (processed_list, cursor_line_number) = processed_line_numbers(0, 1);
62+
/*
63+
Converts a row index to its display text.
64+
Returns "\n" for empty rows, or the line number (absolute or relative) with newline.
65+
*/
66+
let index_to_text = (i): string => {
67+
let line_number = List.nth(processed_list, i);
68+
line_number == 0
69+
? "\n" // if this is a line we want to skip
70+
: {
71+
(
72+
if (show_relative_numbers && selected) {
73+
string_of_int(
74+
abs(line_number - cursor_line_number) == 0
75+
? line_number : abs(line_number - cursor_line_number),
76+
);
77+
} else {
78+
string_of_int(line_number);
79+
}
80+
)
81+
++ (i == num_rows ? "" : "\n"); // Add a line break if this is not the last row
82+
};
83+
};
84+
let index_to_span = (i): Node.t => {
85+
Node.span(
86+
~attrs=
87+
i == row && selected ? [Attr.classes(["line-numbers-bold"])] : [],
88+
[Text(index_to_text(i))],
89+
);
90+
};
91+
[
92+
Node.div(
93+
~attrs=[
94+
Attr.classes([
95+
"code",
96+
"line-numbers",
97+
selected ? "line-numbers-selected" : "",
98+
]),
99+
],
100+
[
101+
Node.span(
102+
~attrs=[Attr.classes(["code-text", "line-numbers-text"])],
103+
List.init(num_rows, (i): Node.t => {index_to_span(i)}),
104+
),
105+
],
106+
),
107+
];
108+
};
109+
};

src/web/view/ExerciseMode.re

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,7 @@ module View = {
651651
)
652652
| _ => CellCommon.caption(caption, ~rest=?subcaption)
653653
},
654+
~lines=true,
654655
cell,
655656
);
656657
};

0 commit comments

Comments
 (0)