diff --git a/src/haz3lschool/Exercise.re b/src/haz3lschool/Exercise.re index b5e7dfc76d..e96bebdec6 100644 --- a/src/haz3lschool/Exercise.re +++ b/src/haz3lschool/Exercise.re @@ -53,8 +53,8 @@ module F = (ExerciseEnv: ExerciseEnv) => { [@deriving (show({with_path: false}), sexp, yojson)] type p('code) = { + id: Id.t, title: string, - version: int, module_name: string, prompt: [@printer (fmt, _) => Format.pp_print_string(fmt, "prompt")] [@opaque] ExerciseEnv.node, @@ -68,15 +68,14 @@ module F = (ExerciseEnv: ExerciseEnv) => { syntax_tests, }; - [@deriving (show({with_path: false}), sexp, yojson)] - type key = (string, int); + type record = p(Zipper.t); - let key_of = p => { - (p.title, p.version); + let id_of = p => { + p.id; }; - let find_key_opt = (key, specs: list(p('code))) => { - specs |> Util.ListUtil.findi_opt(spec => key_of(spec) == key); + let find_id_opt = (id, specs: list(p('code))) => { + specs |> Util.ListUtil.findi_opt(spec => id_of(spec) == id); }; [@deriving (show({with_path: false}), sexp, yojson)] @@ -97,8 +96,8 @@ module F = (ExerciseEnv: ExerciseEnv) => { let map = (p: p('a), f: 'a => 'b): p('b) => { { + id: p.id, title: p.title, - version: p.version, module_name: p.module_name, prompt: p.prompt, point_distribution: p.point_distribution, @@ -135,10 +134,29 @@ module F = (ExerciseEnv: ExerciseEnv) => { eds, }; - let key_of_state = ({eds, _}) => key_of(eds); - [@deriving (show({with_path: false}), sexp, yojson)] - type persistent_state = (pos, list((pos, PersistentZipper.t))); + type persistent_state = { + focus: pos, + editors: list((pos, PersistentZipper.t)), + title: string, + hidden_bugs: list(wrong_impl(PersistentZipper.t)), + // NOTE: Add new fields to record here as new instructor editable features are + // implemented (eg. prelude: PersistentZipper.t when adding the feature + // to edit the prelude). After adding these field(s), we will need to + // go into persistent_state_of_state and unpersist_state to implement + // how these fields are saved and loaded to and from local memory + // respectively. + // NOTE: It may be helpful to look at changes made in the mutant-add-delete and title-editor + // branches in the Hazel repo to see and understand where changes + // were made. It is likely that new implementations of editble features + // will follow a similar route. + }; + + let clamp_idx = (eds: eds, idx: int) => { + let length = List.length(eds.hidden_bugs); + let idx = idx > length - 1 ? idx - 1 : idx; + idx >= 0 ? Some(idx) : None; + }; let editor_of_state: state => Editor.t = ({pos, eds, _}) => @@ -148,7 +166,11 @@ module F = (ExerciseEnv: ExerciseEnv) => { | YourTestsValidation => eds.your_tests.tests | YourTestsTesting => eds.your_tests.tests | YourImpl => eds.your_impl - | HiddenBugs(i) => List.nth(eds.hidden_bugs, i).impl + | HiddenBugs(i) => + switch (clamp_idx(eds, i)) { + | Some(idx) => List.nth(eds.hidden_bugs, idx).impl + | None => eds.your_impl + } | HiddenTests => eds.hidden_tests.tests }; @@ -285,8 +307,8 @@ module F = (ExerciseEnv: ExerciseEnv) => { let transition: transitionary_spec => spec = ( { + id, title, - version, module_name, prompt, point_distribution, @@ -321,8 +343,8 @@ module F = (ExerciseEnv: ExerciseEnv) => { {tests, hints}; }; { + id, title, - version, module_name, prompt, point_distribution, @@ -340,7 +362,7 @@ module F = (ExerciseEnv: ExerciseEnv) => { ( { title, - version, + id, module_name, prompt, point_distribution, @@ -376,7 +398,7 @@ module F = (ExerciseEnv: ExerciseEnv) => { }; { title, - version, + id, module_name, prompt, point_distribution, @@ -467,6 +489,84 @@ module F = (ExerciseEnv: ExerciseEnv) => { }, }; + let set_editing_title = ({eds, _} as state: state, editing: bool) => { + ...state, + eds: { + ...eds, + prelude: Editor.set_read_only(eds.prelude, editing), + correct_impl: Editor.set_read_only(eds.correct_impl, editing), + your_tests: { + let tests = Editor.set_read_only(eds.your_tests.tests, editing); + { + tests, + required: eds.your_tests.required, + provided: eds.your_tests.provided, + }; + }, + hidden_bugs: + eds.hidden_bugs + |> List.map(({impl, hint}) => { + let impl = Editor.set_read_only(impl, editing); + {impl, hint}; + }), + hidden_tests: { + let tests = Editor.set_read_only(eds.hidden_tests.tests, editing); + {tests, hints: eds.hidden_tests.hints}; + }, + your_impl: Editor.set_read_only(eds.your_impl, editing), + }, + }; + + let update_exercise_title = ({eds, _} as state: state, new_title: string) => { + ...state, + eds: { + ...eds, + title: new_title, + }, + }; + + let add_buggy_impl = + (~settings: CoreSettings.t, state: state, ~editing_title) => { + let new_buggy_impl = { + impl: Editor.init(Zipper.init(), ~settings), + hint: "no hint available", + }; + let new_state = { + pos: HiddenBugs(List.length(state.eds.hidden_bugs)), + eds: { + ...state.eds, + hidden_bugs: state.eds.hidden_bugs @ [new_buggy_impl], + }, + }; + let new_state = set_editing_title(new_state, editing_title); + put_editor(new_state, new_buggy_impl.impl); + }; + + let delete_buggy_impl = (state: state, index: int) => { + let length = List.length(state.eds.hidden_bugs); + let editor_on = + length > 1 + ? List.nth( + state.eds.hidden_bugs, + index < length - 1 ? index + 1 : index - 1, + ). + impl + : state.eds.your_tests.tests; + let pos = + length > 1 + ? HiddenBugs(index < length - 1 ? index : index - 1) + : YourTestsValidation; + let new_state = { + pos, + eds: { + ...state.eds, + hidden_bugs: + List.filteri((i, _) => i != index, state.eds.hidden_bugs), + }, + }; + put_editor(new_state, editor_on); + }; + let visible_in = (pos, ~instructor_mode) => { switch (pos) { | Prelude => instructor_mode @@ -485,28 +585,38 @@ module F = (ExerciseEnv: ExerciseEnv) => { set_instructor_mode({pos: YourImpl, eds}, instructor_mode); }; - let persistent_state_of_state = - ({pos, _} as state: state, ~instructor_mode: bool) => { + let persistent_state_of_state = (state: state, ~instructor_mode: bool) => { let zippers = positioned_editors(state) |> List.filter(((pos, _)) => visible_in(pos, ~instructor_mode)) |> List.map(((pos, editor)) => { (pos, PersistentZipper.persist(Editor.(editor.state.zipper))) }); - (pos, zippers); + let persistent_hidden_bugs = + state.eds.hidden_bugs + |> List.map(({impl, hint}) => { + {impl: PersistentZipper.persist(Editor.(impl.state.zipper)), hint} + }); + { + focus: state.pos, + editors: zippers, + title: state.eds.title, + hidden_bugs: persistent_hidden_bugs, + }; }; let unpersist_state = ( - (pos, positioned_zippers): persistent_state, + {focus, editors, title, hidden_bugs}: persistent_state, ~spec: spec, ~instructor_mode: bool, + ~editing_title: bool, ~settings: CoreSettings.t, ) : state => { let lookup = (pos, default) => if (visible_in(pos, ~instructor_mode)) { - let persisted_zipper = List.assoc(pos, positioned_zippers); + let persisted_zipper = List.assoc(pos, editors); let zipper = PersistentZipper.unpersist(persisted_zipper); Editor.init(zipper, ~settings); } else { @@ -516,44 +626,43 @@ module F = (ExerciseEnv: ExerciseEnv) => { let correct_impl = lookup(CorrectImpl, spec.correct_impl); let your_tests_tests = lookup(YourTestsValidation, spec.your_tests.tests); let your_impl = lookup(YourImpl, spec.your_impl); - let (_, hidden_bugs) = - List.fold_left( - ((i, hidden_bugs: list(wrong_impl(Editor.t))), {impl, hint}) => { - let impl = lookup(HiddenBugs(i), impl); - (i + 1, hidden_bugs @ [{impl, hint}]); - }, - (0, []), - spec.hidden_bugs, - ); + let hidden_bugs = + hidden_bugs + |> List.map(({impl, hint}) => { + let impl = + Editor.init(PersistentZipper.unpersist(impl), ~settings); + {impl, hint}; + }); let hidden_tests_tests = lookup(HiddenTests, spec.hidden_tests.tests); - - set_instructor_mode( - { - pos, - eds: { - title: spec.title, - version: spec.version, - module_name: spec.module_name, - prompt: spec.prompt, - point_distribution: spec.point_distribution, - prelude, - correct_impl, - your_tests: { - tests: your_tests_tests, - required: spec.your_tests.required, - provided: spec.your_tests.provided, - }, - your_impl, - hidden_bugs, - hidden_tests: { - tests: hidden_tests_tests, - hints: spec.hidden_tests.hints, + let state = + set_instructor_mode( + { + pos: focus, + eds: { + id: spec.id, + title, + module_name: spec.module_name, + prompt: spec.prompt, + point_distribution: spec.point_distribution, + prelude, + correct_impl, + your_tests: { + tests: your_tests_tests, + required: spec.your_tests.required, + provided: spec.your_tests.provided, + }, + your_impl, + hidden_bugs, + hidden_tests: { + tests: hidden_tests_tests, + hints: spec.hidden_tests.hints, + }, + syntax_tests: spec.syntax_tests, }, - syntax_tests: spec.syntax_tests, }, - }, - instructor_mode, - ); + instructor_mode, + ); + set_editing_title(state, editing_title); }; // # Stitching @@ -725,7 +834,11 @@ module F = (ExerciseEnv: ExerciseEnv) => { | YourTestsValidation => s.test_validation.statics | YourTestsTesting => s.user_tests.statics | YourImpl => s.user_impl.statics - | HiddenBugs(idx) => List.nth(s.hidden_bugs, idx).statics + | HiddenBugs(idx) => + switch (clamp_idx(state.eds, idx)) { + | Some(idx) => List.nth(s.hidden_bugs, idx).statics + | None => s.user_impl.statics + } | HiddenTests => s.hidden_tests.statics }; @@ -897,8 +1010,8 @@ module F = (ExerciseEnv: ExerciseEnv) => { ); let hidden_tests_tests = Zipper.next_blank(); { + id: Id.mk(), title, - version: 1, module_name, prompt: ExerciseEnv.default, point_distribution, @@ -923,8 +1036,8 @@ module F = (ExerciseEnv: ExerciseEnv) => { [@deriving (show({with_path: false}), sexp, yojson)] type exercise_export = { - cur_exercise: key, - exercise_data: list((key, persistent_state)), + cur_exercise: Id.t, + exercise_data: list((Id.t, persistent_state)), }; let serialize_exercise = (exercise, ~instructor_mode) => { @@ -933,11 +1046,11 @@ module F = (ExerciseEnv: ExerciseEnv) => { |> Sexplib.Sexp.to_string; }; - let deserialize_exercise = (data, ~spec, ~instructor_mode) => { + let deserialize_exercise = (data, ~spec, ~instructor_mode, ~editing_title) => { data |> Sexplib.Sexp.of_string |> persistent_state_of_sexp - |> unpersist_state(~spec, ~instructor_mode); + |> unpersist_state(~spec, ~instructor_mode, ~editing_title); }; let deserialize_exercise_export = data => { diff --git a/src/haz3lschool/Gradescope.re b/src/haz3lschool/Gradescope.re index 7277fcf85b..caab18dbd8 100644 --- a/src/haz3lschool/Gradescope.re +++ b/src/haz3lschool/Gradescope.re @@ -37,7 +37,7 @@ type report = { }; [@deriving (sexp, yojson)] type section = { - name: string, + id: Id.t, report, }; @@ -108,8 +108,8 @@ module Main = { let hw = name_to_exercise_export(hw_path); let export_chapter = hw.exercise_data - |> List.map(~f=(((name, _) as key, persistent_state)) => { - switch (find_key_opt(key, specs)) { + |> List.map(~f=((id, persistent_state)) => { + switch (find_id_opt(id, specs)) { | Some((_n, spec)) => let exercise = unpersist_state( @@ -117,9 +117,10 @@ module Main = { ~settings, ~spec, ~instructor_mode=true, + ~editing_title=false, ); let report = exercise |> gen_grading_report; - {name, report}; + {id, report}; | None => failwith("Invalid spec") // | None => (key |> yojson_of_key |> Yojson.Safe.to_string, "?") } diff --git a/src/haz3lweb/Editors.re b/src/haz3lweb/Editors.re index f8fa4e0b06..5f16723833 100644 --- a/src/haz3lweb/Editors.re +++ b/src/haz3lweb/Editors.re @@ -115,6 +115,44 @@ let set_instructor_mode = (editors: t, instructor_mode: bool): t => ) }; +let set_editing_title = (editors: t, editing: bool): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.set_editing_title(exercise, editing)) + }; + +let update_exercise_title = (editors: t, new_title: string): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.update_exercise_title(exercise, new_title)) + }; + +let add_buggy_impl = (~settings: CoreSettings.t, editors: t, ~editing_title) => { + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises( + n, + specs, + Exercise.add_buggy_impl(~settings, exercise, ~editing_title), + ) + }; +}; + +let delete_buggy_impl = (editors: t, index: int) => { + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.delete_buggy_impl(exercise, index)) + }; +}; + let reset_nth_slide = (~settings: CoreSettings.t, n, slides): list(Editor.t) => { let (_, init_editors, _) = Init.startup.scratch; let data = List.nth(init_editors, n); diff --git a/src/haz3lweb/Export.re b/src/haz3lweb/Export.re index 9c9f709fa9..08ff9fe0f8 100644 --- a/src/haz3lweb/Export.re +++ b/src/haz3lweb/Export.re @@ -55,12 +55,14 @@ let import_all = (data, ~specs) => { let settings = Store.Settings.import(all.settings); Store.ExplainThisModel.import(all.explainThisModel); let instructor_mode = settings.instructor_mode; + let editing_title = settings.editing_title; Store.Scratch.import(~settings=settings.core, all.scratch); Store.Exercise.import( ~settings=settings.core, all.exercise, ~specs, ~instructor_mode, + ~editing_title, ); Log.import(all.log); }; diff --git a/src/haz3lweb/Init.ml b/src/haz3lweb/Init.ml index c3d2de0aba..0deb6d64d2 100644 --- a/src/haz3lweb/Init.ml +++ b/src/haz3lweb/Init.ml @@ -26,6 +26,7 @@ let startup : PersistentData.t = async_evaluation = false; context_inspector = false; instructor_mode = true; + editing_title = false; benchmark = false; explainThis = { show = true; show_feedback = false; highlight = NoHighlight }; diff --git a/src/haz3lweb/Log.re b/src/haz3lweb/Log.re index 7c87a640f6..2752676057 100644 --- a/src/haz3lweb/Log.re +++ b/src/haz3lweb/Log.re @@ -25,6 +25,9 @@ let is_action_logged: UpdateAction.t => bool = | Undo | Redo | UpdateResult(_) + | UpdateTitle(_) + | AddBuggyImplementation + | DeleteBuggyImplementation(_) | ToggleStepper(_) | StepperAction(_, StepForward(_) | StepBackward) | UpdateExplainThisModel(_) => true; diff --git a/src/haz3lweb/Model.re b/src/haz3lweb/Model.re index e4939b3af0..5329853eeb 100644 --- a/src/haz3lweb/Model.re +++ b/src/haz3lweb/Model.re @@ -55,7 +55,12 @@ let mk = (editors, results) => { let blank = mk(Editors.Scratch(0, []), ModelResults.empty); let load_editors = - (~settings, ~mode: Settings.mode, ~instructor_mode: bool) + ( + ~settings, + ~mode: Settings.mode, + ~instructor_mode: bool, + ~editing_title: bool, + ) : (Editors.t, ModelResults.t) => switch (mode) { | Scratch => @@ -70,6 +75,7 @@ let load_editors = ~settings, ~specs=ExerciseSettings.exercises, ~instructor_mode, + ~editing_title, ); (Exercises(n, specs, exercise), ModelResults.empty); }; @@ -93,6 +99,7 @@ let load = (init_model: t): t => { ~settings=settings.core, ~mode=settings.mode, ~instructor_mode=settings.instructor_mode, + ~editing_title=settings.editing_title, ); let ui_state = init_model.ui_state; {editors, settings, results, explainThisModel, ui_state}; diff --git a/src/haz3lweb/Settings.re b/src/haz3lweb/Settings.re index 1481b54621..643d5e2180 100644 --- a/src/haz3lweb/Settings.re +++ b/src/haz3lweb/Settings.re @@ -22,6 +22,7 @@ type t = { async_evaluation: bool, context_inspector: bool, instructor_mode: bool, + editing_title: bool, benchmark: bool, explainThis: ExplainThisModel.Settings.t, mode, diff --git a/src/haz3lweb/Store.re b/src/haz3lweb/Store.re index f30a18ab85..3bb5897563 100644 --- a/src/haz3lweb/Store.re +++ b/src/haz3lweb/Store.re @@ -260,33 +260,19 @@ module Exercise = { let cur_exercise_key = "CUR_EXERCISE"; - let keystring_of_key = key => { - key |> sexp_of_key |> Sexplib.Sexp.to_string; - }; - - let keystring_of = p => { - key_of(p) |> keystring_of_key; - }; - - let key_of_keystring = keystring => { - keystring |> Sexplib.Sexp.of_string |> key_of_sexp; - }; - - let save_exercise_key = key => { - JsUtil.set_localstore(cur_exercise_key, keystring_of_key(key)); + let save_exercise_id = id => { + JsUtil.set_localstore(cur_exercise_key, Id.to_string(id)); }; let save_exercise = (exercise, ~instructor_mode): unit => { - let key = Exercise.key_of_state(exercise); - let keystring = keystring_of_key(key); - let value = Exercise.serialize_exercise(exercise, ~instructor_mode); - JsUtil.set_localstore(keystring, value); + let keystring = Id.to_string(exercise.eds.id); + let data = Exercise.serialize_exercise(exercise, ~instructor_mode); + JsUtil.set_localstore(keystring, data); }; let init_exercise = (~settings: CoreSettings.t, spec, ~instructor_mode): state => { - let key = Exercise.key_of(spec); - let keystring = keystring_of_key(key); + let keystring = Id.to_string(spec.id); let exercise = Exercise.state_of_spec(spec, ~instructor_mode, ~settings); save_exercise(exercise, ~instructor_mode); JsUtil.set_localstore(cur_exercise_key, keystring); @@ -294,8 +280,9 @@ module Exercise = { }; let load_exercise = - (~settings: CoreSettings.t, key, spec, ~instructor_mode): Exercise.state => { - let keystring = keystring_of_key(key); + (~settings: CoreSettings.t, spec, ~instructor_mode, ~editing_title) + : Exercise.state => { + let keystring = Id.to_string(spec.id); switch (JsUtil.get_localstore(keystring)) { | Some(data) => let exercise = @@ -304,6 +291,7 @@ module Exercise = { data, ~spec, ~instructor_mode, + ~editing_title, ~settings, ) ) { @@ -316,8 +304,7 @@ module Exercise = { }; let save = ((n, specs, exercise), ~instructor_mode): unit => { - let key = key_of(List.nth(specs, n)); - let keystring = keystring_of_key(key); + let keystring = Id.to_string(List.nth(specs, n).id); save_exercise(exercise, ~instructor_mode); JsUtil.set_localstore(cur_exercise_key, keystring); }; @@ -338,57 +325,82 @@ module Exercise = { }; let load = - (~settings: CoreSettings.t, ~specs, ~instructor_mode) + (~settings: CoreSettings.t, ~specs, ~instructor_mode, ~editing_title) : (int, list(p(ZipperBase.t)), state) => { switch (JsUtil.get_localstore(cur_exercise_key)) { | Some(keystring) => - let key = key_of_keystring(keystring); - switch (Exercise.find_key_opt(key, specs)) { - | Some((n, spec)) => - switch (JsUtil.get_localstore(keystring)) { - | Some(data) => - let exercise = - try( - deserialize_exercise(data, ~spec, ~instructor_mode, ~settings) - ) { - | _ => init_exercise(spec, ~instructor_mode, ~settings) - }; - (n, specs, exercise); + switch (Id.of_string(keystring)) { + | Some(id) => + switch (Exercise.find_id_opt(id, specs)) { + | Some((n, spec)) => + switch (JsUtil.get_localstore(keystring)) { + | Some(data) => + let exercise = + try( + deserialize_exercise( + data, + ~spec, + ~instructor_mode, + ~editing_title, + ~settings, + ) + ) { + | _ => init_exercise(spec, ~instructor_mode, ~settings) + }; + (n, specs, exercise); + | None => + // initialize exercise from spec + let exercise = + Exercise.state_of_spec(spec, ~instructor_mode, ~settings); + save_exercise(exercise, ~instructor_mode); + (n, specs, exercise); + } | None => - // initialize exercise from spec - let exercise = - Exercise.state_of_spec(spec, ~instructor_mode, ~settings); - save_exercise(exercise, ~instructor_mode); - (n, specs, exercise); + // invalid current exercise key saved, load the first exercise + let first_spec = List.nth(specs, 0); + ( + 0, + specs, + load_exercise( + first_spec, + ~instructor_mode, + ~editing_title, + ~settings, + ), + ); } - | None => - // invalid current exercise key saved, load the first exercise - let first_spec = List.nth(specs, 0); - let first_key = Exercise.key_of(first_spec); - ( - 0, - specs, - load_exercise(first_key, first_spec, ~instructor_mode, ~settings), - ); - }; + | None => failwith("parse error") + } | None => init(~instructor_mode, ~settings) }; }; let prep_exercise_export = - (~specs, ~instructor_mode: bool, ~settings: CoreSettings.t) + ( + ~specs, + ~instructor_mode: bool, + ~settings: CoreSettings.t, + ~editing_title, + ) : exercise_export => { { cur_exercise: - key_of_keystring( - Option.get(JsUtil.get_localstore(cur_exercise_key)), + Id.t_of_sexp( + Sexplib.Sexp.of_string( + Option.get(JsUtil.get_localstore(cur_exercise_key)), + ), ), exercise_data: specs |> List.map(spec => { - let key = Exercise.key_of(spec); + let key = spec.id; let exercise = - load_exercise(key, spec, ~instructor_mode, ~settings) + load_exercise( + spec, + ~instructor_mode, + ~editing_title, + ~settings, + ) |> Exercise.persistent_state_of_state(~instructor_mode); (key, exercise); }), @@ -397,7 +409,12 @@ module Exercise = { let serialize_exercise_export = (~specs, ~instructor_mode, ~settings: CoreSettings.t) => { - prep_exercise_export(~specs, ~instructor_mode, ~settings) + prep_exercise_export( + ~specs, + ~instructor_mode, + ~editing_title=false, + ~settings, + ) |> sexp_of_exercise_export |> Sexplib.Sexp.to_string; }; @@ -407,12 +424,18 @@ module Exercise = { }; let import = - (data, ~specs, ~instructor_mode: bool, ~settings: CoreSettings.t) => { + ( + data, + ~specs, + ~instructor_mode: bool, + ~editing_title: bool, + ~settings: CoreSettings.t, + ) => { let exercise_export = data |> deserialize_exercise_export; - save_exercise_key(exercise_export.cur_exercise); + save_exercise_id(exercise_export.cur_exercise); exercise_export.exercise_data |> List.iter(((key, persistent_state)) => { - let spec = Exercise.find_key_opt(key, specs); + let spec = Exercise.find_id_opt(key, specs); switch (spec) { | None => print_endline("Warning: saved key does not correspond to exercise") @@ -422,6 +445,7 @@ module Exercise = { persistent_state, ~spec, ~instructor_mode, + ~editing_title, ~settings, ), ~instructor_mode, diff --git a/src/haz3lweb/Update.re b/src/haz3lweb/Update.re index 1aacb159f3..65fac663e9 100644 --- a/src/haz3lweb/Update.re +++ b/src/haz3lweb/Update.re @@ -185,12 +185,25 @@ let update_settings = } | InstructorMode => let new_mode = !settings.instructor_mode; + let editors = Editors.set_editing_title(model.editors, false); + let editors = Editors.set_instructor_mode(editors, new_mode); { ...model, - editors: Editors.set_instructor_mode(model.editors, new_mode), + editors, settings: { ...settings, instructor_mode: !settings.instructor_mode, + editing_title: false, + }, + }; + | EditingTitle => + let editing = !settings.editing_title; + { + ...model, + editors: Editors.set_editing_title(model.editors, editing), + settings: { + ...settings, + editing_title: editing, }, }; | Mode(mode) => { @@ -281,7 +294,13 @@ let update_cached_data = (~schedule_action, update, m: Model.t): Model.t => { }; let switch_scratch_slide = - (~settings, editors: Editors.t, ~instructor_mode, idx: int) + ( + ~settings, + editors: Editors.t, + ~instructor_mode, + ~editing_title, + idx: int, + ) : option(Editors.t) => switch (editors) { | Documentation(_) => None @@ -291,9 +310,13 @@ let switch_scratch_slide = | Exercises(_, specs, _) when idx >= List.length(specs) => None | Exercises(_, specs, _) => let spec = List.nth(specs, idx); - let key = Exercise.key_of(spec); let exercise = - Store.Exercise.load_exercise(key, spec, ~instructor_mode, ~settings); + Store.Exercise.load_exercise( + spec, + ~instructor_mode, + ~editing_title, + ~settings, + ); Some(Exercises(idx, specs, exercise)); }; @@ -487,16 +510,19 @@ let apply = (model: Model.t, update: t, ~schedule_action): Result.t(Model.t) => Model.save_and_return({...model, editors}); | SwitchScratchSlide(n) => let instructor_mode = model.settings.instructor_mode; + let editors = Editors.set_editing_title(model.editors, false); + let settings = {...model.settings, editing_title: false}; switch ( switch_scratch_slide( ~settings=model.settings.core, - model.editors, + editors, ~instructor_mode, + ~editing_title=false, n, ) ) { | None => Error(FailedToSwitch) - | Some(editors) => Model.save_and_return({...model, editors}) + | Some(editors) => Model.save_and_return({...model, editors, settings}) }; | SwitchDocumentationSlide(name) => switch (Editors.switch_example_slide(model.editors, name)) { @@ -574,6 +600,25 @@ let apply = (model: Model.t, update: t, ~schedule_action): Result.t(Model.t) => let results = ModelResults.union((_, _a, b) => Some(b), model.results, results); Ok({...model, results}); + | UpdateTitle(new_title) => + Model.save_and_return({ + ...model, + editors: Editors.update_exercise_title(model.editors, new_title), + }) + | AddBuggyImplementation => + Model.save_and_return({ + ...model, + editors: + Editors.add_buggy_impl( + ~settings=model.settings.core, + model.editors, + ~editing_title=model.settings.editing_title, + ), + }) + | DeleteBuggyImplementation(index) => + let editors = Editors.delete_buggy_impl(model.editors, index); + print_endline(Editors.show(editors)); + Model.save_and_return({...model, editors}); }; m |> Result.map(~f=update_cached_data(~schedule_action, update)); }; diff --git a/src/haz3lweb/UpdateAction.re b/src/haz3lweb/UpdateAction.re index cd2f145f3e..05c5ee7153 100644 --- a/src/haz3lweb/UpdateAction.re +++ b/src/haz3lweb/UpdateAction.re @@ -24,6 +24,7 @@ type settings_action = | Benchmark | ContextInspector | InstructorMode + | EditingTitle | Evaluation(evaluation_settings_action) | ExplainThis(ExplainThisModel.Settings.action) | Mode(Settings.mode); @@ -45,6 +46,12 @@ type benchmark_action = | Start | Finish; +// To-do: Use this to update either title or model +[@deriving (show({with_path: false}), sexp, yojson)] +type edit_action = + | Title + | Model; + [@deriving (show({with_path: false}), sexp, yojson)] type export_action = | ExportScratchSlide @@ -83,7 +90,10 @@ type t = | Benchmark(benchmark_action) | ToggleStepper(ModelResults.Key.t) | StepperAction(ModelResults.Key.t, stepper_action) - | UpdateResult(ModelResults.t); + | UpdateResult(ModelResults.t) + | UpdateTitle(string) + | AddBuggyImplementation + | DeleteBuggyImplementation(int); module Failure = { [@deriving (show({with_path: false}), sexp, yojson)] @@ -117,6 +127,7 @@ let is_edit: t => bool = | Benchmark | ContextInspector | InstructorMode + | EditingTitle | Evaluation(_) => false } | SetMeta(meta_action) => @@ -135,6 +146,9 @@ let is_edit: t => bool = | FinishImportAll(_) | FinishImportScratchpad(_) | ResetCurrentEditor + | UpdateTitle(_) + | AddBuggyImplementation + | DeleteBuggyImplementation(_) | Reset | TAB => true | UpdateResult(_) @@ -171,6 +185,7 @@ let reevaluate_post_update: t => bool = | Assist | Dynamics | InstructorMode + | EditingTitle | Mode(_) => true } | SetMeta(meta_action) => @@ -180,6 +195,9 @@ let reevaluate_post_update: t => bool = | ShowBackpackTargets(_) | FontMetrics(_) => false } + | AddBuggyImplementation + | DeleteBuggyImplementation(_) + | UpdateTitle(_) => false | Save | InitImportAll(_) | InitImportScratchpad(_) @@ -217,6 +235,7 @@ let should_scroll_to_caret = | Benchmark | ContextInspector | InstructorMode + | EditingTitle | Evaluation(_) => false } | SetMeta(meta_action) => @@ -228,6 +247,9 @@ let should_scroll_to_caret = } | UpdateResult(_) | ToggleStepper(_) + | UpdateTitle(_) + | AddBuggyImplementation + | DeleteBuggyImplementation(_) | StepperAction(_, StepBackward | StepForward(_)) => false | FinishImportScratchpad(_) | FinishImportAll(_) diff --git a/src/haz3lweb/exercises/Ex_OddlyRecursive.ml b/src/haz3lweb/exercises/Ex_OddlyRecursive.ml index 3d4ae0ce35..7648e34980 100644 --- a/src/haz3lweb/exercises/Ex_OddlyRecursive.ml +++ b/src/haz3lweb/exercises/Ex_OddlyRecursive.ml @@ -4,8 +4,8 @@ let prompt = Ex_OddlyRecursive_prompt.prompt let exercise : Exercise.spec = { + id = Option.get (Id.of_string "3335e34d-d211-4332-91e2-815e9e183885"); title = "Oddly Recursive"; - version = 1; module_name = "Ex_OddlyRecursive"; prompt; point_distribution = diff --git a/src/haz3lweb/exercises/Ex_RecursiveFibonacci.ml b/src/haz3lweb/exercises/Ex_RecursiveFibonacci.ml index cdcf9cb651..381db4f816 100644 --- a/src/haz3lweb/exercises/Ex_RecursiveFibonacci.ml +++ b/src/haz3lweb/exercises/Ex_RecursiveFibonacci.ml @@ -4,8 +4,8 @@ let prompt = Ex_RecursiveFibonacci_prompt.prompt let exercise : Exercise.spec = { + id = Option.get (Id.of_string "12f5e34d-d211-4332-91e2-815e9e183885"); title = "Recursive Fibonacci"; - version = 1; module_name = "Ex_RecursiveFibonacci"; prompt; point_distribution = diff --git a/src/haz3lweb/view/Cell.re b/src/haz3lweb/view/Cell.re index 65c01e1a6f..0fb56b1060 100644 --- a/src/haz3lweb/view/Cell.re +++ b/src/haz3lweb/view/Cell.re @@ -354,6 +354,25 @@ let title_cell = title => { ]); }; +let wrong_impl_caption = (~inject, sub: string, n: int) => { + div( + ~attrs=[Attr.class_("wrong-impl-cell-caption")], + [ + caption("", ~rest=sub), + div( + ~attrs=[Attr.class_("instructor-edit-icon")], + [ + Widgets.button( + Icons.delete, + _ => inject(UpdateAction.DeleteBuggyImplementation(n)), + ~tooltip="Delete Buggy Implementation", + ), + ], + ), + ], + ); +}; + /* An editor view that is not selectable or editable, * and does not show error holes or test results. * Used in Docs to display the header example */ diff --git a/src/haz3lweb/view/ExerciseMode.re b/src/haz3lweb/view/ExerciseMode.re index 2b291c99f4..fc0fcc95f5 100644 --- a/src/haz3lweb/view/ExerciseMode.re +++ b/src/haz3lweb/view/ExerciseMode.re @@ -57,14 +57,81 @@ let view = ~mousedown_updates=[SwitchEditor(this_pos)], ~settings, ~highlights, - ~caption=Cell.caption(caption, ~rest=?subcaption), + ~caption= + switch (this_pos) { + | HiddenBugs(n) => Cell.wrong_impl_caption(~inject, caption, n) + | _ => Cell.caption(caption, ~rest=?subcaption) + }, ~target_id=Exercise.show_pos(this_pos), ~test_results=ModelResult.test_results(di.result), ~footer?, editor, ); }; - let title_view = Cell.title_cell(eds.title); + + let update_title = _ => { + let new_title = + Obj.magic( + Js_of_ocaml.Js.some(JsUtil.get_elem_by_id("title-input-box")), + )##.value; + let update_events = [ + inject(Set(EditingTitle)), + inject(UpdateTitle(new_title)), + ]; + Virtual_dom.Vdom.Effect.Many(update_events); + }; + + let title_view = { + Cell.simple_cell_view([ + div( + ~attrs=[Attr.class_("title-cell")], + [ + settings.instructor_mode + ? settings.editing_title + ? div( + ~attrs=[Attr.class_("title-edit")], + [ + input( + ~attrs=[ + Attr.class_("title-text"), + Attr.id("title-input-box"), + Attr.value(eds.title), + ], + (), + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [Widgets.button(Icons.confirm, update_title)], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.cancel, _ => + inject(Set(EditingTitle)) + ), + ], + ), + ], + ) + : div( + ~attrs=[Attr.class_("title-edit")], + [ + text(eds.title), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.pencil, _ => + inject(Set(EditingTitle)) + ), + ], + ), + ], + ) + : div(~attrs=[Attr.class_("title-text")], [text(eds.title)]), + ], + ), + ]); + }; let prompt_view = Cell.narrative_cell( @@ -157,18 +224,40 @@ let view = let wrong_impl_views = List.mapi( (i, (Exercise.{impl, _}, di)) => { - InstructorOnly( - () => - editor_view( - HiddenBugs(i), - ~caption="Wrong Implementation " ++ string_of_int(i + 1), - ~editor=impl, - ~di, - ), + editor_view( + HiddenBugs(i), + ~caption="Mutant " ++ string_of_int(i + 1), + ~editor=impl, + ~di, ) }, List.combine(eds.hidden_bugs, hidden_bugs), ); + + let add_wrong_impl_view = + Cell.simple_cell_view([ + Cell.simple_cell_item([ + div( + ~attrs=[Attr.class_("wrong-impl-cell-caption")], + [ + div( + ~attrs=[ + Attr.class_("instructor-edit-icon"), + Attr.id("add-icon"), + ], + [ + Widgets.button( + Icons.add, + _ => inject(UpdateAction.AddBuggyImplementation), + ~tooltip="Add Buggy Implementation", + ), + ], + ), + ], + ), + ]), + ]); + let mutation_testing_view = Always( Grading.MutationTestingReport.view( @@ -237,6 +326,19 @@ let view = ~max_points=grading_report.point_distribution.impl_grading, ), ); + + let wrong_impl_views = + InstructorOnly( + () => + Cell.simple_cell_view([ + Cell.simple_cell_item( + [Cell.caption("Mutation Tests")] + @ wrong_impl_views + @ [add_wrong_impl_view], + ), + ]), + ); + [score_view, title_view, prompt_view] @ render_cells( settings, @@ -245,9 +347,7 @@ let view = correct_impl_view, correct_impl_ctx_view, your_tests_view, - ] - @ wrong_impl_views - @ [ + wrong_impl_views, mutation_testing_view, your_impl_view, syntax_grading_view, diff --git a/src/haz3lweb/view/Icons.re b/src/haz3lweb/view/Icons.re index 52d4e130db..06bba15d53 100644 --- a/src/haz3lweb/view/Icons.re +++ b/src/haz3lweb/view/Icons.re @@ -222,6 +222,30 @@ let backpack = ], ); +let pencil = + simple_icon( + ~view="0 0 512 512", + [ + "M403.914,0L54.044,349.871L0,512l162.128-54.044L512,108.086L403.914,0z M295.829,151.319l21.617,21.617L110.638,379.745 l-21.617-21.617L295.829,151.319z M71.532,455.932l-15.463-15.463l18.015-54.043l51.491,51.491L71.532,455.932z M153.871,422.979 l-21.617-21.617l206.809-206.809l21.617,21.617L153.871,422.979z M382.297,194.555l-64.852-64.852l21.617-21.617l64.852,64.852 L382.297,194.555z M360.679,86.468l43.234-43.235l64.853,64.853l-43.235,43.234L360.679,86.468z", + ], + ); + +let confirm = + simple_icon( + ~view="0 0 32 32", + [ + "m16 0c8.836556 0 16 7.163444 16 16s-7.163444 16-16 16-16-7.163444-16-16 7.163444-16 16-16zm0 2c-7.7319865 0-14 6.2680135-14 14s6.2680135 14 14 14 14-6.2680135 14-14-6.2680135-14-14-14zm6.6208153 9.8786797c.3905243.3905242.3905243 1.0236892 0 1.4142135l-7.0710678 7.0710678c-.3626297.3626297-.9344751.3885319-1.3269928.0777064l-.0872208-.0777064-4.24264068-4.2426407c-.39052429-.3905242-.39052429-1.0236892 0-1.4142135.39052428-.3905243 1.02368928-.3905243 1.41421358 0l3.5348268 3.5348268 6.3646681-6.3632539c.3905243-.3905243 1.0236893-.3905243 1.4142136 0z", + ], + ); + +let cancel = + simple_icon( + ~view="0 0 32 32", + [ + "m16 0c8.836556 0 16 7.163444 16 16s-7.163444 16-16 16-16-7.163444-16-16 7.163444-16 16-16zm0 2c-7.7319865 0-14 6.2680135-14 14s6.2680135 14 14 14 14-6.2680135 14-14-6.2680135-14-14-14zm4.2426407 9.7573593c.3905243.3905243.3905243 1.0236893 0 1.4142136l-2.8284271 2.8284271 2.8284271 2.8284271c.3905243.3905243.3905243 1.0236893 0 1.4142136s-1.0236893.3905243-1.4142136 0l-2.8284271-2.8284271-2.8284271 2.8284271c-.3905243.3905243-1.0236893.3905243-1.4142136 0s-.3905243-1.0236893 0-1.4142136l2.8284271-2.8284271-2.8284271-2.8284271c-.3905243-.3905243-.3905243-1.0236893 0-1.4142136s1.0236893-.3905243 1.4142136 0l2.8284271 2.8284271 2.8284271-2.8284271c.3905243-.3905243 1.0236893-.3905243 1.4142136 0z", + ], + ); + let command_palette_sparkle = simple_icon( ~view="400 400 400 400", @@ -231,3 +255,24 @@ let command_palette_sparkle = "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", ], ); + +let add = + simple_icon( + ~view="0 0 24 24", + [ + "M12.75 9C12.75 8.58579 12.4142 8.25 12 8.25C11.5858 8.25 11.25 8.58579 11.25 9L11.25 11.25H9C8.58579 11.25 8.25 11.5858 8.25 12C8.25 12.4142 8.58579 12.75 9 12.75H11.25V15C11.25 15.4142 11.5858 15.75 12 15.75C12.4142 15.75 12.75 15.4142 12.75 15L12.75 12.75H15C15.4142 12.75 15.75 12.4142 15.75 12C15.75 11.5858 15.4142 11.25 15 11.25H12.75V9Z", + "M12 1.25C6.06294 1.25 1.25 6.06294 1.25 12C1.25 17.9371 6.06294 22.75 12 22.75C17.9371 22.75 22.75 17.9371 22.75 12C22.75 6.06294 17.9371 1.25 12 1.25ZM2.75 12C2.75 6.89137 6.89137 2.75 12 2.75C17.1086 2.75 21.25 6.89137 21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12Z", + ], + ); + +let delete = + simple_icon( + ~view="0 0 24 24", + [ + "M12 2.75C11.0215 2.75 10.1871 3.37503 9.87787 4.24993C9.73983 4.64047 9.31134 4.84517 8.9208 4.70713C8.53026 4.56909 8.32557 4.1406 8.46361 3.75007C8.97804 2.29459 10.3661 1.25 12 1.25C13.634 1.25 15.022 2.29459 15.5365 3.75007C15.6745 4.1406 15.4698 4.56909 15.0793 4.70713C14.6887 4.84517 14.2602 4.64047 14.1222 4.24993C13.813 3.37503 12.9785 2.75 12 2.75Z", + "M2.75 6C2.75 5.58579 3.08579 5.25 3.5 5.25H20.5001C20.9143 5.25 21.2501 5.58579 21.2501 6C21.2501 6.41421 20.9143 6.75 20.5001 6.75H3.5C3.08579 6.75 2.75 6.41421 2.75 6Z", + "M5.91508 8.45011C5.88753 8.03681 5.53015 7.72411 5.11686 7.75166C4.70356 7.77921 4.39085 8.13659 4.41841 8.54989L4.88186 15.5016C4.96735 16.7844 5.03641 17.8205 5.19838 18.6336C5.36678 19.4789 5.6532 20.185 6.2448 20.7384C6.83639 21.2919 7.55994 21.5307 8.41459 21.6425C9.23663 21.75 10.2751 21.75 11.5607 21.75H12.4395C13.7251 21.75 14.7635 21.75 15.5856 21.6425C16.4402 21.5307 17.1638 21.2919 17.7554 20.7384C18.347 20.185 18.6334 19.4789 18.8018 18.6336C18.9637 17.8205 19.0328 16.7844 19.1183 15.5016L19.5818 8.54989C19.6093 8.13659 19.2966 7.77921 18.8833 7.75166C18.47 7.72411 18.1126 8.03681 18.0851 8.45011L17.6251 15.3492C17.5353 16.6971 17.4712 17.6349 17.3307 18.3405C17.1943 19.025 17.004 19.3873 16.7306 19.6431C16.4572 19.8988 16.083 20.0647 15.391 20.1552C14.6776 20.2485 13.7376 20.25 12.3868 20.25H11.6134C10.2626 20.25 9.32255 20.2485 8.60915 20.1552C7.91715 20.0647 7.54299 19.8988 7.26957 19.6431C6.99616 19.3873 6.80583 19.025 6.66948 18.3405C6.52891 17.6349 6.46488 16.6971 6.37503 15.3492L5.91508 8.45011Z", + "M9.42546 10.2537C9.83762 10.2125 10.2051 10.5132 10.2464 10.9254L10.7464 15.9254C10.7876 16.3375 10.4869 16.7051 10.0747 16.7463C9.66256 16.7875 9.29502 16.4868 9.25381 16.0746L8.75381 11.0746C8.71259 10.6625 9.0133 10.2949 9.42546 10.2537Z", + "M15.2464 11.0746C15.2876 10.6625 14.9869 10.2949 14.5747 10.2537C14.1626 10.2125 13.795 10.5132 13.7538 10.9254L13.2538 15.9254C13.2126 16.3375 13.5133 16.7051 13.9255 16.7463C14.3376 16.7875 14.7051 16.4868 14.7464 16.0746L15.2464 11.0746Z", + ], + ); diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index df98ba5ee9..66ed0ea588 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -10,27 +10,36 @@ let key_handler = ~inject: UpdateAction.t => Ui_effect.t(unit), ~dir: Key.dir, editor: Editor.t, + model: Model.t, evt: Js.t(Dom_html.keyboardEvent), ) : Effect.t(unit) => { open Effect; let key = Key.mk(dir, evt); + let get_settings = (model: Model.t): Settings.t => model.settings; switch (ProjectorView.key_handoff(editor, key)) { | Some(action) => Many([Prevent_default, inject(PerformAction(Project(action)))]) | None => switch (Keyboard.handle_key_event(key)) { | None => Ignore - | Some(action) => Many([Prevent_default, inject(action)]) + | Some(action) => + get_settings(model).editing_title + ? Many([inject(action)]) + : Many([Prevent_default, Stop_propagation, inject(action)]) } }; }; let handlers = - (~inject: UpdateAction.t => Ui_effect.t(unit), editor: Editor.t) => { - [ - Attr.on_keyup(key_handler(~inject, editor, ~dir=KeyUp)), - Attr.on_keydown(key_handler(~inject, editor, ~dir=KeyDown)), + ( + ~inject: UpdateAction.t => Ui_effect.t(unit), + editor: Editor.t, + model: Model.t, + ) => { + let attrs = [ + Attr.on_keyup(key_handler(~inject, editor, model, ~dir=KeyUp)), + Attr.on_keydown(key_handler(~inject, editor, model, ~dir=KeyDown)), /* safety handler in case mousedown overlay doesn't catch it */ Attr.on_mouseup(_ => inject(SetMeta(Mouseup))), Attr.on_blur(_ => { @@ -57,6 +66,8 @@ let handlers = inject(PerformAction(Paste(pasted_text))); }), ]; + model.settings.editing_title + ? attrs : attrs @ [Attr.on_keypress(_ => Effect.Prevent_default)]; }; let top_bar = @@ -89,7 +100,9 @@ let main_view = ~inject: UpdateAction.t => Ui_effect.t(unit), {settings, editors, explainThisModel, results, ui_state, _}: Model.t, ) => { + print_endline("here, at main view, getting editor"); let editor = Editors.get_editor(editors); + print_endline("got editor!"); let cursor_info = Indicated.ci_of(editor.state.zipper, editor.state.meta.statics.info_map); let highlights = @@ -187,7 +200,7 @@ let view = (~inject: UpdateAction.t => Ui_effect.t(unit), model: Model.t) => div( ~attrs=[ Attr.id("page"), - ...handlers(~inject, Editors.get_editor(model.editors)), + ...handlers(~inject, Editors.get_editor(model.editors), model), ], [ FontSpecimen.view("font-specimen"), diff --git a/src/haz3lweb/www/style/cell.css b/src/haz3lweb/www/style/cell.css index e10c38a126..a3fd22369e 100644 --- a/src/haz3lweb/www/style/cell.css +++ b/src/haz3lweb/www/style/cell.css @@ -62,10 +62,50 @@ color: var(--BR4); } +.title-edit .edit-icon { + margin-left: 0.5em; + cursor: pointer; + fill: #7a6219; +} + +.title-cell .title-edit { + font-size: 1.5rem; + font-weight: bold; + color: var(--light-text-color); + flex-grow: 1; + display: flex; + align-items: center; +} + +.title-edit .edit-icon:hover { + animation: wobble 0.6s ease 0s 1 normal forwards; +} + .cell-prompt { padding: 1em; } +.wrong-impl-cell-caption { + flex-grow: 1; + display: flex; + align-items: center; +} + +.instructor-edit-icon { + margin-top: 0.175em; + margin-left: 1em; + cursor: pointer; + fill: #7a6219; +} + +#add-icon { + margin-left: 0em; +} + +.instructor-edit-icon:hover { + animation: wobble 0.6s ease 0s 1 normal forwards; +} + /* DOCUMENTATION SLIDES */ .slide-img { diff --git a/src/haz3lweb/www/style/exercise-mode.css b/src/haz3lweb/www/style/exercise-mode.css index 5eb8e2d74c..b69c7037f0 100644 --- a/src/haz3lweb/www/style/exercise-mode.css +++ b/src/haz3lweb/www/style/exercise-mode.css @@ -234,4 +234,4 @@ #main.Exercises .context-entry { max-width: fit-content; /* Correct implementation type sigs */ -} +} \ No newline at end of file