diff --git a/src/haz3lschool/Exercise.re b/src/haz3lschool/Exercise.re index 01050d5e7b..eda13f1942 100644 --- a/src/haz3lschool/Exercise.re +++ b/src/haz3lschool/Exercise.re @@ -53,11 +53,10 @@ 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, + prompt: string, point_distribution, prelude: 'code, correct_impl: 'code, @@ -68,15 +67,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 +95,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 +133,33 @@ 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)), + prompt: string, + point_distribution, + required: int, + module_name: string, + // 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 repository 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 +169,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 +310,8 @@ module F = (ExerciseEnv: ExerciseEnv) => { let transition: transitionary_spec => spec = ( { + id, title, - version, module_name, prompt, point_distribution, @@ -321,8 +346,8 @@ module F = (ExerciseEnv: ExerciseEnv) => { {tests, hints}; }; { + id, title, - version, module_name, prompt, point_distribution, @@ -339,8 +364,8 @@ module F = (ExerciseEnv: ExerciseEnv) => { let eds_of_spec = ( { + id, title, - version, module_name, prompt, point_distribution, @@ -375,8 +400,8 @@ module F = (ExerciseEnv: ExerciseEnv) => { {tests, hints}; }; { + id, title, - version, module_name, prompt, point_distribution, @@ -467,6 +492,289 @@ 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_impl; + let pos = + length > 1 + ? HiddenBugs(index < length - 1 ? index : index - 1) : YourImpl; + 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 set_editing_prompt = ({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_prompt = ({eds, _} as state: state, new_prompt: string) => { + ...state, + eds: { + ...eds, + prompt: new_prompt, + }, + }; + + let set_editing_test_val_rep = ({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_test_val_rep = + ({eds, _} as state: state, new_test_num: int, new_dist: int) => { + ...state, + eds: { + ...eds, + your_tests: { + ...eds.your_tests, + required: new_test_num, + }, + point_distribution: { + ...eds.point_distribution, + test_validation: new_dist, + }, + }, + }; + + let set_editing_mut_test_rep = ({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_mut_test_rep = ({eds, _} as state: state, new_dist: int) => { + ...state, + eds: { + ...eds, + point_distribution: { + ...eds.point_distribution, + mutation_testing: new_dist, + }, + }, + }; + + let set_editing_impl_grd_rep = ({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_impl_grd_rep = ({eds, _} as state: state, new_dist: int) => { + ...state, + eds: { + ...eds, + point_distribution: { + ...eds.point_distribution, + impl_grading: new_dist, + }, + }, + }; + + let set_editing_module_name = ({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_module_name = + ({eds, _} as state: state, new_module_name: string) => { + ...state, + eds: { + ...eds, + module_name: new_module_name, + }, + }; + + let update_prov_tests = ({eds, _} as state: state, new_prov_tests: int) => { + ...state, + eds: { + ...eds, + your_tests: { + ...eds.your_tests, + provided: new_prov_tests, + }, + }, + }; + let visible_in = (pos, ~instructor_mode) => { switch (pos) { | Prelude => instructor_mode @@ -485,28 +793,56 @@ 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, + prompt: state.eds.prompt, + point_distribution: state.eds.point_distribution, + required: state.eds.your_tests.required, + module_name: state.eds.module_name, + }; }; let unpersist_state = ( - (pos, positioned_zippers): persistent_state, + { + focus, + editors, + title, + hidden_bugs, + prompt, + point_distribution, + required, + module_name, + }: persistent_state, ~spec: spec, ~instructor_mode: bool, + ~editing_title: bool, + ~editing_prompt: bool, + ~editing_test_val_rep: bool, + ~editing_mut_test_rep: bool, + ~editing_impl_grd_rep: bool, + ~editing_module_name: 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 +852,48 @@ 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, + prompt, + point_distribution, + prelude, + correct_impl, + your_tests: { + tests: your_tests_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, + ); + let state = set_editing_title(state, editing_title); + let state = set_editing_prompt(state, editing_prompt); + let state = set_editing_test_val_rep(state, editing_test_val_rep); + let state = set_editing_mut_test_rep(state, editing_mut_test_rep); + let state = set_editing_module_name(state, editing_module_name); + set_editing_impl_grd_rep(state, editing_impl_grd_rep); }; // # Stitching @@ -724,7 +1064,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 }; @@ -896,10 +1240,10 @@ module F = (ExerciseEnv: ExerciseEnv) => { ); let hidden_tests_tests = Zipper.next_blank(); { + id: Id.mk(), title, - version: 1, module_name, - prompt: ExerciseEnv.default, + prompt: "", point_distribution, prelude, correct_impl, @@ -922,8 +1266,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) => { @@ -932,11 +1276,31 @@ module F = (ExerciseEnv: ExerciseEnv) => { |> Sexplib.Sexp.to_string; }; - let deserialize_exercise = (data, ~spec, ~instructor_mode) => { + let deserialize_exercise = + ( + data, + ~spec, + ~instructor_mode, + ~editing_title, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, + ) => { data |> Sexplib.Sexp.of_string |> persistent_state_of_sexp - |> unpersist_state(~spec, ~instructor_mode); + |> unpersist_state( + ~spec, + ~instructor_mode, + ~editing_title, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, + ); }; let deserialize_exercise_export = data => { diff --git a/src/haz3lschool/Gradescope.re b/src/haz3lschool/Gradescope.re index 7277fcf85b..aedf6ea127 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,15 @@ module Main = { ~settings, ~spec, ~instructor_mode=true, + ~editing_title=false, + ~editing_prompt=false, + ~editing_test_val_rep=false, + ~editing_mut_test_rep=false, + ~editing_impl_grd_rep=false, + ~editing_module_name=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..78946cdeb5 100644 --- a/src/haz3lweb/Editors.re +++ b/src/haz3lweb/Editors.re @@ -1,4 +1,5 @@ open Util; +// open Virtual_dom.Vdom; open Haz3lcore; [@deriving (show({with_path: false}), sexp, yojson)] @@ -39,6 +40,27 @@ let put_editor = (ed: Editor.t, eds: t): t => Exercises(n, specs, Exercise.put_editor(exercise, ed)) }; +let obtain_new_prov_tests = + (eds: t, results: ModelResults.t, ~settings: CoreSettings.t): int => + switch (eds) { + | Exercises(_, _, exercise) => + let stitched_dynamics = + Exercise.stitch_dynamic( + settings, + exercise, + settings.dynamics ? Some(results) : None, + ); + let test_results = + ModelResult.test_results(stitched_dynamics.test_validation.result); + let new_prov_test = + switch (test_results) { + | Some(test_results) => test_results.total + | None => 0 + }; + new_prov_test; + | _ => 0 + }; + let update = (f: Editor.t => Editor.t, editors: t): t => editors |> get_editor |> f |> put_editor(_, editors); @@ -115,6 +137,144 @@ 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 set_editing_prompt = (editors: t, editing: bool): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.set_editing_prompt(exercise, editing)) + }; + +let update_exercise_prompt = (editors: t, new_prompt: string): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises( + n, + specs, + Exercise.update_exercise_prompt(exercise, new_prompt), + ) + }; + +let set_editing_test_val_rep = (editors: t, editing: bool): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.set_editing_test_val_rep(exercise, editing)) + }; + +let update_test_val_rep = (editors: t, new_test_num: int, new_dist: int): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises( + n, + specs, + Exercise.update_test_val_rep(exercise, new_test_num, new_dist), + ) + }; + +let set_editing_mut_test_rep = (editors: t, editing: bool): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.set_editing_mut_test_rep(exercise, editing)) + }; + +let update_mut_test_rep = (editors: t, new_dist: int): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.update_mut_test_rep(exercise, new_dist)) + }; + +let set_editing_impl_grd_rep = (editors: t, editing: bool): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.set_editing_impl_grd_rep(exercise, editing)) + }; + +let update_impl_grd_rep = (editors: t, new_dist: int): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.update_impl_grd_rep(exercise, new_dist)) + }; + +let set_editing_module_name = (editors: t, editing: bool): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.set_editing_module_name(exercise, editing)) + }; + +let update_module_name = (editors: t, new_module_name: string): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises( + n, + specs, + Exercise.update_module_name(exercise, new_module_name), + ) + }; + +let update_prov_tests = (editors: t, new_prov_tests: int): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.update_prov_tests(exercise, new_prov_tests)) + }; + 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..b020625687 100644 --- a/src/haz3lweb/Export.re +++ b/src/haz3lweb/Export.re @@ -55,12 +55,24 @@ 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; + let editing_prompt = settings.editing_prompt; + let editing_test_val_rep = settings.editing_test_val_rep; + let editing_mut_test_rep = settings.editing_mut_test_rep; + let editing_impl_grd_rep = settings.editing_impl_grd_rep; + let editing_module_name = settings.editing_module_name; Store.Scratch.import(~settings=settings.core, all.scratch); Store.Exercise.import( ~settings=settings.core, all.exercise, ~specs, ~instructor_mode, + ~editing_title, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, ); Log.import(all.log); }; diff --git a/src/haz3lweb/Grading.re b/src/haz3lweb/Grading.re index e16827b918..1661748c9b 100644 --- a/src/haz3lweb/Grading.re +++ b/src/haz3lweb/Grading.re @@ -1,7 +1,9 @@ +open Util; open Virtual_dom.Vdom; open Node; include Haz3lschool.Grading.F(Exercise.ExerciseEnv); +include Update; let score_view = ((earned: points, max: points)) => { div( @@ -53,16 +55,126 @@ module TestValidationReport = { }; }; - let view = (~inject, report: t, max_points: int) => { + let view = + ( + ~inject, + report: t, + max_points: int, + max_tests: int, + settings: Settings.t, + ) => { Cell.report_footer_view([ div( ~attrs=[Attr.classes(["test-summary"])], [ - div( - ~attrs=[Attr.class_("test-text")], - [score_view(score_of_percent(percentage(report), max_points))] - @ textual_summary(report), - ), + settings.instructor_mode + ? settings.editing_test_val_rep + ? div( + ~attrs=[Attr.class_("test-val-rep-edit")], + [ + div( + ~attrs=[Attr.class_("input-field")], + [ + label([text("New point max:")]), + input( + ~attrs=[ + Attr.type_("number"), + Attr.class_("point-num-input"), + Attr.id("point-max-input"), + Attr.value(string_of_int(max_points)), + ], + (), + ), + ], + ), + div( + ~attrs=[Attr.class_("input-field")], + [ + label([text("Tests required:")]), + input( + ~attrs=[ + Attr.type_("number"), + Attr.class_("point-num-input"), + Attr.id("test-required-input"), + Attr.value(string_of_int(max_tests)), + ], + (), + ), + ], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button( + Icons.confirm, + _ => { + let new_dist = + Obj.magic( + Js_of_ocaml.Js.some( + JsUtil.get_elem_by_id("point-max-input"), + ), + )##.value; + let new_test_num = + Obj.magic( + Js_of_ocaml.Js.some( + JsUtil.get_elem_by_id( + "test-required-input", + ), + ), + )##.value; + + let update_events = [ + inject(Set(EditingTestValRep)), + inject( + UpdateTestValRep( + int_of_string(new_test_num), + int_of_string(new_dist), + ), + ), + ]; + Virtual_dom.Vdom.Effect.Many(update_events); + }, + ), + ], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.cancel, _ => + inject(Set(EditingTestValRep)) + ), + ], + ), + ], + ) + : div( + ~attrs=[Attr.class_("test-text")], + [ + score_view( + score_of_percent(percentage(report), max_points), + ), + ] + @ textual_summary(report) + @ [ + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.pencil, _ => + inject(Set(EditingTestValRep)) + ), + ], + ), + ], + ) + : div( + ~attrs=[Attr.class_("test-text")], + [ + score_view( + score_of_percent(percentage(report), max_points), + ), + ] + @ textual_summary(report), + ), ] @ Option.to_list( report.test_results @@ -83,10 +195,73 @@ module MutationTestingReport = { include MutationTestingReport; open Haz3lcore; - let summary_message = (~score, ~total, ~found): Node.t => + let summary_message = + (~inject, ~score, ~total, ~found, ~max_points, settings: Settings.t) + : Node.t => div( - ~attrs=[Attr.classes(["test-text"])], - [score_view(score), text(summary_str(~total, ~found))], + ~attrs=[Attr.class_("test-text")], + settings.instructor_mode + ? settings.editing_mut_test_rep + ? [ + div( + ~attrs=[Attr.class_("input-field")], + [ + label([text("New point max:")]), + input( + ~attrs=[ + Attr.type_("number"), + Attr.class_("point-num-input"), + Attr.id("point-max-input"), + Attr.value(string_of_int(max_points)), + ], + (), + ), + ], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button( + Icons.confirm, + _ => { + let new_dist = + Obj.magic( + Js_of_ocaml.Js.some( + JsUtil.get_elem_by_id("point-max-input"), + ), + )##.value; + + let update_events = [ + inject(Set(EditingMutTestRep)), + inject(UpdateMutTestRep(int_of_string(new_dist))), + ]; + Virtual_dom.Vdom.Effect.Many(update_events); + }, + ), + ], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.cancel, _ => + inject(Set(EditingMutTestRep)) + ), + ], + ), + ] + : [ + score_view(score), + text(summary_str(~total, ~found)), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.pencil, _ => + inject(Set(EditingMutTestRep)) + ), + ], + ), + ] + : [score_view(score), text(summary_str(~total, ~found))], ); let bar = (~inject, instances) => @@ -108,7 +283,7 @@ module MutationTestingReport = { ), ); - let summary = (~inject, ~report, ~max_points) => { + let summary = (~inject, ~report, ~max_points, settings: Settings.t) => { let total = List.length(report.results); let found = List.length( @@ -126,9 +301,12 @@ module MutationTestingReport = { ], [ summary_message( + ~inject, ~score=score_of_percent(percentage(report), max_points), ~total, ~found, + ~max_points, + settings, ), bar(~inject, report.results), ], @@ -236,7 +414,19 @@ module MutationTestingReport = { // }; // }; - let view = (~inject, report: t, max_points: int) => + // let update_requirements = _ => { + // let new_prompt = + // Obj.magic( + // Js_of_ocaml.Js.some(JsUtil.get_elem_by_id("prompt-input-box")), + // )##.value; + // let update_events = [ + // inject(Set(EditingPrompt)), + // inject(UpdatePrompt(new_prompt)), + // ]; + // Virtual_dom.Vdom.Effect.Many(update_events); + // }; + + let view = (~inject, report: t, max_points: int, settings: Settings.t) => if (max_points == 0) { Node.div([]); } else { @@ -249,7 +439,7 @@ module MutationTestingReport = { ), individual_reports(~inject, report.results), ], - ~footer=Some(summary(~inject, ~report, ~max_points)), + ~footer=Some(summary(~inject, ~report, ~max_points, settings)), ); }; }; @@ -418,7 +608,13 @@ module ImplGradingReport = { }; let view = - (~inject, ~report: t, ~syntax_report: SyntaxReport.t, ~max_points: int) => { + ( + ~inject, + ~report: t, + ~syntax_report: SyntaxReport.t, + ~max_points: int, + ~settings: Settings.t, + ) => { Cell.panel( ~classes=["cell-item", "panel", "test-panel"], [ @@ -434,18 +630,99 @@ module ImplGradingReport = { div( ~attrs=[Attr.classes(["test-summary"])], [ - div( - ~attrs=[Attr.class_("test-text")], - [ - score_view( - score_of_percent( - percentage(report, syntax_report), - max_points, + settings.instructor_mode + ? settings.editing_impl_grd_rep + ? Node.div([ + div( + ~attrs=[Attr.class_("input-field")], + [ + label([text("New point max:")]), + input( + ~attrs=[ + Attr.type_("number"), + Attr.class_("point-num-input"), + Attr.id("point-max-input"), + Attr.value(string_of_int(max_points)), + ], + (), + ), + ], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button( + Icons.confirm, + _ => { + let new_dist = + Obj.magic( + Js_of_ocaml.Js.some( + JsUtil.get_elem_by_id( + "point-max-input", + ), + ), + )##.value; + + let update_events = [ + inject(Set(EditingImplGrdRep)), + inject( + UpdateImplGrdRep( + int_of_string(new_dist), + ), + ), + ]; + Virtual_dom.Vdom.Effect.Many(update_events); + }, + ), + ], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.cancel, _ => + inject(Set(EditingImplGrdRep)) + ), + ], + ), + ]) + : Node.div([ + div( + ~attrs=[Attr.class_("test-text")], + [ + score_view( + score_of_percent( + percentage(report, syntax_report), + max_points, + ), + ), + ] + @ textual_summary(report) + @ [ + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.pencil, _ => + inject(Set(EditingImplGrdRep)) + ), + ], + ), + ], + ), + ]) + : Node.div([ + div( + ~attrs=[Attr.class_("test-text")], + [ + score_view( + score_of_percent( + percentage(report, syntax_report), + max_points, + ), + ), + ] + @ textual_summary(report), ), - ), - ] - @ textual_summary(report), - ), + ]), ] @ Option.to_list( report.test_results diff --git a/src/haz3lweb/Init.ml b/src/haz3lweb/Init.ml index c3d2de0aba..8118a19f71 100644 --- a/src/haz3lweb/Init.ml +++ b/src/haz3lweb/Init.ml @@ -26,6 +26,12 @@ let startup : PersistentData.t = async_evaluation = false; context_inspector = false; instructor_mode = true; + editing_title = false; + editing_prompt = false; + editing_test_val_rep = false; + editing_mut_test_rep = false; + editing_impl_grd_rep = false; + editing_module_name = 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..f751efe36a 100644 --- a/src/haz3lweb/Log.re +++ b/src/haz3lweb/Log.re @@ -25,6 +25,14 @@ let is_action_logged: UpdateAction.t => bool = | Undo | Redo | UpdateResult(_) + | UpdateTitle(_) + | AddBuggyImplementation + | DeleteBuggyImplementation(_) + | UpdatePrompt(_) + | UpdateTestValRep(_) + | UpdateMutTestRep(_) + | UpdateImplGrdRep(_) + | UpdateModuleName(_) | ToggleStepper(_) | StepperAction(_, StepForward(_) | StepBackward) | UpdateExplainThisModel(_) => true; diff --git a/src/haz3lweb/Main.re b/src/haz3lweb/Main.re index 16811a32cb..bf79046762 100644 --- a/src/haz3lweb/Main.re +++ b/src/haz3lweb/Main.re @@ -19,43 +19,81 @@ let restart_caret_animation = () => let apply = (model, action, ~schedule_action, ~schedule_autosave): Model.t => { restart_caret_animation(); - if (UpdateAction.is_edit(action)) { - schedule_autosave( - BonsaiUtil.Alarm.Action.SetAlarm( - Core.Time_ns.add(Core.Time_ns.now(), Core.Time_ns.Span.of_sec(1.0)), - ), - ); - } else { - schedule_autosave( - BonsaiUtil.Alarm.Action.SnoozeAlarm( - Core.Time_ns.add(Core.Time_ns.now(), Core.Time_ns.Span.of_sec(1.0)), - ), - ); - }; - if (Update.should_scroll_to_caret(action)) { - scroll_to_caret := true; - }; - switch ( - try({ - let new_model = Update.apply(model, action, ~schedule_action); - Log.update(action); - new_model; - }) { - | exc => - Printf.printf( - "ERROR: Exception during apply: %s\n", - Printexc.to_string(exc), + + let get_settings = (model: Model.t): Settings.t => model.settings; + + let settings = get_settings(model); + let editing_mode = + settings.editing_prompt + || settings.editing_title + || settings.editing_test_val_rep + || settings.editing_mut_test_rep + || settings.editing_impl_grd_rep + || settings.editing_module_name; + + switch (action, editing_mode) { + | (UpdateAction.PerformAction(Insert(_)), true) => model + | (UpdateAction.PerformAction(Destruct(_)), true) => model + | (UpdateAction.PerformAction(Move(_)), true) => model + | (action, _) => + if (UpdateAction.is_edit(action)) { + schedule_autosave( + BonsaiUtil.Alarm.Action.SetAlarm( + Core.Time_ns.add( + Core.Time_ns.now(), + Core.Time_ns.Span.of_sec(1.0), + ), + ), + ); + } else { + schedule_autosave( + BonsaiUtil.Alarm.Action.SnoozeAlarm( + Core.Time_ns.add( + Core.Time_ns.now(), + Core.Time_ns.Span.of_sec(1.0), + ), + ), ); - Error(Exception(Printexc.to_string(exc))); - } - ) { - | Ok(model) => model - | Error(FailedToPerform(err)) => - print_endline(Update.Failure.show(FailedToPerform(err))); - model; - | Error(err) => - print_endline(Update.Failure.show(err)); - model; + }; + if (Update.should_scroll_to_caret(action)) { + scroll_to_caret := true; + }; + switch ( + try({ + let new_model = Update.apply(model, action, ~schedule_action); + Log.update(action); + new_model; + }) { + | exc => + Printf.printf( + "ERROR: Exception during apply: %s\n", + Printexc.to_string(exc), + ); + Error(Exception(Printexc.to_string(exc))); + } + ) { + | Ok(model) => + let new_prov_tests = + Editors.obtain_new_prov_tests( + model.editors, + model.results, + ~settings=model.settings.core, + ); + let updated_model = + model.settings.instructor_mode + ? { + ...model, + editors: Editors.update_prov_tests(model.editors, new_prov_tests), + } + : model; + updated_model; + | Error(FailedToPerform(err)) => + print_endline(Update.Failure.show(FailedToPerform(err))); + model; + | Error(err) => + print_endline(Update.Failure.show(err)); + model; + }; }; }; diff --git a/src/haz3lweb/Model.re b/src/haz3lweb/Model.re index e4939b3af0..413247bec0 100644 --- a/src/haz3lweb/Model.re +++ b/src/haz3lweb/Model.re @@ -55,7 +55,17 @@ 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, + ~editing_prompt: bool, + ~editing_test_val_rep: bool, + ~editing_mut_test_rep: bool, + ~editing_impl_grd_rep: bool, + ~editing_module_name: bool, + ) : (Editors.t, ModelResults.t) => switch (mode) { | Scratch => @@ -70,6 +80,12 @@ let load_editors = ~settings, ~specs=ExerciseSettings.exercises, ~instructor_mode, + ~editing_title, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, ); (Exercises(n, specs, exercise), ModelResults.empty); }; @@ -93,6 +109,12 @@ let load = (init_model: t): t => { ~settings=settings.core, ~mode=settings.mode, ~instructor_mode=settings.instructor_mode, + ~editing_title=settings.editing_title, + ~editing_prompt=settings.editing_prompt, + ~editing_test_val_rep=settings.editing_test_val_rep, + ~editing_mut_test_rep=settings.editing_mut_test_rep, + ~editing_impl_grd_rep=settings.editing_impl_grd_rep, + ~editing_module_name=settings.editing_module_name, ); 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..6c7af34aba 100644 --- a/src/haz3lweb/Settings.re +++ b/src/haz3lweb/Settings.re @@ -22,6 +22,12 @@ type t = { async_evaluation: bool, context_inspector: bool, instructor_mode: bool, + editing_title: bool, + editing_prompt: bool, + editing_test_val_rep: bool, + editing_mut_test_rep: bool, + editing_impl_grd_rep: bool, + editing_module_name: bool, benchmark: bool, explainThis: ExplainThisModel.Settings.t, mode, diff --git a/src/haz3lweb/Store.re b/src/haz3lweb/Store.re index f30a18ab85..91e2825277 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,16 +280,33 @@ 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, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, + ) + : Exercise.state => { + let keystring = Id.to_string(spec.id); switch (JsUtil.get_localstore(keystring)) { | Some(data) => let exercise = try( - Exercise.deserialize_exercise( + deserialize_exercise( data, ~spec, ~instructor_mode, + ~editing_title, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, ~settings, ) ) { @@ -316,8 +319,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 +340,112 @@ module Exercise = { }; let load = - (~settings: CoreSettings.t, ~specs, ~instructor_mode) + ( + ~settings: CoreSettings.t, + ~specs, + ~instructor_mode, + ~editing_title, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, + ) : (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, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, + ~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, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, + ~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, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, + ) : 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, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, + ~settings, + ) |> Exercise.persistent_state_of_state(~instructor_mode); (key, exercise); }), @@ -397,7 +454,17 @@ 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, + ~editing_prompt=false, + ~editing_test_val_rep=false, + ~editing_mut_test_rep=false, + ~editing_impl_grd_rep=false, + ~editing_module_name=false, + ~settings, + ) |> sexp_of_exercise_export |> Sexplib.Sexp.to_string; }; @@ -407,12 +474,23 @@ module Exercise = { }; let import = - (data, ~specs, ~instructor_mode: bool, ~settings: CoreSettings.t) => { + ( + data, + ~specs, + ~instructor_mode: bool, + ~editing_title: bool, + ~editing_prompt: bool, + ~editing_test_val_rep: bool, + ~editing_mut_test_rep: bool, + ~editing_impl_grd_rep: bool, + ~editing_module_name: 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 +500,12 @@ module Exercise = { persistent_state, ~spec, ~instructor_mode, + ~editing_title, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, ~settings, ), ~instructor_mode, diff --git a/src/haz3lweb/Update.re b/src/haz3lweb/Update.re index 778df5114a..b5e9b99580 100644 --- a/src/haz3lweb/Update.re +++ b/src/haz3lweb/Update.re @@ -185,12 +185,80 @@ let update_settings = } | InstructorMode => let new_mode = !settings.instructor_mode; + let editors = Editors.set_editing_prompt(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, + editing_prompt: false, + editing_test_val_rep: false, + editing_mut_test_rep: false, + editing_impl_grd_rep: false, + editing_module_name: false, + }, + }; + | EditingTitle => + let editing = !settings.editing_title; + { + ...model, + editors: Editors.set_editing_title(model.editors, editing), + settings: { + ...settings, + editing_title: editing, + }, + }; + | EditingPrompt => + let editing = !settings.editing_prompt; + { + ...model, + editors: Editors.set_editing_prompt(model.editors, editing), + settings: { + ...settings, + editing_prompt: editing, + }, + }; + | EditingTestValRep => + let editing = !settings.editing_test_val_rep; + { + ...model, + editors: Editors.set_editing_test_val_rep(model.editors, editing), + settings: { + ...settings, + editing_test_val_rep: editing, + }, + }; + | EditingMutTestRep => + let editing = !settings.editing_mut_test_rep; + { + ...model, + editors: Editors.set_editing_mut_test_rep(model.editors, editing), + settings: { + ...settings, + editing_mut_test_rep: editing, + }, + }; + | EditingImplGrdRep => + let editing = !settings.editing_impl_grd_rep; + { + ...model, + editors: Editors.set_editing_impl_grd_rep(model.editors, editing), + settings: { + ...settings, + editing_impl_grd_rep: editing, + }, + }; + | EditingModuleName => + let editing = !settings.editing_module_name; + { + ...model, + editors: Editors.set_editing_module_name(model.editors, editing), + settings: { + ...settings, + editing_module_name: editing, }, }; | Mode(mode) => { @@ -281,7 +349,18 @@ 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, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, + idx: int, + ) : option(Editors.t) => switch (editors) { | Documentation(_) => None @@ -291,9 +370,18 @@ 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, + ~editing_prompt, + ~editing_test_val_rep, + ~editing_mut_test_rep, + ~editing_impl_grd_rep, + ~editing_module_name, + ~settings, + ); Some(Exercises(idx, specs, exercise)); }; @@ -489,16 +577,32 @@ 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, + editing_prompt: false, + editing_test_val_rep: false, + editing_mut_test_rep: false, + editing_impl_grd_rep: false, + editing_module_name: false, + }; switch ( switch_scratch_slide( ~settings=model.settings.core, - model.editors, + editors, ~instructor_mode, + ~editing_title=false, + ~editing_prompt=false, + ~editing_test_val_rep=false, + ~editing_mut_test_rep=false, + ~editing_impl_grd_rep=false, + ~editing_module_name=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)) { @@ -576,6 +680,50 @@ 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); + Model.save_and_return({...model, editors}); + | UpdatePrompt(new_prompt) => + Model.save_and_return({ + ...model, + editors: Editors.update_exercise_prompt(model.editors, new_prompt), + }) + | UpdateTestValRep(new_test_num, new_dist) => + Model.save_and_return({ + ...model, + editors: + Editors.update_test_val_rep(model.editors, new_test_num, new_dist), + }) + | UpdateMutTestRep(new_dist) => + Model.save_and_return({ + ...model, + editors: Editors.update_mut_test_rep(model.editors, new_dist), + }) + | UpdateImplGrdRep(new_dist) => + Model.save_and_return({ + ...model, + editors: Editors.update_impl_grd_rep(model.editors, new_dist), + }) + | UpdateModuleName(new_module_name) => + Model.save_and_return({ + ...model, + editors: Editors.update_module_name(model.editors, new_module_name), + }) }; m |> Result.map(~f=update_cached_data(~schedule_action, update)); }; diff --git a/src/haz3lweb/UpdateAction.re b/src/haz3lweb/UpdateAction.re index cd2f145f3e..b611f4c44e 100644 --- a/src/haz3lweb/UpdateAction.re +++ b/src/haz3lweb/UpdateAction.re @@ -24,6 +24,12 @@ type settings_action = | Benchmark | ContextInspector | InstructorMode + | EditingTitle + | EditingPrompt + | EditingTestValRep + | EditingMutTestRep + | EditingImplGrdRep + | EditingModuleName | Evaluation(evaluation_settings_action) | ExplainThis(ExplainThisModel.Settings.action) | Mode(Settings.mode); @@ -45,6 +51,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 +95,15 @@ 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) + | UpdatePrompt(string) + | UpdateTestValRep(int, int) + | UpdateMutTestRep(int) + | UpdateImplGrdRep(int) + | UpdateModuleName(string); module Failure = { [@deriving (show({with_path: false}), sexp, yojson)] @@ -117,6 +137,12 @@ let is_edit: t => bool = | Benchmark | ContextInspector | InstructorMode + | EditingTitle + | EditingPrompt + | EditingTestValRep + | EditingMutTestRep + | EditingImplGrdRep + | EditingModuleName | Evaluation(_) => false } | SetMeta(meta_action) => @@ -135,6 +161,14 @@ let is_edit: t => bool = | FinishImportAll(_) | FinishImportScratchpad(_) | ResetCurrentEditor + | UpdateTitle(_) + | AddBuggyImplementation + | DeleteBuggyImplementation(_) + | UpdatePrompt(_) + | UpdateTestValRep(_) + | UpdateMutTestRep(_) + | UpdateImplGrdRep(_) + | UpdateModuleName(_) | Reset | TAB => true | UpdateResult(_) @@ -171,6 +205,12 @@ let reevaluate_post_update: t => bool = | Assist | Dynamics | InstructorMode + | EditingTitle + | EditingPrompt + | EditingTestValRep + | EditingMutTestRep + | EditingImplGrdRep + | EditingModuleName | Mode(_) => true } | SetMeta(meta_action) => @@ -180,12 +220,20 @@ let reevaluate_post_update: t => bool = | ShowBackpackTargets(_) | FontMetrics(_) => false } + | AddBuggyImplementation + | DeleteBuggyImplementation(_) + | UpdateTitle(_) => false | Save | InitImportAll(_) | InitImportScratchpad(_) | UpdateExplainThisModel(_) | Export(_) | UpdateResult(_) + | UpdatePrompt(_) + | UpdateTestValRep(_) + | UpdateMutTestRep(_) + | UpdateImplGrdRep(_) + | UpdateModuleName(_) | SwitchEditor(_) | DebugConsole(_) | Benchmark(_) => false @@ -206,6 +254,12 @@ let should_scroll_to_caret = fun | Set(s_action) => switch (s_action) { + | EditingTitle => false + | EditingPrompt => false + | EditingTestValRep => false + | EditingMutTestRep => false + | EditingImplGrdRep => false + | EditingModuleName => false | Mode(_) => true | Captions | SecondaryIcons @@ -228,6 +282,12 @@ let should_scroll_to_caret = } | UpdateResult(_) | ToggleStepper(_) + | UpdateTitle(_) + | UpdatePrompt(_) + | UpdateTestValRep(_) + | UpdateMutTestRep(_) + | UpdateImplGrdRep(_) + | UpdateModuleName(_) | StepperAction(_, StepBackward | StepForward(_)) => false | FinishImportScratchpad(_) | FinishImportAll(_) @@ -239,6 +299,8 @@ let should_scroll_to_caret = | Undo | Redo | TAB + | AddBuggyImplementation + | DeleteBuggyImplementation(_) | Startup => true | PerformAction(a) => switch (a) { 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_OddlyRecursive_prompt.re b/src/haz3lweb/exercises/Ex_OddlyRecursive_prompt.re index eed88befa9..aaaafecd21 100644 --- a/src/haz3lweb/exercises/Ex_OddlyRecursive_prompt.re +++ b/src/haz3lweb/exercises/Ex_OddlyRecursive_prompt.re @@ -1,20 +1,5 @@ -open Virtual_dom.Vdom; -open Node; -open ExerciseUtil; +// open Virtual_dom.Vdom; +// open Node; +// open ExerciseUtil; -let prompt = - div([ - p([ - text( - "Write a recursive function that determines whether the given integer is odd. ", - ), - ]), - p([ - code("odd(n)"), - equiv, - code("true"), - text(" iff "), - code("n"), - text(" is odd."), - ]), - ]); +let prompt = "Write a recursive function that determines whether the given integer is odd. \n `odd(n)` is equivalent to `true` iff `n` is odd."; 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/exercises/Ex_RecursiveFibonacci_prompt.re b/src/haz3lweb/exercises/Ex_RecursiveFibonacci_prompt.re index 278dfe941a..4bf27fe4c7 100644 --- a/src/haz3lweb/exercises/Ex_RecursiveFibonacci_prompt.re +++ b/src/haz3lweb/exercises/Ex_RecursiveFibonacci_prompt.re @@ -1,22 +1,5 @@ -open Virtual_dom.Vdom; -open Node; -open ExerciseUtil; +// open Virtual_dom.Vdom; +// open Node; +// open ExerciseUtil; -let prompt = - div([ - p([ - div([ - text( - "Write tests cases for, and then implement, a function, that recursively determines the nth fibonacci number.", - ), - ]), - ]), - p([ - code("fib(n)"), - equiv, - text("the "), - code("n"), - text("th fibonacci number, assuming "), - code("n >= 0."), - ]), - ]); +let prompt = "Write test cases for, and then implement, a function that recursively determines the nth Fibonacci number. \n`fib(n)` is equivalent to the `n`th Fibonacci number, assuming `n >= 0`."; diff --git a/src/haz3lweb/view/Cell.re b/src/haz3lweb/view/Cell.re index ba06d2a29e..ffec6fe5b8 100644 --- a/src/haz3lweb/view/Cell.re +++ b/src/haz3lweb/view/Cell.re @@ -65,10 +65,10 @@ let mousedown_handler = | (false, n) => inject(PerformAction(Select(Smart(n)))) }; -let narrative_cell = (content: Node.t) => +let narrative_cell = (content: list(Node.t)) => div( ~attrs=[Attr.class_("cell")], - [div(~attrs=[Attr.class_("cell-chapter")], [content])], + [div(~attrs=[Attr.class_("cell-chapter")], content)], ); let simple_cell_item = (content: list(Node.t)) => @@ -354,6 +354,32 @@ 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"), + Attr.on_mousedown(_ => + Virtual_dom.Vdom.Effect.( + Many([Prevent_default, Stop_propagation]) + ) + ), + ], + [ + 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..a856f6f051 100644 --- a/src/haz3lweb/view/ExerciseMode.re +++ b/src/haz3lweb/view/ExerciseMode.re @@ -57,19 +57,241 @@ 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 prompt_view = - Cell.narrative_cell( - div(~attrs=[Attr.class_("cell-prompt")], [eds.prompt]), - ); + 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 = { + let title_placeholder = eds.title == "" ? "Untitled Exercise" : eds.title; + 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")], + [ + div( + ~attrs=[ + Attr.classes([ + "title-text", + eds.title == "" ? "title-placeholder" : "", + ]), + ], + [text(title_placeholder)], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.pencil, _ => + inject(Set(EditingTitle)) + ), + ], + ), + ], + ) + : div(~attrs=[Attr.class_("title-text")], [text(eds.title)]), + ], + ), + ]); + }; + + let update_module_name = _ => { + let new_module_name = + Obj.magic( + Js_of_ocaml.Js.some(JsUtil.get_elem_by_id("module-name-input")), + )##.value; + let update_events = [ + inject(Set(EditingModuleName)), + inject(UpdateModuleName(new_module_name)), + ]; + Virtual_dom.Vdom.Effect.Many(update_events); + }; + + let module_name_view = { + let module_placeholder = + eds.module_name == "" ? "Unnamed Module" : eds.module_name; + settings.instructor_mode + ? Cell.narrative_cell([ + div( + ~attrs=[Attr.class_("cell-module-name")], + [ + settings.editing_module_name + ? div( + ~attrs=[Attr.class_("module-name-edit")], + [ + label([text("Module name:")]), + input( + ~attrs=[ + Attr.type_("text"), + Attr.class_("text-input"), + Attr.id("module-name-input"), + Attr.value(eds.module_name), + ], + (), + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [Widgets.button(Icons.confirm, update_module_name)], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.cancel, _ => + inject(Set(EditingModuleName)) + ), + ], + ), + ], + ) + : div( + ~attrs=[Attr.class_("module-name-text")], + [ + text("Module name: "), + div( + ~attrs=[ + Attr.classes([ + eds.module_name == "" ? "module-placeholder" : "", + ]), + ], + [text(module_placeholder)], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.pencil, _ => + inject(Set(EditingModuleName)) + ), + ], + ), + ], + ), + ], + ), + ]) + : Node.none; + }; + + let update_prompt = _ => { + let new_prompt = + Obj.magic( + Js_of_ocaml.Js.some(JsUtil.get_elem_by_id("prompt-input-box")), + )##.value; + let update_events = [ + inject(Set(EditingPrompt)), + inject(UpdatePrompt(new_prompt)), + ]; + Virtual_dom.Vdom.Effect.Many(update_events); + }; + + let prompt_view = { + let prompt_placeholder = eds.prompt == "" ? "Empty Prompt" : eds.prompt; + let (msg, _) = + ExplainThis.mk_translation(~inject=Some(inject), prompt_placeholder); + Cell.narrative_cell([ + div( + ~attrs=[Attr.class_("cell-prompt")], + [ + settings.instructor_mode + ? settings.editing_prompt + ? div( + ~attrs=[Attr.class_("prompt-edit")], + [ + textarea( + ~attrs=[ + Attr.class_("prompt-text"), + Attr.id("prompt-input-box"), + ], + [text(eds.prompt)], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [Widgets.button(Icons.confirm, update_prompt)], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.cancel, _ => + inject(Set(EditingPrompt)) + ), + ], + ), + ], + ) + : div( + ~attrs=[Attr.class_("prompt-edit")], + [ + div( + ~attrs=[ + Attr.classes([ + "prompt-content", + eds.prompt == "" ? "prompt-placeholder" : "", + ]), + ], + msg, + ), + div( + ~attrs=[Attr.class_("edit-pencil")], + [ + Widgets.button(Icons.pencil, _ => + inject(Set(EditingPrompt)) + ), + ], + ), + ], + ) + : div(~attrs=[Attr.class_("prompt-content")], msg), + ], + ), + ]); + }; + let prelude_view = Always( editor_view( @@ -150,6 +372,8 @@ let view = ~inject, grading_report.test_validation_report, grading_report.point_distribution.test_validation, + eds.your_tests.required, + settings, ), ], ), @@ -157,24 +381,47 @@ 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( ~inject, grading_report.mutation_testing_report, grading_report.point_distribution.mutation_testing, + settings, ), ); let your_impl_view = { @@ -235,9 +482,23 @@ let view = ~report=grading_report.impl_grading_report, ~syntax_report=grading_report.syntax_report, ~max_points=grading_report.point_distribution.impl_grading, + ~settings, ), ); - [score_view, title_view, prompt_view] + + 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, module_name_view, prompt_view] @ render_cells( settings, [ @@ -245,9 +506,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/ExplainThis.re b/src/haz3lweb/view/ExplainThis.re index a74a5187d8..4d6953ce11 100644 --- a/src/haz3lweb/view/ExplainThis.re +++ b/src/haz3lweb/view/ExplainThis.re @@ -174,6 +174,7 @@ let mk_translation = (~inject, text: string): (list(Node.t), ColorSteps.t) => { ), mapping, ); + | Omd.Soft_break(_) => (List.append(msg, [Node.br()]), mapping) | _ => (msg, mapping) }; }; @@ -184,7 +185,9 @@ let mk_translation = (~inject, text: string): (list(Node.t), ColorSteps.t) => { List.fold_left( ((msg, mapping), elem) => { switch (elem) { - | Omd.Paragraph(_, d) => translate_inline(d, msg, mapping, ~inject) + | Omd.Paragraph(_, d) => + let (n, _) = translate_inline(d, [], mapping, ~inject); + (List.append(msg, [Node.p(n)]), mapping); | Omd.List(_, _, _, items) => let (bullets, mapping) = List.fold_left( 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..ad2f6f6604 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -10,27 +10,42 @@ 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) => + let settings = get_settings(model); + settings.editing_prompt + || settings.editing_title + || settings.editing_test_val_rep + || settings.editing_mut_test_rep + || settings.editing_impl_grd_rep + || settings.editing_module_name + ? Many([Stop_propagation, 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 +72,13 @@ let handlers = inject(PerformAction(Paste(pasted_text))); }), ]; + model.settings.editing_prompt + || model.settings.editing_title + || model.settings.editing_test_val_rep + || model.settings.editing_mut_test_rep + || model.settings.editing_impl_grd_rep + || model.settings.editing_module_name + ? attrs : attrs @ [Attr.on_keypress(_ => Effect.Prevent_default)]; }; let top_bar = @@ -187,7 +209,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.css b/src/haz3lweb/www/style.css index d70e70a422..91c4b8c2bb 100644 --- a/src/haz3lweb/www/style.css +++ b/src/haz3lweb/www/style.css @@ -35,6 +35,7 @@ select { color: var(--select-text); /* text-transform: capitalize; */ } + select:hover { border-radius: 1em; background-color: var(--SAND); @@ -142,4 +143,4 @@ ninja-keys { --ninja-footer-background: var(--T2); --ninja-modal-shadow: 0px 10px 20px var(--menu-shadow); --ninja-overflow-background: none; -} +} \ No newline at end of file diff --git a/src/haz3lweb/www/style/cell.css b/src/haz3lweb/www/style/cell.css index e10c38a126..35aa18e12b 100644 --- a/src/haz3lweb/www/style/cell.css +++ b/src/haz3lweb/www/style/cell.css @@ -62,8 +62,50 @@ color: var(--BR4); } -.cell-prompt { - padding: 1em; +.title-cell .title-placeholder { + font-style: italic; + color: var(--BR4); + opacity: 0.5; +} + +.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; +} + +.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 */ @@ -92,3 +134,105 @@ .file-select-button { display: none; } + +/* EDITING */ + +.cell-module-name { + padding: 1em; + display: flex; + align-items: center; + gap: 0.5em; +} + +.module-name-text { + display: flex; + align-items: center; + gap: 0.5em; +} + +.module-name-edit { + display: flex; + align-items: center; + gap: 0.5em; +} + +.module-name-edit label { + white-space: nowrap; +} + +.module-name-edit input { + margin: 0 0.5em; +} + +.module-name-edit .edit-icon { + display: inline-flex; + align-items: center; + margin-left: 0.5em; +} + +.module-placeholder { + display: inline; + font-style: italic; + color: var(--BR4); + opacity: 0.5; +} + +.cell-prompt { + padding: 1em; +} + +.prompt-edit .prompt-text { + width: 500px; + height: 300px; + padding: 10px; + font-size: 16px; + + white-space: pre-wrap; + word-wrap: break-word; +} + +.cell-prompt .prompt-edit { + font-size: 1.5rem; + font-weight: bold; + color: var(--light-text-color); + flex-grow: 1; + display: flex; + align-items: center; +} + +.cell-prompt .prompt-edit .edit-pencil { + align-self: top; +} + +.cell-prompt .prompt-edit .edit-icon { + align-self: flex-start; +} + +.cell-prompt .prompt-placeholder { + font-style: italic; + color: var(--BR4); + opacity: 0.5; +} + +.prompt-content { + font-size: 1rem; +} + +.edit-icon, +.edit-pencil { + margin-left: 0.5em; + cursor: pointer; + fill: #7a6219; + display: inline; + vertical-align: top; +} + +.edit-icon:hover, +.edit-pencil:hover { + animation: wobble 0.6s ease 0s 1 normal forwards; +} + +.point-num-input { + width: 50px; + font-size: 12px; +} \ No newline at end of file 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