aboutsummaryrefslogtreecommitdiff
path: root/ide/coqide/ideutils.ml
diff options
context:
space:
mode:
Diffstat (limited to 'ide/coqide/ideutils.ml')
-rw-r--r--ide/coqide/ideutils.ml607
1 files changed, 607 insertions, 0 deletions
diff --git a/ide/coqide/ideutils.ml b/ide/coqide/ideutils.ml
new file mode 100644
index 0000000000..482cecc1f8
--- /dev/null
+++ b/ide/coqide/ideutils.ml
@@ -0,0 +1,607 @@
+(************************************************************************)
+(* * The Coq Proof Assistant / The Coq Development Team *)
+(* v * Copyright INRIA, CNRS and contributors *)
+(* <O___,, * (see version control and CREDITS file for authors & dates) *)
+(* \VV/ **************************************************************)
+(* // * This file is distributed under the terms of the *)
+(* * GNU Lesser General Public License Version 2.1 *)
+(* * (see LICENSE file for the text of the license) *)
+(************************************************************************)
+
+open Preferences
+
+let _ = GtkMain.Main.init ()
+
+let warn_image () =
+ let img = GMisc.image () in
+ img#set_stock `DIALOG_WARNING;
+ img#set_icon_size `DIALOG;
+ img
+
+let warning msg =
+ GToolbox.message_box ~title:"Warning" ~icon:(warn_image ())#coerce msg
+
+let cb = GData.clipboard Gdk.Atom.primary
+
+(* status bar and locations *)
+
+let status = GMisc.statusbar ()
+
+let push_info,pop_info,clear_info =
+ let status_context = status#new_context ~name:"Messages" in
+ let size = ref 0 in
+ (fun s -> incr size; ignore (status_context#push s)),
+ (fun () -> decr size; status_context#pop ()),
+ (fun () -> for _i = 1 to !size do status_context#pop () done; size := 0)
+
+type 'a mlist = Nil | Cons of { hd : 'a ; mutable tl : 'a mlist }
+
+let enqueue a x =
+ let rec aux x = match x with
+ | Nil -> assert false
+ | Cons p ->
+ match p.tl with
+ | Nil -> p.tl <- Cons { hd = a ; tl = Nil }
+ | _ -> aux p.tl in
+ match !x with
+ | Nil -> x := Cons { hd = a ; tl = Nil }
+ | _ -> aux !x
+
+let pop = function
+ | Cons p -> p.tl
+ | Nil -> assert false
+
+let flash_info =
+ let queue = ref Nil in
+ let flash_context = status#new_context ~name:"Flash" in
+ let rec process () = match !queue with
+ | Cons { hd = (delay,text) } ->
+ let msg = flash_context#push text in
+ ignore (Glib.Timeout.add ~ms:delay ~callback:(fun () ->
+ flash_context#remove msg;
+ queue := pop !queue;
+ process (); false))
+ | Nil -> () in
+ fun ?(delay=5000) text ->
+ let processing = !queue <> Nil in
+ enqueue (delay,text) queue;
+ if not processing then process ()
+
+(* Note: Setting the same attribute with two separate tags appears to use
+the first value applied and not the second. I saw this trying to set the background
+color on Windows. A clean fix, if ever needed, would be to combine the attributes
+of the tags into a single composite tag before applying. This is left as an
+exercise for the reader. *)
+let insert_with_tags (buf : #GText.buffer_skel) mark rmark tags text =
+ (* FIXME: LablGTK2 does not export the C insert_with_tags function, so that
+ it has to reimplement its own helper function. Unluckily, it relies on
+ a slow algorithm, so that we have to have our own quicker version here.
+ Alas, it is still much slower than the native version... *)
+ if CList.is_empty tags then buf#insert ~iter:(buf#get_iter_at_mark mark) text
+ else
+ let it = buf#get_iter_at_mark mark in
+ let () = buf#move_mark rmark ~where:it in
+ let () = buf#insert ~iter:(buf#get_iter_at_mark mark) text in
+ let start = buf#get_iter_at_mark mark in
+ let stop = buf#get_iter_at_mark rmark in
+ let iter tag = buf#apply_tag tag ~start ~stop in
+ List.iter iter (List.rev tags)
+
+let nl_white_regex = Str.regexp "^\\( *\n *\\)"
+let diff_regex = Str.regexp "^diff."
+
+let insert_xml ?(mark = `INSERT) ?(tags = []) (buf : #GText.buffer_skel) msg =
+ let open Xml_datatype in
+ let dtags = ref [] in
+ let tag name =
+ match GtkText.TagTable.lookup buf#tag_table name with
+ | None -> raise Not_found
+ | Some tag -> new GText.tag tag
+ in
+ let rmark = `MARK (buf#create_mark buf#start_iter) in
+ (* insert the string, but don't apply diff highlights to white space at the begin/end of line *)
+ let rec insert_str tags s =
+ let etags = try List.hd !dtags :: tags with hd -> tags in
+ try
+ let start = Str.search_forward nl_white_regex s 0 in
+ insert_with_tags buf mark rmark etags (String.sub s 0 start);
+ insert_with_tags buf mark rmark tags (Str.matched_group 1 s);
+ let mend = Str.match_end () in
+ insert_str tags (String.sub s mend (String.length s - mend))
+ with Not_found ->
+ insert_with_tags buf mark rmark etags s
+ in
+ let rec insert tags = function
+ | PCData s -> insert_str tags s
+ | Element (t, _, children) ->
+ let (pfx, tname) = Pp.split_tag t in
+ let is_diff = try let _ = Str.search_forward diff_regex tname 0 in true with Not_found -> false in
+ let (tags, have_tag) =
+ try
+ let t = tag tname in
+ if is_diff && pfx <> Pp.end_pfx then
+ dtags := t :: !dtags;
+ if pfx = "" then
+ ((if is_diff then tags else t :: tags), true)
+ else
+ (tags, true)
+ with Not_found -> (tags, false)
+ in
+ List.iter (fun xml -> insert tags xml) children;
+ if have_tag && is_diff && pfx <> Pp.start_pfx then
+ dtags := (try List.tl !dtags with tl -> []);
+ in
+ let () = try insert tags msg with _ -> () in
+ buf#delete_mark rmark
+
+let set_location = ref (function s -> failwith "not ready")
+
+let display_location ins =
+ let line = ins#line + 1 in
+ let off = ins#line_offset + 1 in
+ let msg = Printf.sprintf "Line: %5d Char: %3d" line off in
+ !set_location msg
+
+(** A utf8 char is either a single byte (ascii char, 0xxxxxxx)
+ or multi-byte (with a leading byte 11xxxxxx and extra bytes 10xxxxxx) *)
+
+let is_extra_byte c = ((Char.code c) lsr 6 = 2)
+
+(** For a string buffer that may contain utf8 chars,
+ we convert a byte offset into a char offset
+ by only counting char-starting bytes.
+ Normally the string buffer starts with a char-starting byte
+ (buffer produced by a [#get_text]) *)
+
+let byte_offset_to_char_offset s byte_offset =
+ let extra_bytes = ref 0 in
+ for i = 0 to min byte_offset (String.length s - 1) do
+ if is_extra_byte s.[i] then incr extra_bytes
+ done;
+ byte_offset - !extra_bytes
+
+let glib_utf8_pos_to_offset s ~off = byte_offset_to_char_offset s off
+
+let do_convert s =
+ let from_loc () =
+ let _,char_set = Glib.Convert.get_charset () in
+ flash_info ("Converting from locale ("^char_set^")");
+ Glib.Convert.convert_with_fallback
+ ~to_codeset:"UTF-8" ~from_codeset:char_set s
+ in
+ let from_manual enc =
+ flash_info ("Converting from "^ enc);
+ Glib.Convert.convert s ~to_codeset:"UTF-8" ~from_codeset:enc
+ in
+ let s =
+ if Glib.Utf8.validate s then (Minilib.log "Input is UTF-8"; s)
+ else match encoding#get with
+ |Preferences.Eutf8 | Preferences.Elocale -> from_loc ()
+ |Emanual enc -> try from_manual enc with _ -> from_loc ()
+ in
+ Utf8_convert.f s
+
+let try_convert s =
+ try
+ do_convert s
+ with _ ->
+ "(* Fatal error: wrong encoding in input. \
+Please choose a correct encoding in the preference panel.*)";;
+
+let export file_name s =
+ let oc = open_out_bin file_name in
+ let ending = line_ending#get in
+ let is_windows = ref false in
+ for i = 0 to String.length s - 1 do
+ match s.[i] with
+ | '\r' -> is_windows := true
+ | '\n' ->
+ begin match ending with
+ | `DEFAULT ->
+ if !is_windows then (output_char oc '\r'; output_char oc '\n')
+ else output_char oc '\n'
+ | `WINDOWS -> output_char oc '\r'; output_char oc '\n'
+ | `UNIX -> output_char oc '\n'
+ end
+ | c -> output_char oc c
+ done;
+ close_out oc
+
+let try_export file_name s =
+ let s =
+ try match encoding#get with
+ |Eutf8 -> Minilib.log "UTF-8 is enforced" ; s
+ |Elocale ->
+ let is_unicode,char_set = Glib.Convert.get_charset () in
+ if is_unicode then
+ (Minilib.log "Locale is UTF-8" ; s)
+ else
+ (Minilib.log ("Locale is "^char_set);
+ Glib.Convert.convert_with_fallback
+ ~from_codeset:"UTF-8" ~to_codeset:char_set s)
+ |Emanual enc ->
+ (Minilib.log ("Manual charset is "^ enc);
+ Glib.Convert.convert_with_fallback
+ ~from_codeset:"UTF-8" ~to_codeset:enc s)
+ with e ->
+ let str = Printexc.to_string e in
+ Minilib.log ("Error ("^str^") in transcoding: falling back to UTF-8");
+ s
+ in
+ try export file_name s; true
+ with e -> Minilib.log (Printexc.to_string e);false
+
+type timer = { run : ms:int -> callback:(unit->bool) -> unit;
+ kill : unit -> unit }
+
+let mktimer () =
+ let timer = ref None in
+ { run =
+ (fun ~ms ~callback ->
+ timer := Some (GMain.Timeout.add ~ms ~callback));
+ kill =
+ (fun () -> match !timer with
+ | None -> ()
+ | Some id ->
+ (try GMain.Timeout.remove id
+ with Glib.GError _ -> ());
+ timer := None) }
+
+let filter_all_files () = GFile.filter
+ ~name:"All"
+ ~patterns:["*"] ()
+
+let filter_coq_files () = GFile.filter
+ ~name:"Coq source code"
+ ~patterns:[ "*.v"] ()
+
+let current_dir () = match project_path#get with
+| None -> ""
+| Some dir -> dir
+
+let select_file_for_open ~title ?(filter=true) ?(multiple=false) ?parent ?filename () =
+ let file_chooser =
+ GWindow.file_chooser_dialog ~action:`OPEN ~modal:true ~title ?parent ()
+ in
+ file_chooser#add_button_stock `CANCEL `CANCEL ;
+ file_chooser#add_select_button_stock `OPEN `OPEN ;
+ if filter then
+ begin
+ file_chooser#add_filter (filter_coq_files ());
+ file_chooser#add_filter (filter_all_files ())
+ end;
+ file_chooser#set_default_response `OPEN;
+ file_chooser#set_select_multiple multiple;
+ let dir = match filename with
+ | None -> current_dir ()
+ | Some f -> Filename.dirname f in
+ ignore (file_chooser#set_current_folder dir);
+ let file =
+ match file_chooser#run () with
+ | `OPEN ->
+ begin
+ let filenames = file_chooser#get_filenames in
+ (if filenames <> [] then
+ project_path#set file_chooser#current_folder);
+ filenames
+ end
+ | `DELETE_EVENT | `CANCEL -> [] in
+ file_chooser#destroy ();
+ file
+
+let select_file_for_save ~title ?parent ?filename () =
+ let file = ref None in
+ let file_chooser =
+ GWindow.file_chooser_dialog ~action:`SAVE ~modal:true ~title ?parent ()
+ in
+ file_chooser#add_button_stock `CANCEL `CANCEL ;
+ file_chooser#add_select_button_stock `SAVE `SAVE ;
+ file_chooser#add_filter (filter_coq_files ());
+ file_chooser#add_filter (filter_all_files ());
+ file_chooser#set_do_overwrite_confirmation true;
+ file_chooser#set_default_response `SAVE;
+ let dir,filename = match filename with
+ |None -> current_dir (), ""
+ |Some f -> Filename.dirname f, Filename.basename f
+ in
+ ignore (file_chooser#set_current_folder dir);
+ ignore (file_chooser#set_current_name filename);
+ begin match file_chooser#run () with
+ | `SAVE ->
+ begin
+ file := file_chooser#filename;
+ match !file with
+ None -> ()
+ | Some s -> project_path#set file_chooser#current_folder
+ end
+ | `DELETE_EVENT | `CANCEL -> ()
+ end ;
+ file_chooser#destroy ();
+ !file
+
+let find_tag_start (tag :GText.tag) (it:GText.iter) =
+ let it = it#copy in
+ let tag = Some tag in
+ while not (it#begins_tag tag) && it#nocopy#backward_char do
+ ()
+ done;
+ it
+let find_tag_stop (tag :GText.tag) (it:GText.iter) =
+ let it = it#copy in
+ let tag = Some tag in
+ while not (it#ends_tag tag) && it#nocopy#forward_char do
+ ()
+ done;
+ it
+let find_tag_limits (tag :GText.tag) (it:GText.iter) =
+ (find_tag_start tag it , find_tag_stop tag it)
+
+let stock_to_widget ?(size=`BUTTON) s =
+ let img = GMisc.image () in
+ (match size with
+ | `CUSTOM(width,height) ->
+ let opb = img#misc#render_icon ~size:`BUTTON s in
+ let pb = GdkPixbuf.create ~width ~height
+ ~bits:(GdkPixbuf.get_bits_per_sample opb)
+ ~has_alpha:(GdkPixbuf.get_has_alpha opb) () in
+ GdkPixbuf.scale ~width ~height ~dest:pb opb;
+ img#set_pixbuf pb
+ | #Gtk.Tags.icon_size as icon_size ->
+ img#set_stock s;
+ img#set_icon_size icon_size);
+ img#coerce
+
+let custom_coqtop = ref None
+
+let coqtop_path () =
+ let file = match !custom_coqtop with
+ | Some s -> s
+ | None ->
+ match cmd_coqtop#get with
+ | Some s -> s
+ | None ->
+ try
+ let new_prog = System.get_toplevel_path "coqidetop" in
+ (* The file exists or it is to be found by path *)
+ if Sys.file_exists new_prog ||
+ CString.equal Filename.(basename new_prog) new_prog
+ then new_prog
+ else
+ let in_macos_bundle =
+ Filename.concat
+ (Filename.dirname new_prog)
+ (Filename.concat "../Resources/bin" (Filename.basename new_prog))
+ in if Sys.file_exists in_macos_bundle then in_macos_bundle
+ else "coqidetop.opt"
+ with Not_found -> "coqidetop.opt"
+ in file
+
+(* In win32, when a command-line is to be executed via cmd.exe
+ (i.e. Sys.command, Unix.open_process, ...), it cannot contain several
+ quoted "..." zones otherwise some quotes are lost. Solution: we re-quote
+ everything. Reference: http://ss64.com/nt/cmd.html *)
+
+let requote cmd = if Sys.os_type = "Win32" then "\""^cmd^"\"" else cmd
+
+let textview_width (view : #GText.view_skel) =
+ let rect = view#visible_rect in
+ let pixel_width = Gdk.Rectangle.width rect in
+ let metrics = view#misc#pango_context#get_metrics () in
+ let char_width = GPango.to_pixels metrics#approx_char_width in
+ pixel_width / char_width
+
+type logger = Feedback.level -> Pp.t -> unit
+
+let default_logger level message =
+ let level = match level with
+ | Feedback.Debug -> `DEBUG
+ | Feedback.Info -> `INFO
+ | Feedback.Notice -> `NOTICE
+ | Feedback.Warning -> `WARNING
+ | Feedback.Error -> `ERROR
+ in
+ Minilib.log_pp ~level message
+
+
+(** {6 File operations} *)
+
+(** A customized [stat] function. Exceptions are caught. *)
+
+type stats = MTime of float | NoSuchFile | OtherError
+
+let stat f =
+ try MTime (Unix.stat f).Unix.st_mtime
+ with
+ | Unix.Unix_error (Unix.ENOENT,_,_) -> NoSuchFile
+ | _ -> OtherError
+
+(** I/O utilities
+
+ Note: In a mono-thread coqide, we use the same buffer for
+ different read operations *)
+
+let maxread = 4096
+
+let read_string = Bytes.create maxread
+let read_buffer = Buffer.create maxread
+
+(** Read the content of file [f] and add it to buffer [b].
+ I/O Exceptions are propagated. *)
+
+let read_file name buf =
+ let ic = Util.open_utf8_file_in name in
+ let len = ref 0 in
+ try
+ while len := input ic read_string 0 maxread; !len > 0 do
+ Buffer.add_subbytes buf read_string 0 !len
+ done;
+ close_in ic
+ with e -> close_in ic; raise e
+
+(** Read what is available on a gtk channel. This channel should have been
+ set as non-blocking. When there's nothing more to read, the inner loop
+ will be exited via a GError exception concerning a EAGAIN unix error.
+ Anyway, any other exception also stops the read. *)
+
+let io_read_all chan =
+ Buffer.clear read_buffer;
+ let read_once () =
+ let len = Glib.Io.read_chars ~buf:read_string ~pos:0 ~len:maxread chan in
+ Buffer.add_subbytes read_buffer read_string 0 len
+ in
+ begin
+ try while true do read_once () done
+ with Glib.GError _ -> ()
+ end;
+ Buffer.contents read_buffer
+
+(** Run an external command asynchronously *)
+
+let run_command display finally cmd =
+ let cin = Unix.open_process_in cmd in
+ let fd = Unix.descr_of_in_channel cin in
+ let () = Unix.set_nonblock fd in
+ let io_chan = Glib.Io.channel_of_descr fd in
+ let all_conds = [`ERR; `HUP; `IN; `NVAL; `PRI] in (* all except `OUT *)
+ let rec has_errors = function
+ | [] -> false
+ | (`IN | `PRI) :: conds -> has_errors conds
+ | e :: _ -> true
+ in
+ let handle_end () = finally (Unix.close_process_in cin); false
+ in
+ let handle_input conds =
+ if has_errors conds then handle_end ()
+ else
+ let s = io_read_all io_chan in
+ if s = "" then handle_end ()
+ else (display (try_convert s); true)
+ in
+ ignore (Glib.Io.add_watch ~cond:all_conds ~callback:handle_input io_chan)
+
+(** Web browsing *)
+
+let browse prerr url =
+ let com = Util.subst_command_placeholder cmd_browse#get url in
+ let finally = function
+ | Unix.WEXITED 127 ->
+ prerr
+ ("Could not execute:\n"^com^"\n"^
+ "check your preferences for setting a valid browser command\n")
+ | _ -> ()
+ in
+ run_command (fun _ -> ()) finally com
+
+let url_for_keyword =
+ let ht = Hashtbl.create 97 in
+ lazy (
+ begin try
+ let cin =
+ try let index_urls = Filename.concat (List.find
+ (fun x -> Sys.file_exists (Filename.concat x "index_urls.txt"))
+ (Minilib.coqide_data_dirs ())) "index_urls.txt" in
+ open_in index_urls
+ with Not_found -> raise Exit
+ in
+ try while true do
+ let s = input_line cin in
+ try
+ let i = String.index s ',' in
+ let k = String.sub s 0 i in
+ let u = String.sub s (i + 1) (String.length s - i - 1) in
+ Hashtbl.add ht k u
+ with _ ->
+ Minilib.log "Warning: Cannot parse documentation index file."
+ done with End_of_file ->
+ close_in cin
+ with _ ->
+ Minilib.log "Warning: Cannot find documentation index file."
+ end;
+ Hashtbl.find ht : string -> string)
+
+let browse_keyword prerr text =
+ try
+ let u = Lazy.force url_for_keyword text in
+ browse prerr (Coq_config.wwwrefman ^ u)
+ with Not_found -> prerr ("No documentation found for \""^text^"\".\n")
+
+let rec is_valid (s : Pp.t) = match Pp.repr s with
+ | Pp.Ppcmd_empty
+ | Pp.Ppcmd_print_break _
+ | Pp.Ppcmd_force_newline -> true
+ | Pp.Ppcmd_glue l -> List.for_all is_valid l
+ | Pp.Ppcmd_string s -> Glib.Utf8.validate s
+ | Pp.Ppcmd_box (_,s)
+ | Pp.Ppcmd_tag (_,s) -> is_valid s
+ | Pp.Ppcmd_comment s -> List.for_all Glib.Utf8.validate s
+let validate s =
+ if is_valid s then s else Pp.str "This error massage can't be printed."
+
+(** encoding list of strings as a string in a shell-like compatible way:
+ string with spaces and no ' -> '...'
+ string with spaces and ' -> split string into substrings separated with \'
+ ' -> \'
+ \ -> \\
+ *)
+
+let decode_string_list s =
+ let l = String.length s in
+ let fail_backslash () =
+ failwith "Backslash is used to quote single quotes in quoted strings; it should otherwise be doubled" in
+ let rec find_word quoted b i =
+ if i = l then
+ if quoted then failwith "Unmatched single quote"
+ else i
+ else
+ let c = s.[i] in
+ if c = ' ' && not quoted then i+1
+ else if c = '\'' then find_word (not quoted) b (i+1)
+ else if c = '\\' && not quoted then
+ if i = l-1 then fail_backslash ()
+ else
+ let c = s.[i+1] in
+ if c = '\'' || c = '\\' then
+ (Buffer.add_char b c; find_word quoted b (i+2))
+ else fail_backslash ()
+ else
+ (Buffer.add_char b c;
+ find_word quoted b (i+1)) in
+ let rec collect_words i =
+ if i = l then [] else
+ let b = Buffer.create l in
+ let i = find_word false b i in
+ Buffer.contents b :: collect_words i in
+ collect_words 0
+
+let needs_quote s i =
+ (* Tells if there is a space and if a space, before the next single quote *)
+ match CString.index_from_opt s i ' ', CString.index_from_opt s i '\'' with
+ | Some _, None -> true
+ | Some i, Some j -> i < j
+ | _ -> false
+
+let encode_string s =
+ (* Could be optimized so that "a b'c" is "'a b'\'c" rather than "'a b'\''c'" *)
+ let l = String.length s in
+ let b = Buffer.create (l + 10) in
+ let close quoted = if quoted then Buffer.add_char b '\'' in
+ let rec aux quoted i =
+ if i = l then close quoted
+ else
+ let c = s.[i] in
+ if c = '\'' then
+ (close quoted;
+ Buffer.add_string b "\\'";
+ start (i+1))
+ else if c = '\\' && not quoted then
+ (Buffer.add_string b "\\\\"; aux quoted (i+1))
+ else
+ (Buffer.add_char b c; aux quoted (i+1))
+ and start i =
+ let q = needs_quote s i in
+ if q then Buffer.add_char b '\'';
+ aux q i in
+ start 0;
+ Buffer.contents b
+
+let encode_string_list l = String.concat " " (List.map encode_string l)