diff --git a/src/haz3lweb/Keyboard.re b/src/haz3lweb/Keyboard.re index fd9ebde091..f0501a66a4 100644 --- a/src/haz3lweb/Keyboard.re +++ b/src/haz3lweb/Keyboard.re @@ -4,6 +4,229 @@ open Util; let is_digit = s => StringUtil.(match(regexp("^[0-9]$"), s)); let is_f_key = s => StringUtil.(match(regexp("^F[0-9][0-9]*$"), s)); +type shortcut = { + update_action: option(UpdateAction.t), + hotkey: option(string), + label: string, + mdIcon: option(string), + section: option(string), +}; + +let meta = (sys: Key.sys): string => { + switch (sys) { + | Mac => "cmd" + | PC => "ctrl" + }; +}; + +let mk_shortcut = + (~hotkey=?, ~mdIcon=?, ~section=?, label, update_action): shortcut => { + {update_action: Some(update_action), hotkey, label, mdIcon, section}; +}; + +let instructor_shortcuts: list(shortcut) = [ + mk_shortcut( + ~mdIcon="download", + ~section="Export", + "Export All Persistent Data", + Export(ExportPersistentData), + ), + mk_shortcut( + ~mdIcon="download", + ~section="Export", + "Export Exercise Module", + Export(ExerciseModule) // TODO Would we rather skip contextual stuff for now or include it and have it fail + ), + mk_shortcut( + ~mdIcon="download", + ~section="Export", + "Export Transitionary Exercise Module", + Export(TransitionaryExerciseModule) // TODO Would we rather skip contextual stuff for now or include it and have it fail + ), + mk_shortcut( + ~mdIcon="download", + ~section="Export", + "Export Grading Exercise Module", + Export(GradingExerciseModule) // TODO Would we rather skip contextual stuff for now or include it and have it fail + ), +]; + +// List of shortcuts configured to show up in the command palette and have hotkey support +let shortcuts = (sys: Key.sys): list(shortcut) => + [ + mk_shortcut(~mdIcon="undo", ~hotkey=meta(sys) ++ "+z", "Undo", Undo), + mk_shortcut( + ~hotkey=meta(sys) ++ "+shift+z", + ~mdIcon="redo", + "Redo", + Redo, + ), + mk_shortcut( + ~hotkey="F12", + ~mdIcon="arrow_forward", + ~section="Navigation", + "Go to Definition", + PerformAction(Jump(BindingSiteOfIndicatedVar)), + ), + mk_shortcut( + ~hotkey="shift+tab", + ~mdIcon="swipe_left_alt", + ~section="Navigation", + "Go to Previous Hole", + PerformAction(Move(Goal(Piece(Grout, Left)))), + ), + mk_shortcut( + ~mdIcon="swipe_right_alt", + ~section="Navigation", + "Go To Next Hole", + PerformAction(Move(Goal(Piece(Grout, Right)))), + // Tab is overloaded so not setting it here + ), + mk_shortcut( + ~hotkey=meta(sys) ++ "+d", + ~mdIcon="select_all", + ~section="Selection", + "Select current term", + PerformAction(Select(Term(Current))), + ), + mk_shortcut( + ~hotkey=meta(sys) ++ "+p", + ~mdIcon="backpack", + "Pick up selected term", + PerformAction(Pick_up), + ), + mk_shortcut( + ~mdIcon="select_all", + ~hotkey=meta(sys) ++ "+a", + ~section="Selection", + "Select All", + PerformAction(Select(All)), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Statics", + UpdateAction.Set(Statics), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Completion", + UpdateAction.Set(Assist), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Show Whitespace", + UpdateAction.Set(SecondaryIcons), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Print Benchmarks", + UpdateAction.Set(Benchmark), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Toggle Dynamics", + UpdateAction.Set(Dynamics), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Show Elaboration", + UpdateAction.Set(Elaborate), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Show Function Bodies", + UpdateAction.Set(Evaluation(ShowFnBodies)), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Show Case Clauses", + UpdateAction.Set(Evaluation(ShowCaseClauses)), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Show fixpoints", + UpdateAction.Set(Evaluation(ShowFixpoints)), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Show Casts", + UpdateAction.Set(Evaluation(ShowCasts)), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Show Lookup Steps", + UpdateAction.Set(Evaluation(ShowLookups)), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Show Stepper Filters", + UpdateAction.Set(Evaluation(ShowFilters)), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Show Hidden Steps", + UpdateAction.Set(Evaluation(ShowHiddenSteps)), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Show Docs Sidebar", + UpdateAction.Set(ExplainThis(ToggleShow)), + ), + mk_shortcut( + ~section="Settings", + ~mdIcon="tune", + "Toggle Show Docs Feedback", + UpdateAction.Set(ExplainThis(ToggleShowFeedback)), + ), + mk_shortcut( + ~hotkey=meta(sys) ++ "+/", + ~mdIcon="assistant", + "TyDi Assistant", + PerformAction(Buffer(Set(TyDi))) // I haven't figured out how to trigger this in the editor + ), + mk_shortcut( + ~mdIcon="download", + ~section="Export", + "Export Scratch Slide", + Export(ExportScratchSlide), + ), + mk_shortcut( + ~mdIcon="download", + ~section="Export", + "Export Submission", + Export(Submission) // TODO Would we rather skip contextual stuff for now or include it and have it fail + ), + mk_shortcut( + // ctrl+k conflicts with the command palette + ~section="Diagnostics", + ~mdIcon="refresh", + "Reparse Current Editor", + PerformAction(Reparse), + ), + mk_shortcut( + ~mdIcon="timer", + ~section="Diagnostics", + ~hotkey="F7", + "Run Benchmark", + Benchmark(Start), + ), + ] + @ (if (ExerciseSettings.show_instructor) {instructor_shortcuts} else {[]}); + let handle_key_event = (k: Key.t): option(Update.t) => { let now = (a: Action.t): option(UpdateAction.t) => Some(PerformAction(a)); @@ -20,7 +243,6 @@ let handle_key_event = (k: Key.t): option(Update.t) => { | {key: D(key), sys: _, shift: Down, meta: Up, ctrl: Up, alt: Up} when is_f_key(key) => switch (key) { - | "F7" => Some(Benchmark(Start)) | _ => Some(DebugConsole(key)) } | {key: D(key), sys: _, shift, meta: Up, ctrl: Up, alt: Up} => @@ -52,8 +274,6 @@ let handle_key_event = (k: Key.t): option(Update.t) => { } | {key: D(key), sys: Mac, shift: Down, meta: Down, ctrl: Up, alt: Up} => switch (key) { - | "Z" - | "z" => Some(Redo) | "ArrowLeft" => now(Select(Resize(Extreme(Left(ByToken))))) | "ArrowRight" => now(Select(Resize(Extreme(Right(ByToken))))) | "ArrowUp" => now(Select(Resize(Extreme(Up)))) @@ -62,8 +282,6 @@ let handle_key_event = (k: Key.t): option(Update.t) => { } | {key: D(key), sys: PC, shift: Down, meta: Up, ctrl: Down, alt: Up} => switch (key) { - | "Z" - | "z" => Some(Redo) | "ArrowLeft" => now(Select(Resize(Local(Left(ByToken))))) | "ArrowRight" => now(Select(Resize(Local(Right(ByToken))))) | "ArrowUp" => now(Select(Resize(Local(Up)))) @@ -78,7 +296,6 @@ let handle_key_event = (k: Key.t): option(Update.t) => { | "d" => now(Select(Term(Current))) | "p" => Some(PerformAction(Pick_up)) | "a" => now(Select(All)) - | "k" => Some(PerformAction(Reparse)) | "/" => Some(PerformAction(Buffer(Set(TyDi)))) | "ArrowLeft" => now(Move(Extreme(Left(ByToken)))) | "ArrowRight" => now(Move(Extreme(Right(ByToken)))) @@ -92,7 +309,6 @@ let handle_key_event = (k: Key.t): option(Update.t) => { | "d" => now(Select(Term(Current))) | "p" => Some(PerformAction(Pick_up)) | "a" => now(Select(All)) - | "k" => Some(PerformAction(Reparse)) | "/" => Some(PerformAction(Buffer(Set(TyDi)))) | "ArrowLeft" => now(Move(Local(Left(ByToken)))) | "ArrowRight" => now(Move(Local(Right(ByToken)))) diff --git a/src/haz3lweb/Log.re b/src/haz3lweb/Log.re index 43fb157c7e..4d98345dab 100644 --- a/src/haz3lweb/Log.re +++ b/src/haz3lweb/Log.re @@ -8,7 +8,7 @@ let is_action_logged: UpdateAction.t => bool = | Save | InitImportAll(_) | InitImportScratchpad(_) - | ExportPersistentData + | Export(_) | FinishImportAll(_) | FinishImportScratchpad(_) | Benchmark(_) diff --git a/src/haz3lweb/Main.re b/src/haz3lweb/Main.re index 98e41a529d..53dfa52165 100644 --- a/src/haz3lweb/Main.re +++ b/src/haz3lweb/Main.re @@ -83,6 +83,7 @@ module App = { schedule_action(Haz3lweb.Update.SetMeta(FontMetrics(fm))) ); + NinjaKeys.initialize(NinjaKeys.options(schedule_action)); JsUtil.focus_clipboard_shim(); /* initialize state. */ diff --git a/src/haz3lweb/NinjaKeys.re b/src/haz3lweb/NinjaKeys.re new file mode 100644 index 0000000000..acce5c4ff4 --- /dev/null +++ b/src/haz3lweb/NinjaKeys.re @@ -0,0 +1,57 @@ +open Js_of_ocaml; +open Util; + +/* + Configuration of the command palette using the https://github.com/ssleptsov/ninja-keys web component. + */ + +let from_shortcut = + (schedule_action: UpdateAction.t => unit, shortcut: Keyboard.shortcut) + : { + . + "handler": Js.readonly_prop(unit => unit), + "id": Js.readonly_prop(string), + "mdIcon": Js.readonly_prop(Js.optdef(string)), + "hotkey": Js.readonly_prop(Js.optdef(string)), + "title": Js.readonly_prop(string), + "section": Js.readonly_prop(Js.optdef(string)), + } => { + [%js + { + val id = shortcut.label; + val title = shortcut.label; + val mdIcon = Js.Optdef.option(shortcut.mdIcon); + val hotkey = Js.Optdef.option(shortcut.hotkey); + val section = Js.Optdef.option(shortcut.section); + val handler = + () => { + let foo = shortcut.update_action; + switch (foo) { + | Some(update) => schedule_action(update) + | None => + print_endline("Could not find action for " ++ shortcut.label) + }; + } + }]; +}; + +let options = (schedule_action: UpdateAction.t => unit) => { + Array.of_list( + List.map( + from_shortcut(schedule_action), + Keyboard.shortcuts(Os.is_mac^ ? Mac : PC), + ), + ); +}; + +let elem = () => JsUtil.get_elem_by_id("ninja-keys"); + +let initialize = opts => Js.Unsafe.set(elem(), "data", Js.array(opts)); + +let open_command_palette = (): unit => { + Js.Unsafe.meth_call( + elem(), + "open", + [||] // Can't use ##.open because open is a reserved keyword + ); +}; diff --git a/src/haz3lweb/Update.re b/src/haz3lweb/Update.re index 0ca89b84c4..0d0648e7f4 100644 --- a/src/haz3lweb/Update.re +++ b/src/haz3lweb/Update.re @@ -282,6 +282,7 @@ let switch_exercise_editor = this between users. The former is a TODO, currently difficult due to the more complex architecture of Exercises. */ let export_persistent_data = () => { + // TODO Is this parsing and reserializing? let settings = Store.Settings.load(); let data: PersistentData.t = { documentation: @@ -301,6 +302,52 @@ let export_persistent_data = () => { ); print_endline("INFO: Persistent data exported to Init.ml"); }; +let export_scratch_slide = (editor: Editor.t): unit => { + let json_data = ScratchSlide.export(editor); + JsUtil.download_json("hazel-scratchpad", json_data); +}; + +let export_exercise_module = (exercise: Exercise.state): unit => { + let module_name = exercise.eds.module_name; + let filename = exercise.eds.module_name ++ ".ml"; + let content_type = "text/plain"; + let contents = Exercise.export_module(module_name, exercise); + JsUtil.download_string_file(~filename, ~content_type, ~contents); +}; + +let export_submission = (~instructor_mode) => + Log.get_and(log => { + let data = Export.export_all(~instructor_mode, ~log); + JsUtil.download_json(ExerciseSettings.filename, data); + }); + +let export_transitionary = (exercise: Exercise.state) => { + // .ml files because show uses OCaml syntax (dune handles seamlessly) + let module_name = exercise.eds.module_name; + let filename = exercise.eds.module_name ++ ".ml"; + let content_type = "text/plain"; + let contents = Exercise.export_transitionary_module(module_name, exercise); + JsUtil.download_string_file(~filename, ~content_type, ~contents); +}; + +let export_instructor_grading_report = (exercise: Exercise.state) => { + // .ml files because show uses OCaml syntax (dune handles seamlessly) + let module_name = exercise.eds.module_name; + let filename = exercise.eds.module_name ++ "_grading.ml"; + let content_type = "text/plain"; + let contents = Exercise.export_grading_module(module_name, exercise); + JsUtil.download_string_file(~filename, ~content_type, ~contents); +}; + +let instructor_exercise_update = + (model: Model.t, fn: Exercise.state => unit): Result.t(Model.t) => { + switch (model.editors) { + | Exercises(_, _, exercise) when model.settings.instructor_mode => + fn(exercise); + Ok(model); + | _ => Error(InstructorOnly) // TODO Make command palette contextual and figure out how to represent that here + }; +}; let ui_state_update = (ui_state: Model.ui_state, update: set_meta, ~schedule_action as _) @@ -371,9 +418,23 @@ let apply = data, ); Model.save_and_return({...model, editors}); - | ExportPersistentData => + | Export(ExportPersistentData) => export_persistent_data(); Ok(model); + | Export(ExportScratchSlide) => + let editor = Editors.get_editor(model.editors); + export_scratch_slide(editor); + Ok(model); + | Export(ExerciseModule) => + instructor_exercise_update(model, export_exercise_module) + | Export(Submission) => + export_submission(~instructor_mode=model.settings.instructor_mode); + Ok(model); + + | Export(TransitionaryExerciseModule) => + instructor_exercise_update(model, export_transitionary) + | Export(GradingExerciseModule) => + instructor_exercise_update(model, export_instructor_grading_report) | ResetCurrentEditor => let instructor_mode = model.settings.instructor_mode; let editors = diff --git a/src/haz3lweb/UpdateAction.re b/src/haz3lweb/UpdateAction.re index 264d828633..8b72083e7c 100644 --- a/src/haz3lweb/UpdateAction.re +++ b/src/haz3lweb/UpdateAction.re @@ -45,6 +45,15 @@ type benchmark_action = | Start | Finish; +[@deriving (show({with_path: false}), sexp, yojson)] +type export_action = + | ExportScratchSlide + | ExportPersistentData + | ExerciseModule + | Submission + | TransitionaryExerciseModule + | GradingExerciseModule; + [@deriving (show({with_path: false}), sexp, yojson)] type t = /* meta */ @@ -52,7 +61,7 @@ type t = | Set(settings_action) | SetMeta(set_meta) | UpdateExplainThisModel(ExplainThisUpdate.update) - | ExportPersistentData + | Export(export_action) | DebugConsole(string) /* editors */ | ResetCurrentEditor @@ -82,6 +91,7 @@ module Failure = { | CantRedo | FailedToSwitch | FailedToPerform(Action.Failure.t) + | InstructorOnly | Exception(string); }; @@ -128,7 +138,7 @@ let is_edit: t => bool = | TAB => true | UpdateResult(_) | SwitchEditor(_) - | ExportPersistentData + | Export(_) | Save | UpdateExplainThisModel(_) | DebugConsole(_) @@ -172,7 +182,7 @@ let reevaluate_post_update: t => bool = | InitImportAll(_) | InitImportScratchpad(_) | UpdateExplainThisModel(_) - | ExportPersistentData + | Export(_) | UpdateResult(_) | SwitchEditor(_) | DebugConsole(_) @@ -250,6 +260,6 @@ let should_scroll_to_caret = | InitImportAll(_) | InitImportScratchpad(_) | UpdateExplainThisModel(_) - | ExportPersistentData + | Export(_) | DebugConsole(_) | Benchmark(_) => false; diff --git a/src/haz3lweb/view/ExerciseMode.re b/src/haz3lweb/view/ExerciseMode.re index 03623acfed..2beb2bf66f 100644 --- a/src/haz3lweb/view/ExerciseMode.re +++ b/src/haz3lweb/view/ExerciseMode.re @@ -275,65 +275,32 @@ let reset_button = inject => ~tooltip="Reset Exercise", ); -let instructor_export = (exercise: Exercise.state) => +let instructor_export = (inject: UpdateAction.t => Ui_effect.t(unit)) => Widgets.button_named( Icons.star, - _ => { - // .ml files because show uses OCaml syntax (dune handles seamlessly) - let module_name = exercise.eds.module_name; - let filename = exercise.eds.module_name ++ ".ml"; - let content_type = "text/plain"; - let contents = Exercise.export_module(module_name, exercise); - JsUtil.download_string_file(~filename, ~content_type, ~contents); - Virtual_dom.Vdom.Effect.Ignore; - }, + _ => inject(Export(ExerciseModule)), ~tooltip="Export Exercise Module", ); -let instructor_transitionary_export = (exercise: Exercise.state) => +let instructor_transitionary_export = + (inject: UpdateAction.t => Ui_effect.t(unit)) => Widgets.button_named( Icons.star, - _ => { - // .ml files because show uses OCaml syntax (dune handles seamlessly) - let module_name = exercise.eds.module_name; - let filename = exercise.eds.module_name ++ ".ml"; - let content_type = "text/plain"; - let contents = - Exercise.export_transitionary_module(module_name, exercise); - JsUtil.download_string_file(~filename, ~content_type, ~contents); - Virtual_dom.Vdom.Effect.Ignore; - }, + _ => {inject(Export(TransitionaryExerciseModule))}, ~tooltip="Export Transitionary Exercise Module", ); -let instructor_grading_export = (exercise: Exercise.state) => +let instructor_grading_export = (inject: UpdateAction.t => Ui_effect.t(unit)) => Widgets.button_named( Icons.star, - _ => { - // .ml files because show uses OCaml syntax (dune handles seamlessly) - let module_name = exercise.eds.module_name; - let filename = exercise.eds.module_name ++ "_grading.ml"; - let content_type = "text/plain"; - let contents = Exercise.export_grading_module(module_name, exercise); - JsUtil.download_string_file(~filename, ~content_type, ~contents); - Virtual_dom.Vdom.Effect.Ignore; - }, + _ => {inject(Export(GradingExerciseModule))}, ~tooltip="Export Grading Exercise Module", ); -let download_editor_state = (~instructor_mode) => - Log.get_and(log => { - let data = Export.export_all(~instructor_mode, ~log); - JsUtil.download_json(ExerciseSettings.filename, data); - }); - -let export_submission = (~settings: Settings.t) => +let export_submission = (inject: UpdateAction.t => Ui_effect.t(unit)) => Widgets.button_named( Icons.star, - _ => { - download_editor_state(~instructor_mode=settings.instructor_mode); - Virtual_dom.Vdom.Effect.Ignore; - }, + _ => inject(Export(Submission)), ~tooltip="Export Submission", ); diff --git a/src/haz3lweb/view/Icons.re b/src/haz3lweb/view/Icons.re index 964480c52c..3cca437c2f 100644 --- a/src/haz3lweb/view/Icons.re +++ b/src/haz3lweb/view/Icons.re @@ -205,3 +205,13 @@ let backpack = "m438.25 148.18 41.09-6.3125v-34.773l7.9062-28.441s-37.945 17.387-48.996 34.766c-11.062 17.387-15.816 26.867-15.816 34.766 0 7.9062 15.816-0.003907 15.816-0.003907z", ], ); + +let command_palette_sparkle = + simple_icon( + ~view="400 400 400 400", + [ + "m505.08 561.96c-10.16 36.805-29.699 70.34-56.707 97.328-27.008 26.984-60.559 46.5-97.371 56.633 36.82 10.152 70.375 29.688 97.383 56.695 27.008 27.008 46.543 60.562 56.695 97.383 10.145-36.824 29.676-70.387 56.684-97.395 27.012-27.012 60.57-46.543 97.398-56.684-36.816-10.121-70.375-29.633-97.383-56.621-27.012-26.988-46.547-60.531-56.699-97.34z", + "m849 507.24c-46.578-13.02-82.977-49.418-96-96-13.09 46.758-49.766 83.203-96.602 96 46.812 12.844 83.469 49.273 96.602 96 13.043-46.566 49.434-82.957 96-96z", + "m554.76 426.6c6.5195-23.285 24.715-41.48 48-48-23.297-6.5-41.5-24.707-48-48-6.5 23.293-24.707 41.5-48 48 23.281 6.5195 41.477 24.715 48 48z", + ], + ); diff --git a/src/haz3lweb/view/NutMenu.re b/src/haz3lweb/view/NutMenu.re index c3b99df899..bcb7f995e8 100644 --- a/src/haz3lweb/view/NutMenu.re +++ b/src/haz3lweb/view/NutMenu.re @@ -9,7 +9,7 @@ open Haz3lcore; let export_persistent_data = (~inject: Update.t => 'a) => button_named( Icons.sprout, - _ => inject(ExportPersistentData), + _ => inject(Export(ExportPersistentData)), ~tooltip="Export All Persistent Data", ); @@ -117,22 +117,23 @@ let settings_menu = ]; }; -let export_menu = (~inject, ~settings: Settings.t, editors: Editors.t) => +let export_menu = + ( + ~inject: UpdateAction.t => Ui_effect.t(unit), + ~settings: Settings.t, + editors: Editors.t, + ) => switch (editors) { - | Scratch(slide_idx, slides) => - let state = List.nth(slides, slide_idx); - [ScratchMode.export_button(state)]; - | Documentation(name, slides) => - let state = List.assoc(name, slides); - [ScratchMode.export_button(state)]; - | Exercises(_, _, exercise) when settings.instructor_mode => [ + | Scratch(_) => [ScratchMode.export_button(inject)] + | Documentation(_) => [ScratchMode.export_button(inject)] + | Exercises(_) when settings.instructor_mode => [ export_persistent_data(~inject), - ExerciseMode.export_submission(~settings), - ExerciseMode.instructor_export(exercise), - ExerciseMode.instructor_transitionary_export(exercise), - ExerciseMode.instructor_grading_export(exercise), + ExerciseMode.export_submission(inject), + ExerciseMode.instructor_export(inject), + ExerciseMode.instructor_transitionary_export(inject), + ExerciseMode.instructor_grading_export(inject), ] - | Exercises(_) => [ExerciseMode.export_submission(~settings)] + | Exercises(_) => [ExerciseMode.export_submission(inject)] }; let import_menu = (~inject, editors: Editors.t) => @@ -166,6 +167,17 @@ let view = div( ~attrs=[clss(["nut-menu"])], [ + button( + Icons.command_palette_sparkle, + _ => { + NinjaKeys.open_command_palette(); + Effect.Ignore; + }, + ~tooltip= + "Command Palette (" + ++ Keyboard.meta(JsUtil.is_mac() ? Mac : PC) + ++ " + k)", + ), submenu( ~tooltip="Settings", ~icon=Icons.gear, diff --git a/src/haz3lweb/view/ScratchMode.re b/src/haz3lweb/view/ScratchMode.re index 4967f13b6d..0e3874f534 100644 --- a/src/haz3lweb/view/ScratchMode.re +++ b/src/haz3lweb/view/ScratchMode.re @@ -44,14 +44,10 @@ let view = ]; }; -let export_button = state => +let export_button = (inject: Update.t => Ui_effect.t(unit)) => Widgets.button_named( Icons.star, - _ => { - let json_data = ScratchSlide.export(state); - JsUtil.download_json("hazel-scratchpad", json_data); - Virtual_dom.Vdom.Effect.Ignore; - }, + _ => inject(Export(ExportScratchSlide)), ~tooltip="Export Scratchpad", ); let import_button = inject => diff --git a/src/haz3lweb/www/index.html b/src/haz3lweb/www/index.html index cd9cc8aa2a..7332b02a18 100644 --- a/src/haz3lweb/www/index.html +++ b/src/haz3lweb/www/index.html @@ -9,6 +9,28 @@