Skip to content

Commit 4a79958

Browse files
committed
vlist: vertical list projector on splice sub-editors
1 parent a25f6ff commit 4a79958

13 files changed

Lines changed: 1443 additions & 39 deletions

File tree

src/haz3lcore/pretty/ExpToSegment.re

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,13 +1160,40 @@ let concat_segment =
11601160
let (@) = (seg1: Segment.t, seg2: Segment.t): Segment.t =>
11611161
concat_segment(~secondary=AutoFormat, seg1, seg2);
11621162

1163+
/* Projector-construction hooks, injected by ProjectorInit at module
1164+
* initialization. ExpToSegment needs to construct projectors (folds,
1165+
* tables) while printing, but ProjectorInit needs ExpToSegment to print
1166+
* term-level init overrides, so ExpToSegment cannot depend on
1167+
* ProjectorInit directly. If the hooks are unregistered, projector
1168+
* construction degrades to a no-op (the plain syntax is used). */
1169+
module ProjectorHooks = {
1170+
type t = {
1171+
init_or_noop:
1172+
(ProjectorCore.Kind.t, Base.segment, Any.t) => Base.segment,
1173+
init_or_noop_from_str:
1174+
(ProjectorCore.Kind.t, Base.segment, Any.t, string) => Base.segment,
1175+
};
1176+
let registered: ref(option(t)) = ref(None);
1177+
let register = (hooks: t): unit => registered := Some(hooks);
1178+
let init_or_noop = (kind, seg, any) =>
1179+
switch (registered^) {
1180+
| Some(hooks) => hooks.init_or_noop(kind, seg, any)
1181+
| None => seg
1182+
};
1183+
let init_or_noop_from_str = (kind, seg, any, str) =>
1184+
switch (registered^) {
1185+
| Some(hooks) => hooks.init_or_noop_from_str(kind, seg, any, str)
1186+
| None => seg
1187+
};
1188+
};
1189+
11631190
let fold_if = (condition, pieces) =>
11641191
if (condition) {
11651192
let wrapped =
11661193
mk_form(~secondary=AutoFormat, ParensExp, Id.mk(), [pieces]);
11671194
switch (MakeTerm.for_projection([wrapped])) {
11681195
| None => failwith("ExpToSegment.fold_if")
1169-
| Some(any) => ProjectorInit.init_or_noop(Fold, pieces, any)
1196+
| Some(any) => ProjectorHooks.init_or_noop(Fold, pieces, any)
11701197
};
11711198
} else {
11721199
pieces;
@@ -1182,7 +1209,7 @@ let fold_fun_if = (condition, f_name: string, pieces, exp) =>
11821209
always_render: true,
11831210
})
11841211
|> Sexplib.Sexp.to_string;
1185-
ProjectorInit.init_or_noop_from_str(Fold, pieces, Exp(exp), str);
1212+
ProjectorHooks.init_or_noop_from_str(Fold, pieces, Exp(exp), str);
11861213
| `Text =>
11871214
let name =
11881215
if (String.length(f_name) >= 2) {
@@ -1205,7 +1232,7 @@ let project_table_if = (should_project, pieces) =>
12051232
if (should_project) {
12061233
switch (MakeTerm.for_projection([pieces])) {
12071234
| None => [pieces]
1208-
| Some(any) => ProjectorInit.init_or_noop(Table, [pieces], any)
1235+
| Some(any) => ProjectorHooks.init_or_noop(Table, [pieces], any)
12091236
};
12101237
} else {
12111238
[pieces];

src/haz3lcore/projectors/ProjectorBase.re

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ type info = {
108108
/* A projector-reported error, e.g. "can't render as table" */
109109
type error = {message: string};
110110

111+
/* A replacement for the projector's underlying syntax, returned by
112+
* [init]. Projectors that transform their syntax at projection time
113+
* should prefer [Term]: provide the desired term (e.g. list items
114+
* wrapped in splices) and the framework prints it to a segment,
115+
* exactly as SetTerm does for later edits. [Syntax] is the escape
116+
* hatch for projectors that need piece-level control. */
117+
[@deriving (show({with_path: false}), sexp, yojson)]
118+
type init_override =
119+
| Term(Any.t)
120+
| Syntax(Base.segment);
121+
111122
module View = {
112123
/* A projector has an inline view, which replaces the underlying
113124
* syntax. Optionally, it may have an overlay view, which is shown
@@ -215,15 +226,16 @@ module type Projector = {
215226
/* Init should return None if the projector doesn't want
216227
* to handle the provided term. Otherwise, it should
217228
* return the desired initial state of the model, along with
218-
* an optional replacement syntax. [init] receives the original
219-
* [Base.segment] the user selected (in addition to the parsed
220-
* term); when [Some(seg)] is returned as the second component,
221-
* the projector's stored syntax is set to [seg] instead of the
222-
* selected segment. This lets projectors transform their
223-
* underlying syntax at init time (e.g. to wrap list items in
224-
* splices). Return [None] for the second component to keep the
225-
* selected syntax unchanged. */
226-
let init: (Any.t, Base.segment) => option((model, option(Base.segment)));
229+
* an optional replacement for the underlying syntax. [init]
230+
* receives the original [Base.segment] the user selected (in
231+
* addition to the parsed term). When the second component is
232+
* [Some(Term(t))], the framework prints [t] and stores the result
233+
* as the projector's syntax (the same path SetTerm uses); this is
234+
* the preferred way to transform the underlying syntax at init
235+
* time (e.g. to wrap list items in splices). [Some(Syntax(seg))]
236+
* installs [seg] directly for projectors that need piece-level
237+
* control. Return [None] to keep the selected syntax unchanged. */
238+
let init: (Any.t, Base.segment) => option((model, option(init_override)));
227239
/* Does this projector have some notion of internal
228240
* positions, whose handling should override the editor
229241
* caret & keyboard handlers? If so, provide handlers

src/haz3lcore/projectors/ProjectorInit.re

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,44 @@ let to_module = (kind: ProjectorCore.Kind.t): (module Cooked) =>
1717
| Card => (module Cook(CardProj.M))
1818
| Table => (module Cook(TableProj.M))
1919
| Csv => (module Cook(CSVProjector.M))
20+
| VList => (module Cook(VListProj.M))
21+
};
22+
23+
/* Printer for Term init overrides, injected by ProjectorPerform at
24+
* module initialization. Resolving a Term override requires
25+
* ExpToSegment, but this module cannot depend on ExpToSegment: it is
26+
* reachable from ExpToSegment via MakeTerm -> ... -> Refractors ->
27+
* ProjectorInit. ProjectorPerform sits above both and registers the
28+
* real printer (the same conversion SetTerm uses, reusing splices
29+
* from the original syntax by id). If unregistered, Term overrides
30+
* degrade to keeping the selected syntax. */
31+
let term_printer:
32+
ref((~original_syntax: Base.segment, Language.Any.t) => Base.segment) =
33+
ref((~original_syntax, _term) => original_syntax);
34+
35+
/* Resolve an init-returned syntax override against the selected
36+
* syntax: Term overrides are printed to a segment, Syntax overrides
37+
* are installed directly, and None keeps the selection. */
38+
let resolve_override =
39+
(syntax: syntax, override: option(init_override)): syntax =>
40+
switch (override) {
41+
| None => syntax
42+
| Some(Syntax(seg)) => seg
43+
| Some(Term(term)) => term_printer^(~original_syntax=syntax, term)
2044
};
2145

2246
/* Construct a Projector piece wrapping the given syntax segment.
23-
* The projector's [init] may optionally return a replacement segment
24-
* (e.g. to wrap list items in splices); if so, the stored syntax is
25-
* set to that replacement instead of [syntax]. */
47+
* The projector's [init] may optionally return a replacement for the
48+
* underlying syntax (e.g. to wrap list items in splices); see
49+
* ProjectorBase.init_override. */
2650
let init =
2751
(kind: ProjectorCore.Kind.t, syntax: syntax, any: Language.Any.t)
2852
: option(Base.piece) => {
2953
let (module P) = to_module(kind);
3054
switch (P.init(any, syntax)) {
3155
| None => None
3256
| Some((model, override)) =>
33-
let syntax = Option.value(override, ~default=syntax);
57+
let syntax = resolve_override(syntax, override);
3458
Some(Projector(ProjectorCore.mk(kind, syntax, model)));
3559
};
3660
};
@@ -57,7 +81,8 @@ let init_or_noop_from_str =
5781
switch (P.init(any, syntax)) {
5882
| None => syntax
5983
| Some((_, override)) =>
60-
let syntax = Option.value(override, ~default=syntax);
84+
let syntax = resolve_override(syntax, override);
6185
[Projector(ProjectorCore.mk(kind, syntax, model_str))];
6286
};
6387
};
88+

src/haz3lcore/projectors/ProjectorPerform.re

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ let init =
4949
let* elab_exp =
5050
Language.Exp.find_by_id(Language.Exp.rep_id(exp), elaborated);
5151
let+ (model_str, override) = P.init(Exp(elab_exp), seg);
52-
let syntax = Option.value(override, ~default=seg);
52+
let syntax = ProjectorInit.resolve_override(seg, override);
5353
Base.Projector(ProjectorCore.mk(kind, syntax, model_str));
5454
| (None, _) => None
5555
};
@@ -115,6 +115,21 @@ let term_to_segment =
115115
term,
116116
);
117117

118+
/* Wire up the cyclic-dependency injection points (see the comments at
119+
* the registries for why they exist). This module sits above both
120+
* ExpToSegment and ProjectorInit, so it can hand each the pieces of
121+
* the other it needs. Runs as a module-initialization side effect at
122+
* program startup. */
123+
ExpToSegment.ProjectorHooks.register({
124+
init_or_noop: ProjectorInit.init_or_noop,
125+
init_or_noop_from_str: ProjectorInit.init_or_noop_from_str,
126+
});
127+
ProjectorInit.term_printer :=
128+
(
129+
(~original_syntax, term) =>
130+
term_to_segment(~original_syntax, ~preserve_splices=true, term)
131+
);
132+
118133
let update_piece =
119134
(f: Base.projector => Base.projector, id: Id.t, piece: Base.piece)
120135
: Base.segment =>

src/haz3lcore/projectors/implementations/FoldProj.re

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ module M: Projector = {
3030
type action =
3131
| Toggle;
3232

33-
let init = (_, _) => Some((default, None: option(Base.segment)));
33+
let init = (_, _) =>
34+
Some((default, None: option(ProjectorBase.init_override)));
3435

3536
let focusable = Focusable.non;
3637
let dynamics = false;

src/haz3lcore/projectors/implementations/ProbeProj.re

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1482,7 +1482,7 @@ module M: Projector = {
14821482
type action = a;
14831483

14841484
let init = (any: Any.t, _) => {
1485-
let none_seg: option(Base.segment) = None;
1485+
let none_seg: option(ProjectorBase.init_override) = None;
14861486
switch (any) {
14871487
| Exp(_)
14881488
| Pat(_) => Some(({active_renderer: None}, none_seg))

src/haz3lcore/projectors/implementations/TypeProj.re

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ module M: Projector = {
3535
| ToggleDisplay;
3636

3737
let init = (any: Any.t, _) => {
38-
let none_seg: option(Base.segment) = None;
38+
let none_seg: option(ProjectorBase.init_override) = None;
3939
switch (any) {
4040
| Exp(_)
4141
| Pat(_) => Some((Expected, none_seg))

0 commit comments

Comments
 (0)