Skip to content

Commit b72c6d3

Browse files
authored
Merge pull request #506 from pfitaxel/fix-reload
Remove Mechanism-2 (#372), Add a 3-fold on-demand Reload button, Fix extra minor bugs
2 parents c1054ab + dd69f3c commit b72c6d3

14 files changed

+380
-255
lines changed

src/ace-lib/ace.ml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ let set_synchronized_status editor status =
8888

8989
let focus { editor } = editor##focus
9090

91-
let create_editor editor_div check_valid_state =
91+
let create_editor editor_div =
9292
let editor = edit editor_div in
9393
Js.Unsafe.set editor "$blockScrolling" (Js.Unsafe.variable "Infinity");
9494
let data =
@@ -102,8 +102,6 @@ let create_editor editor_div check_valid_state =
102102
editor##.customData := (data, None);
103103
editor##setOption (Js.string "displayIndentGuides") (Js.bool false);
104104
editor##on (Js.string "change") (fun () ->
105-
check_valid_state (set_contents data) (fun () -> focus data)
106-
(fun () -> set_synchronized_status data true);
107105
set_synchronized_status data false);
108106
data
109107

src/ace-lib/ace.mli

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ type loc = {
1717
loc_end: int * int;
1818
}
1919

20-
val create_editor: Dom_html.divElement Js.t
21-
-> ((string -> unit) -> (unit -> unit) -> (unit -> unit) -> unit) -> 'a editor
20+
val create_editor: Dom_html.divElement Js.t -> 'a editor
2221

2322
val is_synchronized : 'a editor -> bool
2423

src/ace-lib/ocaml_mode.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,8 @@ let do_delete ace_editor =
514514
Ace.remove ace_editor "left"
515515
end
516516

517-
let create_ocaml_editor div check_valid_state =
518-
let ace = Ace.create_editor div check_valid_state in
517+
let create_ocaml_editor div =
518+
let ace = Ace.create_editor div in
519519
Ace.set_mode ace "ace/mode/ocaml.ocp";
520520
Ace.set_tab_size ace !config.indent.IndentConfig.i_base;
521521
let editor = { ace; current_error = None; current_warnings = [] } in

src/ace-lib/ocaml_mode.mli

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ type error = msg list
2424

2525
type warning = error
2626

27-
val create_ocaml_editor:
28-
Dom_html.divElement Js.t -> ((string -> unit) -> (unit -> unit) -> (unit -> unit) -> unit) -> editor
27+
val create_ocaml_editor: Dom_html.divElement Js.t -> editor
2928
val get_editor: editor -> editor Ace.editor
3029

3130
val report_error: editor -> ?set_class: bool -> error option -> warning list -> unit Lwt.t

src/app/learnocaml_common.ml

Lines changed: 116 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(* This file is part of Learn-OCaml.
22
*
3-
* Copyright (C) 2019-2020 OCaml Software Foundation.
3+
* Copyright (C) 2019-2022 OCaml Software Foundation.
44
* Copyright (C) 2016-2018 OCamlPro.
55
*
66
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -283,13 +283,13 @@ let disable_with_button_group component (buttons, _, _) =
283283
((component :> < disabled : bool Js.t Js.prop > Js.t), ref false)
284284
:: !buttons
285285

286-
let button ~container ~theme ?group ?state ~icon lbl cb =
286+
let button ?id ~container ~theme ?group ?state ~icon lbl cb =
287287
let (others, mutex, cnt) as group =
288288
match group with
289289
| None -> button_group ()
290290
| Some group -> group in
291291
let button =
292-
H.(button [
292+
H.(button ~a:(match id with Some id -> [ H.a_id id ] | _ -> []) [
293293
img ~alt:"" ~src:(api_server ^ "/icons/icon_" ^ icon ^ "_" ^ theme ^ ".svg") () ;
294294
txt " " ;
295295
span ~a:[ a_class [ "label" ] ] [ txt lbl ]
@@ -337,6 +337,32 @@ let dropdown ~id ~title items =
337337
H.div ~a: [H.a_id id; H.a_class ["dropdown_content"]] items
338338
]
339339

340+
let button_dropup ~container ~theme ?state ~icon ~id_menu ~items lbl cb_before =
341+
let btn_id = id_menu ^ "-btn" in (* assumed to be unique *)
342+
let toggle cb_before () =
343+
let menu = find_component id_menu in
344+
let disp =
345+
match Manip.Css.display menu with
346+
| "block" -> "none"
347+
| _ ->
348+
Lwt.dont_wait (fun () -> cb_before ()) (fun _exc -> ());
349+
Lwt_js_events.async (fun () ->
350+
Lwt_js_events.click window >|= fun ev ->
351+
Js.Opt.case ev##.target (fun () -> ())
352+
(fun e ->
353+
if Js.to_string e##.id <> btn_id then
354+
Manip.SetCss.display menu "none"));
355+
"block"
356+
in
357+
Manip.SetCss.display menu disp;
358+
Lwt.return_unit
359+
in
360+
let cb = toggle cb_before in
361+
let div_content =
362+
H.div ~a: [H.a_id id_menu; H.a_class ["dropup_content"]] items in
363+
button ~id:btn_id ~container:container ~theme ?state ~icon lbl cb ;
364+
Manip.appendChild container div_content
365+
340366
let gettimeofday () =
341367
(new%js Js.date_now)##getTime /. 1000.
342368

@@ -391,6 +417,8 @@ let set_state_from_save_file ?token save =
391417
let open Learnocaml_local_storage in
392418
(match token with None -> () | Some t -> store sync_token t);
393419
store nickname save.nickname;
420+
store all_graded_solutions
421+
(SMap.map (fun ans -> ans.Answer.solution) save.all_exercise_states);
394422
store all_exercise_states
395423
(SMap.merge (fun _ ans edi ->
396424
match ans, edi with
@@ -504,6 +532,7 @@ let sync_exercise token ?answer ?editor id on_sync =
504532
raise e)
505533
| None -> set_state_from_save_file save_file;
506534
handle_serverless ();
535+
on_sync ();
507536
Lwt.return save_file
508537

509538
let string_of_seconds seconds =
@@ -712,72 +741,11 @@ let mouseover_toggle_signal elt sigvalue setter =
712741
in
713742
Manip.Ev.onmouseover elt hdl
714743
715-
(*
716-
717-
If a user has made no change to a solution for the exercise [id]
718-
for 180 seconds, [check_valid_editor_state id] ensures that there is
719-
no more recent version of this solution in the server. If this is
720-
the case, the user is asked if we should download this solution
721-
from the server.
722-
723-
This function reduces the risk of an involuntary overwriting of a
724-
student solution when the solution is open in several clients.
725-
726-
*)
727-
let is_synchronized_with_server_callback = ref (fun () -> false)
728-
729-
let is_synchronized_with_server () = !is_synchronized_with_server_callback ()
730-
731-
let check_valid_editor_state id =
732-
let last_changed = ref (Unix.gettimeofday ()) in
733-
fun update_content focus_back on_sync ->
734-
let update_local_copy checking_time () =
735-
let get_solution () =
736-
Learnocaml_local_storage.(retrieve (exercise_state id)).Answer.solution in
737-
try let mtime =
738-
Learnocaml_local_storage.(retrieve (exercise_state id)).Answer.mtime in
739-
if mtime > checking_time then begin
740-
let buttons =
741-
if is_synchronized_with_server () then
742-
[
743-
[%i "Fetch from server"],
744-
(fun () -> let solution = get_solution () in
745-
Lwt.return (focus_back (); update_content solution; on_sync ()));
746-
[%i "Ignore & keep editing"],
747-
(fun () -> Lwt.return (focus_back ()));
748-
]
749-
else
750-
[
751-
[%i "Ignore & keep editing"],
752-
(fun () -> Lwt.return (focus_back ()));
753-
[%i "Fetch from server & overwrite"],
754-
(fun () -> let solution = get_solution () in
755-
Lwt.return (focus_back (); update_content solution; on_sync ()));
756-
]
757-
in
758-
lwt_alert ~title:"Question"
759-
~buttons
760-
[ H.p [H.txt [%i "A more recent answer exists on the server. \
761-
Do you want to fetch the new version?"] ] ]
762-
end else Lwt.return_unit
763-
with
764-
| Not_found -> Lwt.return ()
765-
in
766-
let now = Unix.gettimeofday () in
767-
if now -. !last_changed > 180. then (
768-
let checking_time = !last_changed in
769-
last_changed := now;
770-
Lwt.async (update_local_copy checking_time)
771-
) else
772-
last_changed := now
773-
774-
775744
let ace_display tab =
776745
let ace = lazy (
777746
let answer =
778747
Ocaml_mode.create_ocaml_editor
779748
(Tyxml_js.To_dom.of_div tab)
780-
(fun _ _ _ -> ())
781749
in
782750
let ace = Ocaml_mode.get_editor answer in
783751
Ace.set_font_size ace 16;
@@ -942,6 +910,9 @@ module Editor_button (E : Editor_info) = struct
942910
let editor_button =
943911
button ~container:E.buttons_container ~theme:"light"
944912
913+
let editor_button_dropup =
914+
button_dropup ~container:E.buttons_container ~theme:"light"
915+
945916
let cleanup template =
946917
editor_button
947918
~icon: "cleanup" [%i"Reset"] @@ fun () ->
@@ -951,6 +922,81 @@ module Editor_button (E : Editor_info) = struct
951922
Ace.set_contents E.ace template);
952923
Lwt.return ()
953924
925+
let reload token id template =
926+
let rec fetch_draft_solution tok () =
927+
match tok with
928+
| token ->
929+
Server_caller.request (Learnocaml_api.Fetch_save token) >>= function
930+
| Ok save ->
931+
set_state_from_save_file ~token save;
932+
Lwt.return_some (save.Save.nickname)
933+
| Error (`Not_found _) ->
934+
alert ~title:[%i"TOKEN NOT FOUND"]
935+
[%i"The entered token couldn't be recognised."];
936+
Lwt.return_none
937+
| Error e ->
938+
lwt_alert ~title:[%i"REQUEST ERROR"] [
939+
H.p [H.txt [%i"Could not retrieve data from server"]];
940+
H.code [H.txt (Server_caller.string_of_error e)];
941+
] ~buttons:[
942+
[%i"Retry"], (fun () -> fetch_draft_solution tok ());
943+
[%i"Cancel"], (fun () -> Lwt.return_none);
944+
]
945+
in
946+
let id_menu = "reload-button-dropup" in (* assumed to be unique *)
947+
editor_button_dropup
948+
~icon: "down"
949+
~id_menu
950+
~items: [
951+
H.ul [
952+
H.li ~a: [ H.a_id (id_menu ^ "-graded"); H.a_onclick (fun _ ->
953+
confirm ~title:[%i"Reload latest graded code"]
954+
[H.txt [%i"This will replace your code with your last graded code. Are you sure?"]]
955+
(fun () ->
956+
let graded = Learnocaml_local_storage.(retrieve (graded_solution id)) in
957+
Ace.set_contents E.ace graded; Ace.focus E.ace) ; true) ]
958+
[ H.txt [%i"Reload latest graded code"] ];
959+
960+
H.li ~a: [ H.a_id (id_menu ^ "-draft"); H.a_onclick (fun _ ->
961+
confirm ~title:[%i"Reload latest saved draft"]
962+
[H.txt [%i"This will replace your code with your last saved draft. Are you sure?"]]
963+
(fun () ->
964+
let draft = Learnocaml_local_storage.(retrieve (exercise_state id)).Answer.solution in
965+
Ace.set_contents E.ace draft; Ace.focus E.ace) ; true) ]
966+
[ H.txt [%i"Reload latest saved draft"] ];
967+
968+
H.li ~a: [ H.a_onclick (fun _ ->
969+
confirm ~title:[%i"START FROM SCRATCH"]
970+
[H.txt [%i"This will discard all your edits. Are you sure?"]]
971+
(fun () ->
972+
Ace.set_contents E.ace template; Ace.focus E.ace) ; true) ]
973+
[ H.txt [%i"Reset to initial template"] ];
974+
]
975+
]
976+
[%i"Reload"] @@ fun () ->
977+
token >>= function
978+
None ->
979+
(* We may want to only show "Reset to initial template" in this case,
980+
though there is already this code in learnocaml_exercise_main.ml:
981+
{| if has_server then EB.reload ... else EB.cleanup ... |}. *)
982+
Lwt.return_unit
983+
| Some tok ->
984+
let found f =
985+
match f () with
986+
| _val -> true
987+
| exception Not_found -> false
988+
in
989+
fetch_draft_solution tok () >|= fun _save ->
990+
let menu_draft = find_component (id_menu ^ "-draft") in
991+
Manip.SetCss.display menu_draft
992+
(if found (fun () ->
993+
Learnocaml_local_storage.(retrieve (exercise_state id)).Answer.solution)
994+
then "" else "none");
995+
let menu_graded = find_component (id_menu ^ "-graded") in
996+
Manip.SetCss.display menu_graded
997+
(if found (fun () ->
998+
Learnocaml_local_storage.(retrieve (graded_solution id)))
999+
then "" else "none")
9541000
let download id =
9551001
editor_button
9561002
~icon: "download" [%i"Download"] @@ fun () ->
@@ -976,19 +1022,22 @@ module Editor_button (E : Editor_info) = struct
9761022
sync_exercise token id ~editor:(Ace.get_contents E.ace) on_sync
9771023
>|= fun _save -> ());
9781024
Ace.register_sync_observer E.ace (fun sync ->
979-
if sync then disable_button state else enable_button state)
1025+
(* this is run twice when clicking on Reset, because of Ace's implem *)
1026+
if sync then disable_button state else enable_button state);
1027+
(* Disable the Sync button at loading time: *)
1028+
Ace.set_synchronized E.ace
9801029
9811030
end
9821031
983-
let setup_editor id solution =
1032+
let setup_editor solution =
9841033
let editor_pane = find_component "learnocaml-exo-editor-pane" in
9851034
let editor =
9861035
Ocaml_mode.create_ocaml_editor
9871036
(Tyxml_js.To_dom.of_div editor_pane)
988-
(check_valid_editor_state id)
9891037
in
9901038
let ace = Ocaml_mode.get_editor editor in
9911039
Ace.set_contents ace ~reset_undo:true solution;
1040+
(* "Ace.set_synchronized ace" done after "Ace.register_sync_observer" above *)
9921041
Ace.set_font_size ace 18;
9931042
editor, ace
9941043
@@ -1108,8 +1157,6 @@ let get_token ?(has_server = true) () =
11081157
Lwt.return
11091158
with
11101159
Not_found ->
1111-
retrieve (Learnocaml_api.Nonce ())
1112-
>>= fun nonce ->
11131160
ask_string ~title:"Token"
11141161
[H.txt [%i"Enter your token"]]
11151162
>>= fun input_tok ->

src/app/learnocaml_common.mli

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(* This file is part of Learn-OCaml.
22
*
3-
* Copyright (C) 2019 OCaml Software Foundation.
3+
* Copyright (C) 2019-2022 OCaml Software Foundation.
44
* Copyright (C) 2016-2018 OCamlPro.
55
*
66
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -91,6 +91,7 @@ val disable_with_button_group :
9191
button_group -> unit
9292

9393
val button :
94+
?id: string ->
9495
container: 'a Tyxml_js.Html.elt ->
9596
theme: string ->
9697
?group: button_group ->
@@ -105,6 +106,16 @@ val dropdown :
105106
[< Html_types.div_content_fun ] Tyxml_js.Html.elt list ->
106107
[> Html_types.div ] Tyxml_js.Html.elt
107108

109+
val button_dropup :
110+
container: 'a Tyxml_js.Html5.elt ->
111+
theme: string ->
112+
?state: button_state ->
113+
icon: string ->
114+
id_menu: string ->
115+
items: [< Html_types.div_content_fun ] Tyxml_js.Html.elt list ->
116+
string -> (unit -> unit Lwt.t) ->
117+
unit
118+
108119
val render_rich_text :
109120
?on_runnable_clicked: (string -> unit) ->
110121
Learnocaml_data.Tutorial.text ->
@@ -213,14 +224,13 @@ end
213224

214225
module Editor_button (_ : Editor_info) : sig
215226
val cleanup : string -> unit
227+
val reload : Learnocaml_data.Token.t option Lwt.t -> string -> string -> unit
216228
val download : string -> unit
217229
val eval : Learnocaml_toplevel.t -> (string -> unit) -> unit
218230
val sync : Token.t option Lwt.t -> Learnocaml_data.SMap.key -> (unit -> unit) -> unit
219231
end
220232

221-
val setup_editor : string -> string -> Ocaml_mode.editor * Ocaml_mode.editor Ace.editor
222-
223-
val is_synchronized_with_server_callback : (unit -> bool) ref
233+
val setup_editor : string -> Ocaml_mode.editor * Ocaml_mode.editor Ace.editor
224234

225235
val typecheck :
226236
Learnocaml_toplevel.t ->

0 commit comments

Comments
 (0)