diff --git a/src/CLI/Print.re b/src/CLI/Print.re index 8b8146377c..abf8bae277 100644 --- a/src/CLI/Print.re +++ b/src/CLI/Print.re @@ -7,6 +7,7 @@ let exp_to_segment_settings: ExpToSegment.Settings.t = { hide_fixpoints: false, show_filters: true, show_unknown_as_hole: true, + multiline_list_tuples: false, }; let segmentize = diff --git a/src/haz3lcore/pretty/ExpToSegment.re b/src/haz3lcore/pretty/ExpToSegment.re index ee946d02ba..36cb4bf62f 100644 --- a/src/haz3lcore/pretty/ExpToSegment.re +++ b/src/haz3lcore/pretty/ExpToSegment.re @@ -13,6 +13,7 @@ module Settings = { hide_fixpoints: bool, show_filters: bool, show_unknown_as_hole: bool, + multiline_list_tuples: bool, }; let of_core = (~inline, settings: CoreSettings.t) => { @@ -22,9 +23,10 @@ module Settings = { hide_fixpoints: !settings.evaluation.show_fixpoints, show_filters: settings.evaluation.show_stepper_filters, show_unknown_as_hole: true, + multiline_list_tuples: false, }; - let editable = (~inline) => { + let editable = (~multiline_list_tuples, ~inline) => { { inline, fold_case_clauses: false, @@ -32,6 +34,7 @@ module Settings = { hide_fixpoints: false, show_filters: true, show_unknown_as_hole: true, + multiline_list_tuples, }; }; }; @@ -852,19 +855,25 @@ let rec exp_to_pretty = (~settings: Settings.t, exp: Exp.t): pretty => { IdTagged.ids(exp) |> List.hd, IdTagged.ids(exp) |> List.tl |> pad_ids(List.length(xs)), ); + let optional_newline = () => + settings.multiline_list_tuples + ? [Secondary(mk_newline(Id.mk()))] : []; let form = (x, xs) => mk_form( ListLitExp, id, [ - x + optional_newline() + @ x @ List.flatten( List.map2( - (id, x) => [mk_form(CommaExp, id, [])] @ x, + (id, x) => + [mk_form(CommaExp, id, [])] @ optional_newline() @ x, ids, xs, ), - ), + ) + @ optional_newline(), ], ); p_just([form(x, xs)]); @@ -973,10 +982,20 @@ let rec exp_to_pretty = (~settings: Settings.t, exp: Exp.t): pretty => { let+ x = go(x) and+ xs = xs |> List.map(go) |> all; let ids = IdTagged.ids(exp) |> pad_ids(List.length(xs)); - x + let optional_newline = () => + settings.multiline_list_tuples + ? [Secondary(mk_newline(Id.mk()))] : []; + + optional_newline() + @ x @ List.flatten( - List.map2((id, x) => [mk_form(CommaExp, id, [])] @ x, ids, xs), - ); + List.map2( + (id, x) => [mk_form(CommaExp, id, [])] @ optional_newline() @ x, + ids, + xs, + ), + ) + @ optional_newline(); | Label(l) => label_to_pretty( ~label_only_position=false, diff --git a/src/haz3lcore/projectors/ProjectorCore.re b/src/haz3lcore/projectors/ProjectorCore.re index d2a45fa4a4..a3c73c81f9 100644 --- a/src/haz3lcore/projectors/ProjectorCore.re +++ b/src/haz3lcore/projectors/ProjectorCore.re @@ -26,7 +26,8 @@ module Kind = { | SliderF | Card | Livelit - | TextArea; + | TextArea + | Keybinding; let livelit_projectors: list(t) = [ Checkbox, @@ -35,6 +36,7 @@ module Kind = { TextArea, Card, Livelit, + Keybinding, ]; let projectors: list(t) = livelit_projectors @ [Fold, Info, Probe]; @@ -53,6 +55,7 @@ module Kind = { | Card => "card" | Livelit => "livelit" | TextArea => "text" + | Keybinding => "keybinding" }; /* This must be updated and kept 1-to-1 with the above @@ -69,6 +72,7 @@ module Kind = { | "text" => TextArea | "livelit" => Livelit | "card" => Card + | "keybinding" => Keybinding | _ => failwith("Unknown projector kind") }; diff --git a/src/haz3lcore/projectors/ProjectorInit.re b/src/haz3lcore/projectors/ProjectorInit.re index 3c29b2696a..1e25f5687d 100644 --- a/src/haz3lcore/projectors/ProjectorInit.re +++ b/src/haz3lcore/projectors/ProjectorInit.re @@ -15,6 +15,7 @@ let to_module = (kind: ProjectorCore.Kind.t): (module Cooked) => | TextArea => (module Cook(TextAreaProj.M)) | Livelit => (module Cook(LivelitProj.M)) | Card => (module Cook(CardProj.M)) + | Keybinding => (module Cook(KeybindingProj.M)) }; let init = diff --git a/src/haz3lcore/projectors/implementations/KeybindingProj.re b/src/haz3lcore/projectors/implementations/KeybindingProj.re new file mode 100644 index 0000000000..3f1fc15f0f --- /dev/null +++ b/src/haz3lcore/projectors/implementations/KeybindingProj.re @@ -0,0 +1,250 @@ +open Util; +open ProjectorBase; +open Virtual_dom.Vdom; + +module M: Projector = { + [@deriving (show({with_path: false}), sexp, yojson)] + type model = { + committed_keybinding: string, + isRecording: bool, + }; + + [@deriving (show({with_path: false}), sexp, yojson)] + type action = + | StartRecording + | CommitRecording + | CancelRecording; + + let string_of = (any: Language.Any.t): option(string) => + switch (any) { + | Exp({term: Atom(String(s)), _}) => + Some(StringUtil.unescape_linebreaks(s)) + | _ => None + }; + + let init = (any: Language.Any.t) => + switch (string_of(any)) { + | Some(s) => + Some({ + committed_keybinding: s, + isRecording: false, + }) + | None => None + }; + + let get = (info: info): string => { + switch (info.syntax |> info.utility.seg_to_term) { + | Some(s) => + switch (string_of(s)) { + | Some(s) => s + | None => failwith("Keybinding: get: Not string literal") + } + | None => failwith("Keybinding: get: Not string literal") + }; + }; + + let format_keybinding = (keybinding: string): string => + if (keybinding == "") { + "Click to set"; + } else { + keybinding; + }; + + let format_key_combination = (key: Key.t): string => { + let key_name = + switch (key.key) { + | D(k) => k + | U(k) => k + }; + + let mods = + (key.ctrl == Down ? ["ctrl"] : []) + @ (key.meta == Down ? [key.sys == Mac ? "cmd" : "meta"] : []) + @ (key.alt == Down ? ["alt"] : []) + @ (key.shift == Down ? ["shift"] : []); + + // Ignore modifier-only keybindings + let key_name = + switch (key_name) { + | "Control" + | "Shift" + | "Alt" + | "Meta" => [] + | "ArrowUp" => ["up"] + | "ArrowDown" => ["down"] + | "ArrowLeft" => ["left"] + | "ArrowRight" => ["right"] + | " " => ["space"] + | _ => [String.lowercase_ascii(key_name)] + }; + + let keys = mods @ key_name; + + String.concat(" + ", keys); + }; + + let key_handler = (model, info, ~local, ~parent, evt) => { + open Effect; + let key = Key.mk(KeyDown, evt); + + switch (key.key) { + | D("Enter") => + /* Commit recording: update model with current syntax value and stop recording */ + Many([ + local(CommitRecording), + { + JsUtil.get_elem_by_id(Id.cls(info.id))##blur; + Stop_propagation; + }, + ]) + | D("Escape") => + /* Cancel recording: revert syntax to committed value and stop recording */ + Many([ + local(CancelRecording), + parent( + SetSyntax( + info.utility.term_to_seg( + Exp({ + term: Atom(String(model.committed_keybinding)), + annotation: Language.IdTagged.IdTag.fresh(), + }), + ), + ), + ), + { + JsUtil.get_elem_by_id(Id.cls(info.id))##blur; + Stop_propagation; + }, + ]) + | D("Backspace") => + /* Clear current keybinding during recording */ + Many([ + parent( + SetSyntax( + info.utility.term_to_seg( + Exp({ + term: Atom(String("")), + annotation: Language.IdTagged.IdTag.fresh(), + }), + ), + ), + ), + Stop_propagation, + ]) + | D("Tab") => + /* Prevent tab from leaving focus during recording */ + Many([Prevent_default, Stop_propagation]) + | _ when String.length(format_key_combination(key)) > 0 => + /* Update syntax with pressed key during recording */ + let key_str = format_key_combination(key); + Many([ + parent( + SetSyntax( + info.utility.term_to_seg( + Exp({ + term: Atom(String(key_str)), + annotation: Language.IdTagged.IdTag.fresh(), + }), + ), + ), + ), + Stop_propagation, + Prevent_default, + ]); + | _ => Stop_propagation + }; + }; + + let focusable = Focusable.non; + let dynamics = false; + + let placeholder = (model, info) => { + /* Show what's currently displayed in the view */ + let current_display = info |> get; + let display_text = + if (model.isRecording) { + if (current_display == "") { + "Recording..."; + } else { + current_display ++ " ●"; + }; + } else { + format_keybinding(current_display); + }; + ProjectorCore.Shape.inline(1 + String.length(display_text)); + }; + + let update = (model, info, action) => { + switch (action) { + | StartRecording => { + /* Capture current syntax value as committed value */ + committed_keybinding: info |> get, + isRecording: true, + } + | CommitRecording => { + /* Update model with current syntax value */ + committed_keybinding: info |> get, + isRecording: false, + } + | CancelRecording => { + /* Just stop recording, model already has the committed value */ + + ...model, + isRecording: false, + } + }; + }; + + let view = (model, info, ~local, ~parent, ~view_seg as _) => { + let base_class = "keybinding"; + let recording_class = model.isRecording ? "keybinding-recording" : ""; + let all_classes = + [base_class, recording_class] |> List.filter(s => s != ""); + + /* Get current display value from syntax */ + let current_display = info |> get; + + /* Show different text based on state */ + let display_text = + if (model.isRecording) { + if (current_display == "") { + "Recording..."; + } else { + current_display ++ " ●"; + }; + } else { + format_keybinding(current_display); + }; + + ProjectorBase.View.mk( + Node.div( + ~attrs= + [ + Attr.id(Id.cls(info.id)), + Attr.classes(all_classes), + Attr.on_click(_ => + Effect.Many([local(StartRecording), Effect.Stop_propagation]) + ), + ] + @ ( + if (model.isRecording) { + [ + Attr.on_keydown(key_handler(model, info, ~local, ~parent)), + Attr.on_focus(_ => Effect.Stop_propagation), + Attr.on_blur(_ + /* Cancel recording if focus is lost during recording */ + => Effect.Many([local(CancelRecording)])), + ]; + } else { + [ + Attr.on_focus(_ => Effect.Stop_propagation), + Attr.on_blur(_ => Effect.Stop_propagation), + ]; + } + ) + @ [Attr.tabindex(0)], + [Node.text(display_text)], + ), + ); + }; +}; diff --git a/src/haz3lcore/zipper/action/Introduce.re b/src/haz3lcore/zipper/action/Introduce.re index 623b10726a..7ad2a9aa4d 100644 --- a/src/haz3lcore/zipper/action/Introduce.re +++ b/src/haz3lcore/zipper/action/Introduce.re @@ -226,6 +226,7 @@ module Make = hide_fixpoints: false, show_filters: true, show_unknown_as_hole: true, + multiline_list_tuples: false, }, term, already_parenthesized(z), diff --git a/src/util/JsUtil.re b/src/util/JsUtil.re index 5fae47089b..8739795ac2 100644 --- a/src/util/JsUtil.re +++ b/src/util/JsUtil.re @@ -210,6 +210,15 @@ let set_select_value = (select_id, value) => { ); }; +let set_css_variable = (name: string, value: string) => { + let doc = Dom_html.document; + let root = doc##.documentElement; + let style: Js.t(Dom_html.cssStyleDeclaration) = root##.style; + + let _ = + style##setProperty(Js.string(name), Js.string(value), Js.undefined); + (); +}; let prompt = (message: string, default: string): option(string) => { Js.Opt.to_option( Dom_html.window##prompt(Js.string(message), Js.string(default)), diff --git a/src/web/NinjaKeys.re b/src/web/NinjaKeys.re index f986ac9252..b34fda68fb 100644 --- a/src/web/NinjaKeys.re +++ b/src/web/NinjaKeys.re @@ -16,3 +16,21 @@ let open_command_palette = (): unit => { [||] // Can't use ##.open because open is a reserved keyword ); }; + +let update_shortcut_hotkey = (id, hotkey: string): unit => { + let data = Js.Unsafe.get(elem(), "data"); + + // Map over the array data and if the id matches, update the hotkey + let new_data = + Array.map( + item => { + let item_id = Js.Unsafe.get(item, "id") |> Js.to_string; + if (item_id == id) { + Js.Unsafe.set(item, "hotkey", Js.Optdef.option(Some(hotkey))); + }; + item; + }, + data |> Js.to_array, + ); + Js.Unsafe.set(elem(), "data", Js.array(new_data)); +}; diff --git a/src/web/PersistentData.re b/src/web/PersistentData.re index 2e33c622f4..976a87d318 100644 --- a/src/web/PersistentData.re +++ b/src/web/PersistentData.re @@ -4,4 +4,5 @@ open Util; type t = { scratch: (int, list((string, CellEditor.Model.persistent))), documentation: (int, list((string, CellEditor.Model.persistent))), + configuration: (int, list((string, CellEditor.Model.persistent))), }; diff --git a/src/web/Store.re b/src/web/Store.re index 95c0155551..d453b4a92a 100644 --- a/src/web/Store.re +++ b/src/web/Store.re @@ -9,6 +9,7 @@ type key = | Mode | Scratch | Documentation + | Configuration | Tutorial(Haz3lcore.Id.t) | CurrentTutorial | CurrentExercise @@ -22,6 +23,7 @@ let key_to_string = | Mode => "MODE" | Scratch => "SAVE_SCRATCH" | Documentation => "SAVE_DOCUMENTATION" + | Configuration => "SAVE_CONFIGURATION" | Tutorial(id) => Haz3lcore.Id.to_string(id) | CurrentTutorial => "CUR_TUTORIAL" | CurrentExercise => "CUR_EXERCISE" diff --git a/src/web/app/Page.re b/src/web/app/Page.re index 25aa0beb13..e5e9155894 100644 --- a/src/web/app/Page.re +++ b/src/web/app/Page.re @@ -93,6 +93,7 @@ module Update = { | Documentation(m) => (List.nth(m.scratchpads, m.current) |> snd).editor | Tutorial(m) => List.nth(m.exercises, m.current).cells.user_impl.editor | Exercises(m) => List.nth(m.exercises, m.current).cells.user_impl.editor + | Config(m) => (List.nth(m.configs, m.current) |> snd).editor }; let update_global = @@ -165,6 +166,32 @@ module Update = { | ExportForInit => let (filename, content) = switch (model.editors) { + | Config(model) => + let current = List.nth(model.configs, model.current); + let filename = + ( + current + |> fst + |> ConfigurationMode.Model.config_name_of_type + |> StringUtil.sanitize_filename + ) + ++ ".ml"; + + let content = + Haz3lcore.( + [%derive.show: (string, PersistentSegment.t)](( + current |> fst |> ConfigurationMode.Model.config_name_of_type, + current + |> snd + |> ((e: CellEditor.Model.t) => e.editor) + |> ( + (e: CodeWithStatics.Model.t) => + Zipper.zip(e.editor.state.zipper) + ) + |> PersistentSegment.persist, + )) + ); + (filename, content); | Scratch(model) | Documentation(model) => let current = List.nth(model.scratchpads, model.current); diff --git a/src/web/app/editors/Editors.re b/src/web/app/editors/Editors.re index 4876b4d5c1..733495f8b5 100644 --- a/src/web/app/editors/Editors.re +++ b/src/web/app/editors/Editors.re @@ -5,20 +5,23 @@ module Model = { type mode = | Scratch | Documentation - | Tutorial - | Exercises; + | Exercises + | Config + | Tutorial; [@deriving (show({with_path: false}), sexp, yojson)] type t = | Scratch(ScratchMode.Model.t) | Documentation(ScratchMode.Model.t) - | Tutorial(TutorialsMode.Model.t) - | Exercises(ExercisesMode.Model.t); + | Exercises(ExercisesMode.Model.t) + | Config(ConfigurationMode.Model.t) + | Tutorial(TutorialsMode.Model.t); let mode_string: t => string = fun | Scratch(_) => "Scratch" | Documentation(_) => "Documentation" + | Config(_) => "Configuration" | Tutorial(_) => "Tutorial" | Exercises(_) => "Exercises"; }; @@ -70,6 +73,11 @@ module Store = { ExercisesMode.Store.load(~settings, ~instructor_mode) |> ExercisesMode.Model.unpersist(~instructor_mode), ) + | Config => + Model.Config( + ConfigurationMode.StoreConfig.load() + |> ConfigurationMode.Model.unpersist(~settings), + ) }; }; }; @@ -88,6 +96,9 @@ module Store = { | Model.Exercises(m) => StoreMode.save(Exercises); ExercisesMode.Store.save(~instructor_mode, m); + | Model.Config(m) => + StoreMode.save(Config); + ConfigurationMode.StoreConfig.save(ConfigurationMode.Model.persist(m)); }; }; }; @@ -100,6 +111,7 @@ module Update = { | SwitchMode(Model.mode) // Scratch & Documentation | Scratch(ScratchMode.Update.t) + | Configuration(ConfigurationMode.Update.t) | Tutorial(TutorialsMode.Update.t) // Exercises | Exercises(ExercisesMode.Update.t); @@ -108,6 +120,7 @@ module Update = { switch (action) { | SwitchMode(_) => true | Scratch(action) => ScratchMode.Update.can_undo(action) + | Configuration(action) => ConfigurationMode.Update.can_undo(action) | Tutorial(action) => TutorialsMode.Update.can_undo(action) | Exercises(action) => ExercisesMode.Update.can_undo(action) }; @@ -133,6 +146,16 @@ module Update = { m, ); Model.Scratch(scratch); + | (Configuration(action), Config(m)) => + let* config = + ConfigurationMode.Update.update( + ~schedule_action=a => schedule_action(Configuration(a)), + ~settings=globals.settings, + ~send_assistant_insertion_info, + action, + m, + ); + Model.Config(config); | (Scratch(action), Documentation(m)) => let* scratch = ScratchMode.Update.update( @@ -165,12 +188,17 @@ module Update = { | (Tutorial(_), Exercises(_)) | (Tutorial(_), Scratch(_)) | (Tutorial(_), Documentation(_)) + | (Tutorial(_), Config(_)) | (Scratch(_), Exercises(_)) | (Scratch(_), Tutorial(_)) + | (Scratch(_), Config(_)) | (Exercises(_), Scratch(_)) - | (Exercises(_), Documentation(_)) => model |> return_quiet + | (Exercises(_), Config(_)) + | (Exercises(_), Documentation(_)) + | (Configuration(_), _) => model |> return_quiet | (SwitchMode(Scratch), Scratch(_)) | (SwitchMode(Documentation), Documentation(_)) + | (SwitchMode(Config), Config(_)) | (Exercises(_), Tutorial(_)) => model |> return_quiet | (SwitchMode(Exercises), Exercises(_)) => model |> return_quiet | (SwitchMode(Scratch), _) => @@ -185,6 +213,12 @@ module Update = { |> ScratchMode.Model.unpersist(~settings=globals.settings.core), ) |> return + | (SwitchMode(Config), _) => + Model.Config( + ConfigurationMode.StoreConfig.load() + |> ConfigurationMode.Model.unpersist(~settings=globals.settings.core), + ) + |> return | (SwitchMode(Tutorial), Tutorial(_)) => model |> return_quiet | (SwitchMode(Tutorial), _) => Model.Tutorial( @@ -232,6 +266,15 @@ module Update = { m, ), ) + | Model.Config(m) => + Model.Config( + ConfigurationMode.Update.calculate( + ~schedule_action=a => schedule_action(Configuration(a)), + ~settings, + ~is_edited, + m, + ), + ) | Model.Tutorial(m) => Model.Tutorial( TutorialsMode.Update.calculate( @@ -260,7 +303,8 @@ module Selection = { type t = | Scratch(ScratchMode.Selection.t) | Exercises(ExerciseMode.Selection.t) - | Tutorial(TutorialMode.Selection.t); + | Tutorial(TutorialMode.Selection.t) + | Configuration(ConfigurationMode.Selection.t); let get_cursor_info = (~selection: t, editors: Model.t): cursor(Update.t) => { switch (selection, editors) { @@ -270,6 +314,9 @@ module Selection = { | (Scratch(selection), Documentation(m)) => let+ ci = ScratchMode.Selection.get_cursor_info(~selection, m); Update.Scratch(ci); + | (Configuration(selection), Config(m)) => + let+ ci = ConfigurationMode.Selection.get_cursor_info(~selection, m); + Update.Configuration(ci); | (Tutorial(selection), Tutorial(m)) => let+ ci = TutorialsMode.Selection.get_cursor_info(~selection, m); Update.Tutorial(ci); @@ -280,10 +327,13 @@ module Selection = { | (Exercises(_), Tutorial(_)) | (Scratch(_), Exercises(_)) | (Exercises(_), Scratch(_)) + | (Exercises(_), Documentation(_)) + | (Exercises(_), Config(_)) + | (Configuration(_), _) + | (_, Config(_)) | (Tutorial(_), Scratch(_)) | (Tutorial(_), Exercises(_)) - | (Tutorial(_), Documentation(_)) - | (Exercises(_), Documentation(_)) => empty + | (Tutorial(_), Documentation(_)) => empty }; }; @@ -296,6 +346,9 @@ module Selection = { | (Some(Scratch(selection)), Documentation(m)) => ScratchMode.Selection.handle_key_event(~selection, ~event, m) |> Option.map(x => Update.Scratch(x)) + | (Some(Configuration(selection)), Config(m)) => + ConfigurationMode.Selection.handle_key_event(~selection, ~event, m) + |> Option.map(x => Update.Configuration(x)) | (Some(Tutorial(selection)), Tutorial(m)) => TutorialsMode.Selection.handle_key_event(~selection, ~event, m) |> Option.map(x => Update.Tutorial(x)) @@ -307,9 +360,13 @@ module Selection = { | (Some(Exercises(_)), Tutorial(_)) | (Some(Exercises(_)), Scratch(_)) | (Some(Exercises(_)), Documentation(_)) - | (Some(Tutorial(_)), Scratch(_)) + | (Some(Exercises(_)), Config(_)) | (Some(Tutorial(_)), Documentation(_)) + | (Some(Tutorial(_)), Config(_)) + | (Some(Tutorial(_)), Scratch(_)) | (Some(Tutorial(_)), Exercises(_)) + | (Some(Configuration(_)), _) + | (_, Config(_)) | (None, _) => None }; }; @@ -320,6 +377,11 @@ module Selection = { | Scratch(m) => ScratchMode.Selection.jump_to_tile(tile, m) |> Option.map(((x, y)) => (Update.Scratch(x), Scratch(y))) + | Config(m) => + ConfigurationMode.Selection.jump_to_tile(tile, m) + |> Option.map(((x, y)) => + (Update.Configuration(x), Configuration(y)) + ) | Documentation(m) => ScratchMode.Selection.jump_to_tile(tile, m) |> Option.map(((x, y)) => (Update.Scratch(x), Scratch(y))) @@ -335,6 +397,7 @@ module Selection = { fun | Model.Scratch(_) => Scratch(Cell(MainEditor)) | Model.Documentation(_) => Scratch(Cell(MainEditor)) + | Model.Config(_) => Scratch(Cell(MainEditor)) | Model.Tutorial(_) => Tutorial(Cell(Tutorial.YourImpl, MainEditor)) | Model.Exercises(_) => Exercises(Cell(Exercise.Prelude, MainEditor)); }; @@ -384,6 +447,20 @@ module View = { ~inject=a => Update.Scratch(a) |> inject, m, ) + | Config(m) => + ConfigurationMode.View.view( + ~signal= + fun + | MakeActive(s) => signal(MakeActive(Configuration(s))), + ~globals, + ~selected= + switch (selection) { + | Some(Configuration(s)) => Some(s) + | _ => None + }, + ~inject=a => Update.Configuration(a) |> inject, + m, + ) | Tutorial(m) => TutorialsMode.View.view( ~signal= @@ -418,7 +495,18 @@ module View = { let file_menu = (~globals, ~inject, editors: Model.t) => switch (editors) { - | Scratch(s) + | Scratch(s) => + ScratchMode.View.file_menu( + ~globals, + ~inject=x => inject(Update.Scratch(x)), + s, + ) + | Config(s) => + ConfigurationMode.View.file_menu( + ~globals, + ~inject=x => inject(Update.Configuration(x)), + s, + ) | Documentation(s) => ScratchMode.View.file_menu( ~globals, @@ -453,6 +541,7 @@ module View = { | "Documentation" => inject(Update.SwitchMode(Documentation)) | "Tutorial" => inject(Update.SwitchMode(Tutorial)) | "Exercises" => inject(Update.SwitchMode(Exercises)) + | "Configuration" => inject(Update.SwitchMode(Config)) | _ => failwith("Invalid mode") ), ], @@ -465,12 +554,19 @@ module View = { | Documentation(_) => "Documentation" | Tutorial(_) => "Tutorial" | Exercises(_) => "Exercises" + | Config(_) => "Configuration" } ) == s, s, ), - ["Scratch", "Documentation", "Tutorial", "Exercises"], + [ + "Scratch", + "Documentation", + "Tutorial", + "Configuration", + "Exercises", + ], ), ), ], @@ -490,6 +586,11 @@ module View = { ~inject=a => Update.Scratch(a) |> inject, m, ) + | Config(m) => + ConfigurationMode.View.top_bar( + ~inject=a => Update.Configuration(a) |> inject, + m, + ) | Tutorial(m) => TutorialsMode.View.top_bar( ~globals, diff --git a/src/web/app/editors/cell/CellEditor.re b/src/web/app/editors/cell/CellEditor.re index 28cef16d58..9e35cc924b 100644 --- a/src/web/app/editors/cell/CellEditor.re +++ b/src/web/app/editors/cell/CellEditor.re @@ -40,6 +40,11 @@ module Model = { result: EvalResult.Model.unpersist(result), }; + let from_persistent_segment = (content: PersistentSegment.t): persistent => { + editor: content |> PersistentSegment.to_persistent_zipper, + result: EvalResult.Model.init |> EvalResult.Model.persist, + }; + let to_string = (model: t) => model.editor |> CodeEditable.Model.to_string; }; diff --git a/src/web/app/editors/result/EvalResult.re b/src/web/app/editors/result/EvalResult.re index 2943f71cbd..62cca352e9 100644 --- a/src/web/app/editors/result/EvalResult.re +++ b/src/web/app/editors/result/EvalResult.re @@ -96,7 +96,7 @@ module Update = { }; // Update is meant to make minimal changes to the model, and calculate will do the rest. - let update = (~settings, action, model: Model.t): Updated.t(Model.t) => + let update = (~settings, action, model: Model.t): Updated.t(Model.t) => { switch (action, model) { | (ToggleStepper, {display: Stepper(_), _}) => { @@ -134,6 +134,7 @@ module Update = { } |> Updated.return_quiet }; + }; let calculate = ( diff --git a/src/web/app/editors/stepper/InductionStep.re b/src/web/app/editors/stepper/InductionStep.re index 4743299fea..29144e1dbe 100644 --- a/src/web/app/editors/stepper/InductionStep.re +++ b/src/web/app/editors/stepper/InductionStep.re @@ -45,7 +45,11 @@ let init = (~exp: option(Exp.t)=?, ()) => { Editor.Model.mk( Zipper.unzip( ExpToSegment.exp_to_segment( - ~settings=ExpToSegment.Settings.editable(~inline=true), + ~settings= + ExpToSegment.Settings.editable( + ~inline=true, + ~multiline_list_tuples=false, + ), e, ), ), diff --git a/src/web/app/inspector/CursorInspector.re b/src/web/app/inspector/CursorInspector.re index 90c5acfad7..5a639b9dc4 100644 --- a/src/web/app/inspector/CursorInspector.re +++ b/src/web/app/inspector/CursorInspector.re @@ -83,6 +83,7 @@ let code_view_settings: Haz3lcore.ExpToSegment.Settings.t = { hide_fixpoints: false, show_filters: false, show_unknown_as_hole: true, + multiline_list_tuples: false, }; let view_any = (~globals, any: Any.t) => diff --git a/src/web/dune b/src/web/dune index b8bf093a56..05f8f2e0c6 100644 --- a/src/web/dune +++ b/src/web/dune @@ -13,7 +13,7 @@ (libraries language) (js_of_ocaml) (preprocess - (pps ppx_yojson_conv ppx_sexp_conv ppx_deriving.show))) + (pps ppx_yojson_conv ppx_sexp_conv ppx_deriving.show ppx_enumerate))) (library (name web) @@ -45,7 +45,8 @@ ppx_sexp_conv ppx_deriving.show ppx_yojson_conv - bonsai.ppx_bonsai))) + bonsai.ppx_bonsai + ppx_enumerate))) (executable (name main) @@ -59,7 +60,8 @@ ppx_let ppx_sexp_conv ppx_deriving.show - bonsai.ppx_bonsai))) + bonsai.ppx_bonsai + ppx_enumerate))) (executable (name worker) diff --git a/src/web/init/Init.re b/src/web/init/Init.re index d45d1fc6a8..6978b399da 100644 --- a/src/web/init/Init.re +++ b/src/web/init/Init.re @@ -23,15 +23,10 @@ let startup: PersistentData.t = { Livelits.out, ] |> List.map(((name, content: PersistentSegment.t)) => - ( - name, - { - editor: content |> PersistentSegment.to_persistent_zipper, - result: EvalResult.Model.init |> EvalResult.Model.persist, - }: CellEditor.Model.persistent, - ) + (name, CellEditor.Model.from_persistent_segment(content)) ), ), + configuration: (0, []), }; let find_documentation_slide = (name: string) => { diff --git a/src/web/util/ColorConfiguration.re b/src/web/util/ColorConfiguration.re new file mode 100644 index 0000000000..3a76d1c71e --- /dev/null +++ b/src/web/util/ColorConfiguration.re @@ -0,0 +1,869 @@ +type color = { + var_name: string, + color: string, +}; + +module LightMode = { + /* Base Colors - Core foundational colors */ + let base_colors = [ + { + var_name: "NONE", + color: "oklch(0% 0 0 / 0%)", + }, /* transparent */ + { + var_name: "SAND", + color: "oklch(99% 0.012 90)", + }, /* code background */ + { + var_name: "STONE", + color: "oklch(52% 0.03 220)", + }, /* code text */ + { + var_name: "BLACK", + color: "oklch(0% 0 0)", + } /* use sparingly */ + ]; + + /* Shale Colors - Focal syntax and top level UI */ + let shale_colors = [ + { + var_name: "BR1", + color: "oklch(85% 0.07 90)", + }, /* caret shard, token buffer */ + { + var_name: "BR2", + color: "oklch(from var(--BR1) 70% c h)", + }, /* exp shard arms */ + { + var_name: "BR3", + color: "oklch(from var(--BR1) 60% c h)", + }, /* top ui accent */ + { + var_name: "BR4", + color: "oklch(from var(--BR1) 50% c h)", + } /* top ui accent */ + ]; + + /* Clay Colors - Peripheral syntax */ + let clay_colors = [ + { + var_name: "T1", + color: "oklch(97% 0.025 90)", + }, /* buffer shards */ + { + var_name: "T2", + color: "oklch(from var(--T1) 94% c h)", + }, /* projector shards */ + { + var_name: "T3", + color: "oklch(from var(--T1) 91% c h)", + }, /* result background */ + { + var_name: "T4", + color: "oklch(from var(--T1) 88% c h)", + } /* darker background */ + ]; + + /* Molten Colors - Under construction */ + let molten_colors = [ + { + var_name: "Y0", + color: "oklch(95% 0.05 90)", + }, /* menu fill */ + { + var_name: "Y1", + color: "oklch(91% 0.11 95)", + }, /* selections */ + { + var_name: "Y2", + color: "oklch(88% 0.2 95)", + }, /* explicit hole */ + { + var_name: "Y3", + color: "oklch(71% 0.2 95)", + } /* incomplete shards */ + ]; + + /* Magma Colors - Error and opportunity states */ + let magma_colors = [ + { + var_name: "R0", + color: "oklch(85% 0.1 30)", + }, /* broken shard fill */ + { + var_name: "R1", + color: "oklch(60% 0.3 30)", + }, /* caret, error stroke */ + { + var_name: "R2", + color: "oklch(40% 0.3 30)", + } /* error text, broken shard text */ + ]; + + /* Glass Colors - Type system */ + let glass_colors = [ + { + var_name: "TYP", + color: "oklch(60% 0.2 300)", + }, /* type colors */ + { + var_name: "PAT", + color: "oklch(from var(--TYP) l c calc(h - 1 * 75))", + }, /* pattern colors */ + { + var_name: "TPAT", + color: "var(--PAT)", + }, /* type pattern colors */ + { + var_name: "LABEL", + color: "oklch(60% 0.2 180)", + } /* label colors */ + ]; + + /* Aura Colors - Documentation highlighting */ + let aura_colors = [ + { + var_name: "highlight-a", + color: "oklch(0.95 0.07 360)", + }, /* primary highlight */ + { + var_name: "highlight-b", + color: "oklch(from var(--highlight-a) l c calc(h - 1 * 120))", + }, /* secondary highlight */ + { + var_name: "highlight-c", + color: "oklch(from var(--highlight-a) l c calc(h - 2 * 120))", + } /* tertiary highlight */ + ]; + + /* Moss Colors - Success and affirmation states */ + let moss_colors = [ + { + var_name: "G0", + color: "oklch(70% 0.15 150)", + }, /* page title, passing tests */ + { + var_name: "G1", + color: "oklch(85% 0.15 150)", + }, /* passing tests hover */ + { + var_name: "G2", + color: "oklch(80% 0.05 150)", + }, /* comments */ + { + var_name: "GB0", + color: "oklch(70% 0.05 120)", + }, /* nut menu active */ + { + var_name: "GB1", + color: "oklch(45% 0.05 120)", + } /* nut menu fill */ + ]; + + /* UI Colors - Interface elements */ + let ui_colors = [ + { + var_name: "primary-accent", + color: "var(--G0)", + }, /* primary UI accent */ + { + var_name: "nut-menu", + color: "var(--GB1)", + }, /* navigation menu background */ + { + var_name: "nut-menu-active", + color: "var(--GB0)", + }, /* active menu state */ + { + var_name: "menu-bkg", + color: "var(--Y0)", + }, /* menu background */ + { + var_name: "menu-item-hover-bkg", + color: "var(--SAND)", + }, /* menu item hover */ + { + var_name: "menu-item-text", + color: "var(--STONE)", + }, /* menu text */ + { + var_name: "menu-outline", + color: "var(--BR2)", + }, /* menu borders */ + { + var_name: "menu-icon", + color: "var(--BR4)", + }, /* menu icons */ + { + var_name: "menu-group-name", + color: "var(--menu-icon)", + }, /* menu section headers */ + { + var_name: "menu-scroll-thumb", + color: "var(--menu-outline)", + }, /* scrollbar */ + { + var_name: "menu-scroll-track", + color: "var(--BR1)", + }, /* scrollbar track */ + { + var_name: "menu-divider", + color: "var(--CREASE)", + }, /* menu separators */ + { + var_name: "menu-shadow", + color: "var(--SHADOW)", + }, /* menu shadows */ + { + var_name: "ui-bkg", + color: "var(--T1)", + }, /* main UI background */ + { + var_name: "ui-header-text", + color: "var(--BR3)", + }, /* header text */ + { + var_name: "toggle-knob", + color: "var(--SAND)", + } /* toggle switches */ + ]; + + /* Code Colors - Tokens and decorations */ + let code_colors = [ + { + var_name: "main-bkg", + color: "var(--T3)", + }, /* main code background */ + { + var_name: "cell-active", + color: "var(--SAND)", + }, /* active cell background */ + { + var_name: "main-scroll-thumb", + color: "var(--BR1)", + }, /* scrollbar */ + { + var_name: "main-scroll-track", + color: "var(--NONE)", + }, /* scrollbar track */ + { + var_name: "cell-selected-accent", + color: "var(--R1)", + }, /* selection accent */ + { + var_name: "caret-color", + color: "var(--R1)", + }, /* text cursor */ + { + var_name: "error-hole-stroke", + color: "var(--R1)", + }, /* error indicators */ + { + var_name: "token-exp", + color: "var(--STONE)", + }, /* expression tokens */ + { + var_name: "token-pat", + color: "var(--PAT)", + }, /* pattern tokens */ + { + var_name: "token-typ", + color: "var(--TYP)", + }, /* type tokens */ + { + var_name: "token-tpat", + color: "var(--TPAT)", + }, /* type pattern tokens */ + { + var_name: "token-label", + color: "var(--LABEL)", + }, /* label tokens */ + { + var_name: "token-string-lit", + color: "var(--Y3)", + }, /* string literals */ + { + var_name: "token-comment", + color: "var(--G2)", + }, /* comments */ + { + var_name: "token-incomplete", + color: "var(--Y3)", + }, /* incomplete code */ + { + var_name: "token-inconsistent", + color: "var(--token-exp)", + }, /* inconsistent code */ + { + var_name: "token-buffer", + color: "var(--BR1)", + }, /* buffer tokens */ + { + var_name: "token-explicit-hole", + color: "var(--Y2)", + }, /* explicit holes */ + { + var_name: "token-explicit-hole-shadow", + color: "var(--BLACK)", + }, /* hole shadows */ + { + var_name: "token-secondary", + color: "var(--shard-exp)", + }, /* secondary text */ + { + var_name: "token-rul", + color: "var(--token-exp)", + }, /* rule tokens */ + { + var_name: "token-any", + color: "var(--R2)", + } /* any type tokens */ + ]; + + /* Shard Colors - Code decoration backgrounds */ + let shard_colors = [ + { + var_name: "shard-caret-exp", + color: "var(--T2)", + }, /* expression caret shards */ + { + var_name: "shard-lines-exp", + color: "var(--BR1)", + }, /* expression shard borders */ + { + var_name: "shard-exp", + color: "var(--T2)", + }, /* expression shards */ + { + var_name: "shard-caret-pat", + color: "oklch(from var(--token-pat) 95% calc(c/4) h)", + }, /* pattern caret shards */ + { + var_name: "shard-caret-typ", + color: "oklch(from var(--token-typ) 95% calc(c/4) h)", + }, /* type caret shards */ + { + var_name: "shard-caret-tpat", + color: "oklch(from var(--token-tpat) 95% calc(c/4) h)", + }, /* type pattern caret shards */ + { + var_name: "shard-pat", + color: "var(--shard-caret-pat)", + }, /* pattern shards */ + { + var_name: "shard-typ", + color: "var(--shard-caret-typ)", + }, /* type shards */ + { + var_name: "shard-tpat", + color: "var(--shard-caret-tpat)", + }, /* type pattern shards */ + { + var_name: "shard-selected", + color: "var(--Y1)", + }, /* selected shards */ + { + var_name: "shard-buffer", + color: "var(--T1)", + }, /* buffer shards */ + { + var_name: "shard_projector", + color: "var(--T2)", + }, /* projector shards */ + { + var_name: "shard-rul", + color: "var(--shard-exp)", + }, /* rule shards */ + { + var_name: "shard-lines-rul", + color: "var(--shard-lines-exp)", + }, /* rule shard borders */ + { + var_name: "shadow-selected", + color: "var(--R0)", + }, /* selection shadows */ + { + var_name: "shard-any", + color: "var(--shard-exp)", + }, /* any type shards */ + { + var_name: "shadow-any", + color: "var(--R0)", + } /* any type shadows */ + ]; + + /* Hole Colors - Empty and error holes */ + let hole_colors = [ + { + var_name: "empty-hole-stroke", + color: "var(--BR1)", + }, /* empty hole borders */ + { + var_name: "empty-hole-fill", + color: "var(--Y0)", + }, /* empty hole backgrounds */ + { + var_name: "error-hole-fill", + color: "var(--ERRHOLE)", + } /* error hole backgrounds */ + ]; + + /* Backpack Colors - Selection and targeting */ + let backpack_colors = [ + { + var_name: "backpack-selection", + color: "var(--shard-selected)", + }, /* selection backgrounds */ + { + var_name: "backpack-joiner", + color: "var(--backpack-selection)", + }, /* connection lines */ + { + var_name: "backpack-genie", + color: "var(--backpack-selection)", + }, /* genie indicators */ + { + var_name: "backpack-selection-outline", + color: "var(--light-page-color)", + }, /* selection borders */ + { + var_name: "backback-targets", + color: "var(--Y3)", + } /* target indicators */ + ]; + + /* Projector Colors - Code projection system */ + let projector_colors = [ + { + var_name: "textarea-indicated", + color: "var(--SAND)", + }, /* indicated text areas */ + { + var_name: "textarea-text", + color: "var(--BR3)", + } /* textarea text */ + ]; + + /* Dynamics Colors - Runtime and evaluation */ + let dynamics_colors = [ + { + var_name: "cell-result-text", + color: "var(--BR4)", + }, /* result text */ + { + var_name: "cell-result-border", + color: "var(--BR1)", + }, /* result borders */ + { + var_name: "cell-result-hidden", + color: "var(--BR1)", + }, /* hidden results */ + { + var_name: "eval-exception", + color: "var(--test-fail-active)", + }, /* evaluation errors */ + { + var_name: "eval-exception-stroke", + color: "var(--R2)", + }, /* error outlines */ + { + var_name: "step-hole-color", + color: "var(--G0)", + } /* stepper holes */ + ]; + + /* Context Inspector Colors - Code analysis UI */ + let ci_colors = [ + { + var_name: "ci-icon-bkg", + color: "var(--BR3)", + }, /* inspector icons */ + { + var_name: "ci-status-text", + color: "var(--BR4)", + }, /* status text */ + { + var_name: "ci-status-error-text", + color: "var(--R2)", + }, /* error text */ + { + var_name: "ci-status-error-bkg", + color: "var(--test-fail-active)", + }, /* error backgrounds */ + { + var_name: "context-inspector-colon", + color: "var(--BR2)", + } /* separator colons */ + ]; + + /* Exercise Mode Colors - Educational features */ + let exercise_colors = [ + { + var_name: "cell-caption", + color: "var(--BR2)", + }, /* exercise captions */ + { + var_name: "cell-result", + color: "var(--T3)", + }, /* exercise results */ + { + var_name: "cell-exercises-border", + color: "var(--BR2)", + }, /* exercise borders */ + { + var_name: "test-panel-bkg", + color: "var(--menu-bkg)", + }, /* test panel background */ + { + var_name: "test-percent-text", + color: "var(--SAND)", + }, /* test percentage text */ + { + var_name: "test-pass", + color: "var(--G0)", + }, /* passing tests */ + { + var_name: "test-pass-active", + color: "var(--G1)", + }, /* active passing tests */ + { + var_name: "test-fail", + color: "var(--R1)", + }, /* failing tests */ + { + var_name: "test-fail-active", + color: "var(--R0)", + }, /* active failing tests */ + { + var_name: "test-indet", + color: "var(--BR2)", + }, /* indeterminate tests */ + { + var_name: "test-indet-active", + color: "var(--BR1)", + } /* active indeterminate tests */ + ]; + + /* Special Colors - Miscellaneous utility colors */ + let special_colors = [ + { + var_name: "textarea-v-stripe", + color: "oklch(78% 0.14 6 / 55%)", + }, /* vertical stripes */ + { + var_name: "textarea-h-stripe", + color: "oklch(87% 0.07 246)", + }, /* horizontal stripes */ + { + var_name: "textarea-h-strip-selected", + color: "oklch(68% 0.14 76 / 30%)", + }, /* selected stripes */ + { + var_name: "SHADOW", + color: "oklch(50% 0.1 90 / 33%)", + }, /* general shadows */ + { + var_name: "ERRHOLE", + color: "oklch(96% 0.02 47)", + }, /* error hole backgrounds */ + { + var_name: "CREASE", + color: "oklch(0% 0 0 / 40%)", + } /* crease/divider lines */ + ]; + + /* Projector Colors - Interactive code analysis system */ + let projector_colors_extended = [ + { + var_name: "live-env-bkg", + color: "var(--T3)", + }, /* live environment background */ + { + var_name: "num-closures", + color: "var(--Y1)", + }, /* number of closures indicator */ + { + var_name: "num-closures-indicated", + color: "var(--R1)", + }, /* indicated number of closures */ + { + var_name: "exp-ap", + color: "hsl(265, 75%, 80%)", + }, /* expression application */ + { + var_name: "pat-ap", + color: "hsl(220, 75%, 80%)", + }, /* pattern application */ + { + var_name: "exp-indicated", + color: "var(--G0)", + }, /* indicated expression state */ + { + var_name: "pat-indicated", + color: "var(--PAT)", + }, /* indicated pattern state */ + { + var_name: "exp-ap-indicated", + color: "var(--TYP)", + }, /* indicated expression application */ + { + var_name: "exp-base", + color: "hsl(120, 40%, 85%)", + }, /* base expression background */ + { + var_name: "pat-base", + color: "hsl(170, 40%, 85%)", + }, /* base pattern background */ + { + var_name: "exp-shadow", + color: "oklch(0.55 0.15 150)", + }, /* expression shadow */ + { + var_name: "pat-shadow", + color: "oklch(0.5 0.1 245)", + }, /* pattern shadow */ + { + var_name: "exp-ap-shadow", + color: "oklch(0.5 0.1 300)", + }, /* expression application shadow */ + { + var_name: "exp-cell", + color: "hsl(115, 30%, 70%)", + }, /* expression cell background */ + { + var_name: "pat-cell", + color: "hsl(165, 30%, 70%)", + }, /* pattern cell background */ + { + var_name: "main-base", + color: "hsl(281, 80%, 95%)", + }, /* type projector main background */ + { + var_name: "main-shadow", + color: "hsl(281, 40%, 25%)", + }, /* type projector shadow */ + { + var_name: "main-indicated", + color: "var(--TYP)", + } /* type projector indicated state */ + ]; + + /* Combined color configuration */ + let vars = + List.concat([ + base_colors, + shale_colors, + clay_colors, + molten_colors, + magma_colors, + glass_colors, + aura_colors, + moss_colors, + ui_colors, + code_colors, + shard_colors, + hole_colors, + backpack_colors, + projector_colors, + projector_colors_extended, + dynamics_colors, + ci_colors, + exercise_colors, + special_colors, + ]); +}; + +module DarkMode = { + let vars = + [ + ("NONE", "oklch(0% 0 0 / 0%)"), + ("SAND", "oklch(25% 0.015 240)"), + ("STONE", "oklch(75% 0.03 250)"), + ("BLACK", "oklch(0% 0 0)"), + ("BR1", "oklch(30% 0.04 250)"), + ("BR2", "oklch(from var(--BR1) 40% c h)"), + ("BR3", "oklch(from var(--BR1) 55% c h)"), + ("BR4", "oklch(from var(--BR1) 70% c h)"), + ("T1", "oklch(15% 0.02 250)"), + ("T2", "oklch(from var(--T1) 18% c h)"), + ("T3", "oklch(from var(--T1) 22% c h)"), + ("T4", "oklch(from var(--T1) 26% c h)"), + ("Y0", "oklch(22% 0.06 95)"), + ("Y1", "oklch(0.32 0.09 169.5)"), + ("Y2", "oklch(45% 0.15 95)"), + ("Y3", "oklch(0.65 0.18 118.38)"), + ("R0", "oklch(40% 0.1 30)"), + ("R1", "oklch(55% 0.25 30)"), + ("R2", "oklch(70% 0.3 30)"), + ("TYP", "oklch(70% 0.18 300)"), + ("PAT", "oklch(from var(--TYP) l c calc(h - 1 * 75))"), + ("TPAT", "var(--PAT)"), + ("LABEL", "oklch(75% 0.15 210)"), + ("highlight-a", "oklch(65% 0.1 260)"), + ("highlight-b", "oklch(from var(--highlight-a) l c calc(h - 1 * 120))"), + ("highlight-c", "oklch(from var(--highlight-a) l c calc(h - 2 * 120))"), + ("G0", "oklch(65% 0.15 150)"), + ("G1", "oklch(75% 0.15 150)"), + ("G2", "oklch(60% 0.05 150)"), + ("GB0", "oklch(60% 0.05 200)"), + ("GB1", "oklch(35% 0.05 200)"), + ("primary-accent", "var(--G0)"), + ("nut-menu", "var(--G2)"), + ("nut-menu-active", "var(--GB0)"), + ("menu-bkg", "var(--T1)"), + ("menu-item-hover-bkg", "oklch(95% 0.015 240)"), + ("menu-item-text", "var(--STONE)"), + ("menu-outline", "var(--BR2)"), + ("menu-icon", "var(--BR4)"), + ("menu-group-name", "var(--menu-icon)"), + ("menu-scroll-thumb", "var(--menu-outline)"), + ("menu-scroll-track", "var(--BR1)"), + ("menu-divider", "var(--CREASE)"), + ("menu-shadow", "var(--SHADOW)"), + ("ui-bkg", "var(--T2)"), + ("ui-header-text", "var(--BR4)"), + ("toggle-knob", "var(--SAND)"), + ("main-bkg", "var(--T3)"), + ("cell-active", "oklch(30% 0.02 240)"), + ("main-scroll-thumb", "var(--BR3)"), + ("main-scroll-track", "var(--NONE)"), + ("cell-selected-accent", "var(--R1)"), + ("caret-color", "var(--R1)"), + ("error-hole-stroke", "var(--R1)"), + ("token-exp", "var(--STONE)"), + ("token-pat", "var(--PAT)"), + ("token-typ", "var(--TYP)"), + ("token-tpat", "var(--TPAT)"), + ("token-label", "var(--LABEL)"), + ("token-string-lit", "var(--Y3)"), + ("token-comment", "var(--G2)"), + ("token-incomplete", "var(--Y3)"), + ("token-inconsistent", "var(--token-exp)"), + ("token-buffer", "var(--BR3)"), + ("token-explicit-hole", "var(--Y2)"), + ("token-explicit-hole-shadow", "var(--BLACK)"), + ("token-secondary", "var(--shard-exp)"), + ("token-rul", "var(--token-exp)"), + ("token-any", "var(--R2)"), + ("shard-caret-exp", "var(--T2)"), + ("shard-lines-exp", "var(--BR2)"), + ("shard-exp", "var(--T2)"), + ("shard-caret-pat", "oklch(from var(--token-pat) 40% calc(c/3) h)"), + ("shard-caret-typ", "oklch(from var(--token-typ) 40% calc(c/3) h)"), + ("shard-caret-tpat", "oklch(from var(--token-tpat) 40% calc(c/3) h)"), + ("shard-pat", "var(--shard-caret-pat)"), + ("shard-typ", "var(--shard-caret-typ)"), + ("shard-tpat", "var(--shard-caret-tpat)"), + ("shard-selected", "var(--Y1)"), + ("shard-buffer", "var(--T1)"), + ("shard_projector", "var(--T2)"), + ("shard-rul", "var(--shard-exp)"), + ("shard-lines-rul", "var(--shard-lines-exp)"), + ("shadow-selected", "var(--R0)"), + ("shard-any", "var(--shard-exp)"), + ("shadow-any", "var(--R0)"), + ("empty-hole-stroke", "var(--BR3)"), + ("empty-hole-fill", "var(--T2)"), + ("error-hole-fill", "var(--ERRHOLE)"), + ("backpack-selection", "var(--shard-selected)"), + ("backpack-joiner", "var(--backpack-selection)"), + ("backpack-genie", "var(--backpack-selection)"), + ("backpack-selection-outline", "oklch(80% 0.02 250)"), + ("backback-targets", "var(--Y2)"), + ("textarea-indicated", "var(--SAND)"), + ("textarea-text", "var(--STONE)"), + ("live-env-bkg", "var(--T3)"), + ("num-closures", "var(--Y1)"), + ("num-closures-indicated", "var(--R1)"), + ("exp-ap", "hsl(265, 75%, 50%)"), + ("pat-ap", "hsl(220, 75%, 50%)"), + ("exp-indicated", "var(--G0)"), + ("pat-indicated", "var(--PAT)"), + ("exp-ap-indicated", "var(--TYP)"), + ("exp-base", "hsl(210, 20%, 35%)"), + ("pat-base", "hsl(230, 20%, 35%)"), + ("exp-shadow", "oklch(0.3 0.1 230)"), + ("pat-shadow", "oklch(0.25 0.1 260)"), + ("exp-ap-shadow", "oklch(0.25 0.1 300)"), + ("exp-cell", "hsl(215, 20%, 40%)"), + ("pat-cell", "hsl(240, 20%, 40%)"), + ("main-base", "hsl(250, 30%, 15%)"), + ("main-shadow", "hsl(250, 30%, 5%)"), + ("main-indicated", "var(--TYP)"), + ("cell-result-text", "var(--BR4)"), + ("cell-result-border", "var(--BR2)"), + ("cell-result-hidden", "var(--BR1)"), + ("eval-exception", "var(--test-fail-active)"), + ("eval-exception-stroke", "var(--R2)"), + ("step-hole-color", "var(--G0)"), + ("ci-icon-bkg", "var(--BR2)"), + ("ci-status-text", "var(--STONE)"), + ("ci-status-error-text", "var(--R2)"), + ("ci-status-error-bkg", "var(--test-fail-active)"), + ("context-inspector-colon", "var(--BR3)"), + ("cell-caption", "var(--BR3)"), + ("cell-result", "var(--T3)"), + ("cell-exercises-border", "var(--BR3)"), + ("test-panel-bkg", "var(--menu-bkg)"), + ("test-percent-text", "var(--SAND)"), + ("test-pass", "var(--G0)"), + ("test-pass-active", "var(--G1)"), + ("test-fail", "var(--R1)"), + ("test-fail-active", "var(--R0)"), + ("test-indet", "var(--BR2)"), + ("test-indet-active", "var(--BR1)"), + ("textarea-v-stripe", "oklch(35% 0.14 230 / 55%)"), + ("textarea-h-stripe", "oklch(30% 0.07 240)"), + ("textarea-h-strip-selected", "oklch(45% 0.14 230 / 30%)"), + ("SHADOW", "oklch(10% 0.08 250 / 50%)"), + ("ERRHOLE", "oklch(40% 0.02 47)"), + ("CREASE", "oklch(100% 0 0 / 25%)"), + ] + |> List.map(((var_name, color)) => + { + var_name, + color, + } + ); +}; +let color_theme = (vars: list(color)): Language.Exp.t => { + open Language; + open IdTagged.FreshGrammar.Exp; + let lits = + List.map( + ({var_name, color}) => tuple([string(var_name), string(color)]), + vars, + ); + list_lit(lits); +}; + +let segment = { + open Language; + open Haz3lcore; + let light = color_theme(LightMode.vars); + let dark = color_theme(DarkMode.vars); + let exp = + IdTagged.FreshGrammar.( + Exp.( + let_( + Pat.var("light"), + light, + let_( + Pat.var("dark"), + dark, + if_(bool(true), var("light"), var("dark")), + ), + ) + ) + ); + + ExpToSegment.exp_to_segment( + ~settings= + ExpToSegment.Settings.editable( + ~inline=false, + ~multiline_list_tuples=true, + ), + exp, + ) + |> PersistentSegment.persist; +}; diff --git a/src/web/util/ShortcutConfiguration.re b/src/web/util/ShortcutConfiguration.re new file mode 100644 index 0000000000..19c6aa85b5 --- /dev/null +++ b/src/web/util/ShortcutConfiguration.re @@ -0,0 +1,252 @@ +open Language; +open Language.Unboxing; + +type shortcut = { + action_name: string, + hotkey: string, +}; + +module DefaultConfiguration = { + /* Default shortcut configuration - extracted from Shortcuts.ml */ + let shortcuts = [ + { + action_name: "Undo", + hotkey: "ctrl+z", + }, + { + action_name: "Redo", + hotkey: "ctrl+shift+z", + }, + { + action_name: "Go to Definition", + hotkey: "F12", + }, + { + action_name: "Go to Previous Hole", + hotkey: "shift+tab", + }, + { + action_name: "Go To Next Hole", + hotkey: "?", + }, + { + action_name: "Select current term", + hotkey: "ctrl+d", + }, + { + action_name: "Select All", + hotkey: "ctrl+a", + }, + { + action_name: "Toggle Selection Focus", + hotkey: "?", + }, + { + action_name: "Set Selection Focus Left", + hotkey: "ctrl+alt+shift+left", + }, + { + action_name: "Set Selection Focus Right", + hotkey: "ctrl+alt+shift+right", + }, + { + action_name: "Fold", + hotkey: "alt + f", + }, + { + action_name: "Probe", + hotkey: "alt+v", + }, + { + action_name: "Type", + hotkey: "alt+t", + }, + { + action_name: "Livelit", + hotkey: "alt+l", + }, + { + action_name: "Toggle Statics", + hotkey: "?", + }, + { + action_name: "Toggle Completion", + hotkey: "?", + }, + { + action_name: "Toggle Show Whitespace", + hotkey: "?", + }, + { + action_name: "Toggle Print Benchmarks", + hotkey: "?", + }, + { + action_name: "Toggle Toggle Dynamics", + hotkey: "?", + }, + { + action_name: "Toggle Show Elaboration", + hotkey: "?", + }, + { + action_name: "Toggle Show Function Bodies", + hotkey: "?", + }, + { + action_name: "Toggle Show Case Clauses", + hotkey: "?", + }, + { + action_name: "Toggle Show fixpoints", + hotkey: "?", + }, + { + action_name: "Toggle Show Ascription Steps", + hotkey: "?", + }, + { + action_name: "Toggle Show Lookup Steps", + hotkey: "?", + }, + { + action_name: "Toggle Show Stepper Filters", + hotkey: "?", + }, + { + action_name: "Toggle Show Hidden Steps", + hotkey: "?", + }, + { + action_name: "Toggle Show Sidebar", + hotkey: "?", + }, + { + action_name: "Toggle Show Docs Feedback", + hotkey: "?", + }, + { + action_name: "TyDi Assistant", + hotkey: "ctrl+/", + }, + { + action_name: "Export Scratch Slide", + hotkey: "?", + }, + { + action_name: "Export For Init", + hotkey: "?", + }, + { + action_name: "Export Submission", + hotkey: "?", + }, + { + action_name: "Reparse Current Editor", + hotkey: "?", + }, + { + action_name: "Run Benchmark", + hotkey: "F7", + }, + { + action_name: "Introduce", + hotkey: "ctrl+i", + }, + { + action_name: "Add New Buffer", + hotkey: "?", + }, + { + action_name: "Rename Current Buffer", + hotkey: "?", + }, + { + action_name: "Delete Current Buffer", + hotkey: "?", + }, + { + action_name: "Export Exercise Module", + hotkey: "?", + }, + { + action_name: "Export Transitionary Exercise Module", + hotkey: "?", + }, + { + action_name: "Export Grading Exercise Module", + hotkey: "?", + }, + ]; +}; + +let shortcut_theme = (shortcuts: list(shortcut)): Language.Exp.t => { + open Language; + open IdTagged.FreshGrammar.Exp; + let labeled_elements = + List.map( + ({action_name, hotkey}) => + tup_label(label(action_name), string(hotkey)), + shortcuts, + ); + tuple(labeled_elements); +}; + +let segment = { + open Language; + open Haz3lcore; + let exp = + IdTagged.FreshGrammar.( + Exp.( + let_( + Pat.var("shortcuts"), + shortcut_theme(DefaultConfiguration.shortcuts), + var("shortcuts"), + ) + ) + ); + + ExpToSegment.exp_to_segment( + ~settings= + ExpToSegment.Settings.editable( + ~inline=false, + ~multiline_list_tuples=true, + ), + exp, + ) + |> PersistentSegment.persist; +}; + +let perform_shortcut_side_effect = (value: Language.Exp.t): unit => { + switch (value.term) { + | Tuple(tup_labels) => + // We should consider using projection to extract these + let shortcuts = + List.concat_map( + (x: Language.Exp.t) => { + switch (x.term) { + | TupLabel(label, value) => + switch (label.term, Unboxing.unbox(Atom(String), value)) { + | (Label(action_name), Matches(hotkey)) => [ + (action_name, hotkey), + ] + | _ => [] + } + | _ => [] + } + }, + tup_labels, + ); + List.iter( + ((action_name, hotkey)) => { + // Update the hotkey for this action via NinjaKeys + NinjaKeys.update_shortcut_hotkey( + action_name, + hotkey, + ) + }, + shortcuts, + ); + | _ => () + }; +}; diff --git a/src/web/view/ConfigurationMode.re b/src/web/view/ConfigurationMode.re new file mode 100644 index 0000000000..758716ff97 --- /dev/null +++ b/src/web/view/ConfigurationMode.re @@ -0,0 +1,493 @@ +open Haz3lcore; +open Util; +open Language; + +/* Dedicated ConfigurationMode module for handling different types of configuration + with side effects. Currently supports ColorScheme configuration. + + This has a lot of overlap with ScratchMode as they're both full slide editors but configuration slides can not be added/deleted + and each has a side effect after evaluation */ + +module Model = { + [@deriving (show({with_path: false}), sexp, yojson, enumerate)] + type config_type = + | ColorScheme + | Shortcuts; + + [@deriving (show({with_path: false}), sexp, yojson)] + type t = { + current: int, + configs: list((config_type, CellEditor.Model.t)), + }; + + [@deriving (show({with_path: false}), sexp, yojson)] + type persistent = ( + int, + list((string, option(CellEditor.Model.persistent))), + ); + + let get_current_config = (model: t): (config_type, CellEditor.Model.t) => { + List.nth(model.configs, model.current); + }; + + let get_current_config_type = (model: t): config_type => { + get_current_config(model) |> fst; + }; + + let config_name_of_type = (config_type: config_type): string => { + switch (config_type) { + | ColorScheme => "Colors" + | Shortcuts => "Shortcuts" + }; + }; + + let type_of_config_name = (name: string): option(config_type) => { + List.find_opt( + config_type => config_name_of_type(config_type) == name, + all_of_config_type, + ); + }; + + let default_persisted_segment = config_type => { + switch (config_type) { + | ColorScheme => ("Colors", ColorConfiguration.segment) + | Shortcuts => ("Shortcuts", ShortcutConfiguration.segment) + }; + }; + + let perform_side_effect = + (config_type: config_type, value: Language.Exp.t): unit => { + switch (config_type) { + | ColorScheme => + switch (value.term) { + | ListLit(lits) => + let colors = + List.concat_map( + x => { + switch (Unboxing.unbox(Tuple(2), x)) { + | Matches([x, y]) => + switch ( + Unboxing.unbox(Atom(String), x), + Unboxing.unbox(Atom(String), y), + ) { + | (Matches(name), Matches(color)) => [(name, color)] + | _ => [] + } + | _ => [] + } + }, + lits, + ); + List.iter( + ((var, color)) => JsUtil.set_css_variable("--" ++ var, color), + colors, + ); + | _ => () + } + | Shortcuts => ShortcutConfiguration.perform_shortcut_side_effect(value) + }; + }; + + let persist = (model: t): persistent => ( + model.current, + List.map( + ((s: config_type, m: CellEditor.Model.t)) => { + let s = config_name_of_type(s); + let current_segment = Zipper.zip(m.editor.editor.state.zipper); + let original = Init.find_documentation_slide(s); + let original_segment = + original + |> Option.map((pce: CellEditor.Model.persistent) => + PersistentZipper.unpersist(pce.editor) + ) + |> Option.map(Zipper.zip); + + if (Option.equal( + Base.equal_segment, + original_segment, + Some(current_segment), + )) { + (s, None); + } else { + (s, Some(CellEditor.Model.persist(m))); + }; + }, + model.configs, + ), + ); + + let unpersist = (~settings, (current, slides): persistent): t => { + let get_persistent = + ((s: string, m: option(CellEditor.Model.persistent))) => { + let config_type = + switch (type_of_config_name(s)) { + | Some(ct) => ct + | None => + // Fallback to first config type if name is not recognized + List.hd(all_of_config_type) + }; + ( + config_type, + OptUtil.get( + () => + default_persisted_segment(config_type) + |> snd + |> CellEditor.Model.from_persistent_segment, + m, + ) + |> CellEditor.Model.unpersist(~settings), + ); + }; + { + current: + List.find_index( + config_type => + config_name_of_type(config_type) + == (List.nth(slides, current) |> fst), + all_of_config_type, + ) + |> Option.value(~default=0), + configs: + List.map( + (config_type: config_type) => + List.find_map( + s => + s |> fst == config_name_of_type(config_type) + ? Some(get_persistent(s)) : None, + slides, + ) + |> OptUtil.get(() => { + let (_, seg) = default_persisted_segment(config_type); + + ( + config_type, + CellEditor.Model.mk( + Editor.Model.mk( + Zipper.unzip( + ~direction=Left, + PersistentSegment.unpersist(seg), + ), + ), + ), + ); + }), + all_of_config_type, + ), + }; + }; +}; + +module StoreConfig = + Store.F({ + [@deriving (show({with_path: false}), sexp, yojson)] + type t = Model.persistent; + let key = Store.Configuration; + let default = () => ( + 0, + List.map( + x => + Model.default_persisted_segment(x) + |> PairUtil.map_snd(CellEditor.Model.from_persistent_segment) + |> PairUtil.map_snd(Option.some), + Model.all_of_config_type, + ), + ); + }); + +module Update = { + open Updated; + + [@deriving (show({with_path: false}), sexp, yojson)] + type t = + | CellAction(CellEditor.Update.t) + | SwitchConfig(int) + | ResetCurrent; + + let update = + ( + ~schedule_action as _, + ~send_assistant_insertion_info: CodeEditable.Model.t => unit, + ~settings: Settings.t, + action: t, + model: Model.t, + ) + : Updated.t(Model.t) => { + switch (action) { + | CellAction(a) => + switch (a) { + | CellEditor.Update.ResultAction(UpdateResult(ResultOk({result, _}))) => + let (config_type, _) = Model.get_current_config(model); + Model.perform_side_effect(config_type, result); + // Continue with normal cell update + | _ => () + }; + + let (_, ed) = Model.get_current_config(model); + let* new_ed = CellEditor.Update.update(~settings, a, ed); + let new_configs = + ListUtil.put_nth( + model.current, + (Model.get_current_config_type(model), new_ed), + model.configs, + ); + let new_model = { + ...model, + configs: new_configs, + }; + switch (a) { + // Check for assistant hole completion triggers + | MainEditor(Perform(Insert(_))) => + send_assistant_insertion_info(new_ed.editor) + | _ => () + }; + new_model; + | SwitchConfig(i) => + Updated.return({ + ...model, + current: i, + }) + | ResetCurrent => + let (config_type, _) = Model.get_current_config(model); + let (_, source) = Model.default_persisted_segment(config_type); + Updated.return({ + ...model, + configs: + ListUtil.put_nth( + model.current, + ( + config_type, + source + |> CellEditor.Model.from_persistent_segment + |> CellEditor.Model.unpersist(~settings), + ), + model.configs, + ), + }); + }; + }; + let can_undo = (action: t) => { + switch (action) { + | CellAction(action) => CellEditor.Update.can_undo(action) + | SwitchConfig(_) => false + | ResetCurrent => true + }; + }; + let calculate = + (~settings, ~schedule_action, ~is_edited, model: Model.t): Model.t => { + let (key, ed) = List.nth(model.configs, model.current); + let worker_request = ref([]); + let queue_worker = + Some(expr => {worker_request := worker_request^ @ [("", expr)]}); + let new_ed = + CellEditor.Update.calculate( + ~settings, + ~is_edited, + ~queue_worker, + ~stitch=x => x, + ed, + ); + switch (worker_request^) { + | [] => () + | _ => + WorkerClient.request( + worker_request^, + ~handler= + r => + schedule_action( + CellAction( + ResultAction( + UpdateResult( + switch (r |> List.hd |> snd) { + | Ok((r, s)) => + Language.ProgramResult.ResultOk({ + result: r, + state: s, + }) + | Error(e) => Language.ProgramResult.ResultFail(e) + }, + ), + ), + ), + ), + ~timeout= + _ => + schedule_action( + CellAction(ResultAction(UpdateResult(ResultFail(Timeout)))), + ), + ) + }; + let new_sp = + ListUtil.put_nth(model.current, (key, new_ed), model.configs); + { + ...model, + configs: new_sp, + }; + }; +}; +module Selection = { + open Cursor; + + [@deriving (show({with_path: false}), sexp, yojson)] + type t = + | Cell(CellEditor.Selection.t) + | TextBox; + + let get_cursor_info = (~selection, model: Model.t): cursor(Update.t) => { + switch (selection) { + | Cell(selection) => + let+ a = + CellEditor.Selection.get_cursor_info( + ~selection, + List.nth(model.configs, model.current) |> snd, + ); + Update.CellAction(a); + | TextBox => empty + }; + }; + + let handle_key_event = + (~selection, ~event: Key.t, model: Model.t): option(Update.t) => + switch (selection) { + | Cell(selection) => + switch (event) { + | _ => + CellEditor.Selection.handle_key_event( + ~selection, + ~event, + List.nth(model.configs, model.current) |> snd, + ) + |> Option.map(x => Update.CellAction(x)) + } + | TextBox => None + }; + + let jump_to_tile = (tile, model: Model.t): option((Update.t, t)) => + CellEditor.Selection.jump_to_tile( + tile, + List.nth(model.configs, model.current) |> snd, + ) + |> Option.map(((x, y)) => (Update.CellAction(x), Cell(y))); +}; +module View = { + type event = + | MakeActive(Selection.t); + let view = + ( + ~globals, + ~signal: event => 'a, + ~inject: Update.t => 'a, + ~selected: option(Selection.t), + model: Model.t, + ) => { + [ + CellEditor.View.view( + ~globals, + ~signal= + fun + | MakeActive(selection) => signal(MakeActive(Cell(selection))), + ~inject=a => inject(CellAction(a)), + ~selected= + switch (selected) { + | Some(Selection.Cell(s)) => Some(s) + | _ => None + }, + ~locked=false, + List.nth(model.configs, model.current) |> snd, + ), + ]; + }; + + let top_bar = (~inject: Update.t => 'a, model: Model.t) => { + EditorModeView.view( + ~edit_buttons=false, + ~nav_buttons=false, + ~signal= + fun + | Previous => + inject( + SwitchConfig( + (model.current + List.length(model.configs) - 1) + mod List.length(model.configs), + ), + ) + | Next => + inject( + SwitchConfig((model.current + 1) mod List.length(model.configs)), + ) + | Add => Virtual_dom.Vdom.Effect.Ignore + | Rename => Virtual_dom.Vdom.Effect.Ignore + | Delete => Virtual_dom.Vdom.Effect.Ignore, + ~indicator= + EditorModeView.indicator_select( + ~signal=i => inject(SwitchConfig(i)), + model.current, + List.map( + ((config_type, _)) => Model.config_name_of_type(config_type), + model.configs, + ), + ), + ); + }; + + let file_menu = (~globals: Globals.t, ~inject: Update.t => 'a, _: Model.t) => { + let export_button_for_init = + Widgets.button_named( + Icons.export, + _ => globals.inject_global(ExportForInit), + ~tooltip="Export for Init", + ); + + let file_group_scratch = + NutMenu.item_group(~inject, "File", [export_button_for_init]); + + let reset_button = + Widgets.button_named( + Icons.trash, + _ => { + let confirmed = + JsUtil.confirm( + "Are you SURE you want to reset this configuration? You will lose any existing settings.", + ); + if (confirmed) { + inject(ResetCurrent); + } else { + Virtual_dom.Vdom.Effect.Ignore; + }; + }, + ~tooltip="Reset Editor", + ); + + let reparse = + Widgets.button_named( + Icons.backpack, + _ => globals.inject_global(ActiveEditor(Reparse)), + ~tooltip="Reparse Editor", + ); + + let reset_hazel = + Widgets.button_named( + Icons.bomb, + _ => { + let confirmed = + JsUtil.confirm( + "Are you SURE you want to reset Hazel to its initial state? You will lose any existing code that you have written, and course staff have no way to restore it!", + ); + if (confirmed) { + JsUtil.clear_localstore(); + Js_of_ocaml.Dom_html.window##.location##reload; + }; + Virtual_dom.Vdom.Effect.Ignore; + }, + ~tooltip="Reset Hazel (LOSE ALL DATA)", + ); + + let reset_group_scratch = + NutMenu.item_group( + ~inject, + "Reset", + [reset_button, reparse, reset_hazel], + ); + + [file_group_scratch, reset_group_scratch]; + }; +}; diff --git a/src/web/view/ContextInspector.re b/src/web/view/ContextInspector.re index 91700dde42..7c55daf924 100644 --- a/src/web/view/ContextInspector.re +++ b/src/web/view/ContextInspector.re @@ -18,6 +18,7 @@ let context_entry_view = (~globals, entry: Language.Ctx.entry): Node.t => { hide_fixpoints: false, show_filters: false, show_unknown_as_hole: true, + multiline_list_tuples: false, }, ); let div_name = div(~attrs=[clss(["name"])]); diff --git a/src/web/view/Kind.re b/src/web/view/Kind.re index 61c9d8ead6..629af410ec 100644 --- a/src/web/view/Kind.re +++ b/src/web/view/Kind.re @@ -17,6 +17,7 @@ let view = (~globals, kind: Language.Ctx.kind): Node.t => hide_fixpoints: false, show_filters: false, show_unknown_as_hole: true, + multiline_list_tuples: false, }, ty, ), diff --git a/src/web/view/ScratchMode.re b/src/web/view/ScratchMode.re index 80aeb58f83..70008a5de3 100644 --- a/src/web/view/ScratchMode.re +++ b/src/web/view/ScratchMode.re @@ -228,7 +228,7 @@ module Update = { ~send_assistant_insertion_info: CodeEditable.Model.t => unit, ~settings: Settings.t, ~is_documentation: bool, - action, + action: t, model: Model.t, ) => { switch (action) { diff --git a/src/web/www/style/cell.css b/src/web/www/style/cell.css index 4481de1e5c..67648af366 100644 --- a/src/web/www/style/cell.css +++ b/src/web/www/style/cell.css @@ -18,7 +18,8 @@ } #main.Scratch, -#main.Documentation { +#main.Documentation, +#main.Configuration { background-color: var(--cell-active); } diff --git a/test/Test_ExpToSegment.re b/test/Test_ExpToSegment.re index fb364fc2de..376da6e8b0 100644 --- a/test/Test_ExpToSegment.re +++ b/test/Test_ExpToSegment.re @@ -11,6 +11,7 @@ let exp_to_segment_settings: ExpToSegment.Settings.t = { hide_fixpoints: false, show_filters: true, show_unknown_as_hole: true, + multiline_list_tuples: false, }; let exp_to_segment = diff --git a/test/Test_Menhir.re b/test/Test_Menhir.re index 71ce8f035a..f0c8187730 100644 --- a/test/Test_Menhir.re +++ b/test/Test_Menhir.re @@ -106,7 +106,11 @@ let qcheck_menhir_maketerm_equivalent_test = core_exp => { let segment = Haz3lcore.ExpToSegment.( - exp_to_segment(~settings=Settings.editable(~inline=true), core_exp) + exp_to_segment( + ~settings= + Settings.editable(~inline=true, ~multiline_list_tuples=false), + core_exp, + ) ); let serialized = Haz3lcore.Printer.of_segment(~holes="?", segment); @@ -170,6 +174,7 @@ let qcheck_menhir_serialized_equivalent_test = hide_fixpoints: false, show_filters: true, show_unknown_as_hole: true, + multiline_list_tuples: false, }, core_exp, );