1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
(** *)
open Js_of_ocaml
open Ojs_js
let (>>=) = Lwt.(>>=)
type mime_type = string
type session = {
sess_file : Ojs_base.Path.t ;
sess_mime : mime_type ;
sess_ace : Ojs_ace.editSession Js.t ;
mutable sess_changed : bool ;
}
module PMap = Ojs_base.Path.Map
let mk_button label =
let doc = Dom_html.document in
let b = doc##createElement(Js.string "button") in
let text = doc##createTextNode(Js.string label) in
Dom.appendChild b text ;
b
let is_editable_from_mime =
let text = "text/" in
let len_text = String.length text in
function
| "application/octet-stream" -> true
| mime ->
String.length mime >= len_text &&
String.sub mime 0 len_text = text
module type S =
sig
module P : Ojs_ed.Types.P
class editor :
(P.client_msg -> (P.server_msg -> unit Lwt.t) -> unit Lwt.t) ->
(P.client_msg -> unit Lwt.t) ->
bar_id:string ->
msg_id:string ->
string ->
object
val mutable current : session option
val mutable sessions : session PMap.t
method changed_files : PMap.key list
method changed_sessions : session list
method display_error : string -> unit
method display_filename : session -> unit
method display_message : string -> unit
method edit_file : ?mime:mime_type -> PMap.key -> unit Lwt.t
method get_session : PMap.key -> session option
method handle_message : P.server_msg -> bool Js.t
method id : string
method is_editable_from_mime : mime_type -> bool
method load_from_server : session -> unit Lwt.t
method msg_id : string
method new_session : ?mime:mime_type -> PMap.key -> session
method on_changed : session -> unit
method reload : unit Lwt.t
method reload_file : session -> unit Lwt.t
method save : unit Lwt.t
method save_changed_files : unit Lwt.t
method save_file : session -> unit Lwt.t
method simple_call :
?on_ok:(unit -> unit) -> P.client_msg -> unit Lwt.t
end
class editors :
(P.app_client_msg -> (P.app_server_msg -> unit Lwt.t) -> unit Lwt.t) ->
(P.app_client_msg -> unit Lwt.t) ->
((P.client_msg -> (P.server_msg -> unit Lwt.t) -> unit Lwt.t) ->
(P.client_msg -> unit Lwt.t) ->
bar_id:string -> msg_id:string -> string -> editor) ->
object
val mutable editors : editor Ojs_js.SMap.t
method get_editor : Ojs_js.SMap.key -> editor
method get_msg_id : Ojs_js.SMap.key -> string
method handle_message : P.app_server_msg -> bool Js.t
method setup_editor :
bar_id:string -> msg_id:string -> Ojs_js.SMap.key -> editor
end
end
module Make(P:Ojs_ed.Types.P) =
struct
module P = P
class editor call (send : P.client_msg -> unit Lwt.t)
~bar_id ~msg_id ed_id =
let editor = Ojs_ace.ace##edit (Js.string ed_id) in
let _ = editor##setFontSize (Js.string "14px") in
let rend = editor##.renderer in
let () = rend##setShowGutter (Js.bool true) in
let () = rend##.hScrollBarAlwaysVisible := (Js.bool false) in
let () = rend##.vScrollBarAlwaysVisible := (Js.bool false) in
let _ = editor##setKeyboardHandler (Js.string "ace/keyboard/emacs") in
let bar = Ojs_js.node_by_id bar_id in
let doc = Dom_html.document in
let btn_save = mk_button "Save" in
let btn_reload = mk_button "Reload" in
let filename_id = ed_id ^ "__filename" in
let fname = doc##createElement (Js.string "span") in
let _ =
fname##setAttribute (Js.string "id") (Js.string filename_id) ;
fname##setAttribute (Js.string "class") (Js.string "filename") ;
Dom.appendChild bar btn_save ;
Dom.appendChild bar btn_reload ;
Dom.appendChild bar fname
in
object(self)
val mutable current = (None : session option)
val mutable sessions = (PMap.empty : session PMap.t)
method id = ed_id
method msg_id = msg_id
method on_changed sess =
match current with
| Some s when s.sess_file = sess.sess_file ->
self#display_filename s
| _ -> ()
method get_session file =
try Some (PMap.find file sessions)
with Not_found -> None
method display_error msg = Ojs_js.display_text_error msg_id msg
method display_message msg = Ojs_js.display_text_message msg_id msg
method display_filename s =
let node = Ojs_js.node_by_id filename_id in
Ojs_js.clear_children node ;
let fname = Printf.sprintf "%s%s"
(if s.sess_changed then "*" else "")
(Ojs_base.Path.to_string s.sess_file)
in
let t = Dom_html.document##createTextNode (Js.string fname) in
Dom.appendChild node t
method simple_call : ?on_ok: (unit -> unit) -> 'clt -> unit Lwt.t = fun ?on_ok msg ->
call msg
(fun msg -> Lwt.return
(match msg with
| P.SError msg -> self#display_error msg
| P.SOk msg ->
begin
self#display_message msg ;
match on_ok with
| None -> ()
| Some f -> f ()
end
| _ -> ()
)
)
method save_file sess =
let on_ok () =
let b = sess.sess_changed in
if b then
begin
sess.sess_changed <- false ;
self#on_changed sess
end
in
let contents = Js.to_string sess.sess_ace##getValue in
self#simple_call ~on_ok (P.Save_file (sess.sess_file, contents))
method save =
match current with
None -> Lwt.return_unit
| Some sess -> self#save_file sess
method changed_sessions =
PMap.fold
(fun _ s acc -> if s.sess_changed then s :: acc else acc)
sessions []
method changed_files =
PMap.fold
(fun path s acc -> if s.sess_changed then path :: acc else acc)
sessions []
method save_changed_files =
match self#changed_sessions with
| [] -> Lwt.return_unit
| l -> Lwt_list.iter_p self#save_file l
method load_from_server s =
let cb = function
| P.SFile_contents (file, contents) when s.sess_file = file ->
begin
s.sess_ace##setValue (Js.string contents);
s.sess_changed <- false ;
self#on_changed s ;
Lwt.return_unit
end
| _ -> Lwt.return_unit
in
if self#is_editable_from_mime s.sess_mime then
call (P.Get_file_contents s.sess_file) cb
else
Lwt.return_unit
method reload_file sess =
let do_it =
not sess.sess_changed ||
(
let msg = Printf.sprintf
"%s is modified and not saved.\nDo you really want to reload file from server ?"
(Ojs_base.Path.to_string sess.sess_file)
in
Js.to_bool (Dom_html.window##confirm(Js.string msg))
)
in
if do_it then self#load_from_server sess else Lwt.return_unit
method reload =
match current with
| None -> Lwt.return_unit
| Some sess -> self#reload_file sess
method new_session ?(mime="text/") file =
let sess_ace = Ojs_ace.newEditSession "" "" in
sess_ace##setUndoManager(Ojs_ace.newUndoManager());
sess_ace##setUseWrapMode(Js.bool true);
sess_ace##setUseWorker(Js.bool false);
let doc = sess_ace##getDocument in
let sess = {
sess_ace ; sess_mime = mime ;
sess_changed = false ; sess_file = file ;
}
in
let mode =
let mode =
Ojs_ace.modeList##getModeForPath(Js.string (Ojs_base.Path.to_string file))
in
mode##.mode
in
sess_ace##setMode(mode);
doc##on (Js.string "change")
(fun _ ->
if not sess.sess_changed then
begin sess.sess_changed <- true; self#on_changed sess end
);
sessions <- PMap.add file sess sessions;
if not (self#is_editable_from_mime mime) then
sess_ace##setReadOnly(Js.bool true);
sess
method is_editable_from_mime = is_editable_from_mime
method edit_file ?mime path =
(match self#get_session path with
| Some sess -> Lwt.return sess
| None ->
let s = self#new_session ?mime path in
self#load_from_server s >>= fun _ -> Lwt.return s
) >>= fun sess ->
(
editor##setSession(sess.sess_ace);
current <- Some sess ;
Lwt.return (self#on_changed sess)
)
method handle_message (msg : 'srv) =
try
(match msg with
| P.SOk msg -> self#display_message msg
| P.SError msg -> self#display_error msg
| _ -> failwith "Unhandled message received from server"
);
Js._false
with
e ->
log (Printexc.to_string e);
Js._false
initializer
Ojs_js.set_onclick btn_save (fun _ -> self#save);
Ojs_js.set_onclick btn_reload (fun _ -> self#reload);
end
class editors
(call : P.app_client_msg -> (P.app_server_msg -> unit Lwt.t) -> unit Lwt.t)
(send : P.app_client_msg -> unit Lwt.t)
(spawn : (P.client_msg -> (P.server_msg -> unit Lwt.t) -> unit Lwt.t) ->
(P.client_msg -> unit Lwt.t) ->
bar_id: string -> msg_id: string -> string -> editor) =
object(self)
val mutable editors = (SMap.empty : editor SMap.t)
method get_editor id =
try SMap.find id editors
with Not_found -> failwith (Printf.sprintf "Invalid editor id %S" id)
method get_msg_id id = (self#get_editor id)#msg_id
method handle_message (msg : P.app_server_msg) =
match P.unpack_server_msg msg with
| Some (id, msg) -> (self#get_editor id)#handle_message msg
| None -> Js._false
method setup_editor ~bar_id ~msg_id ed_id =
let send msg = send (P.pack_client_msg ed_id msg) in
let call msg cb =
let cb msg =
match P.unpack_server_msg msg with
| Some (_, msg) -> cb msg
| None -> Lwt.return_unit
in
call (P.pack_client_msg ed_id msg) cb
in
let editor = spawn call send ~bar_id ~msg_id ed_id in
editors <- SMap.add ed_id editor editors;
editor
end
end