summaryrefslogtreecommitdiff
path: root/ide/wg_Completion.ml
diff options
context:
space:
mode:
authorGravatar Enrico Tassi <gareuselesinge@debian.org>2015-01-25 14:42:51 +0100
committerGravatar Enrico Tassi <gareuselesinge@debian.org>2015-01-25 14:42:51 +0100
commit7cfc4e5146be5666419451bdd516f1f3f264d24a (patch)
treee4197645da03dc3c7cc84e434cc31d0a0cca7056 /ide/wg_Completion.ml
parent420f78b2caeaaddc6fe484565b2d0e49c66888e5 (diff)
Imported Upstream version 8.5~beta1+dfsg
Diffstat (limited to 'ide/wg_Completion.ml')
-rw-r--r--ide/wg_Completion.ml453
1 files changed, 453 insertions, 0 deletions
diff --git a/ide/wg_Completion.ml b/ide/wg_Completion.ml
new file mode 100644
index 00000000..3f5ae4bd
--- /dev/null
+++ b/ide/wg_Completion.ml
@@ -0,0 +1,453 @@
+(************************************************************************)
+(* v * The Coq Proof Assistant / The Coq Development Team *)
+(* <O___,, * INRIA - CNRS - LIX - LRI - PPS - Copyright 1999-2015 *)
+(* \VV/ **************************************************************)
+(* // * This file is distributed under the terms of the *)
+(* * GNU Lesser General Public License Version 2.1 *)
+(************************************************************************)
+
+module StringOrd =
+struct
+ type t = string
+ let compare s1 s2 =
+ (* we use first size, then usual comparison *)
+ let d = String.length s1 - String.length s2 in
+ if d <> 0 then d
+ else Pervasives.compare s1 s2
+end
+
+module Proposals = Set.Make(StringOrd)
+
+(** Retrieve completion proposals in the buffer *)
+let get_syntactic_completion (buffer : GText.buffer) pattern accu =
+ let rec get_aux accu (iter : GText.iter) =
+ match iter#forward_search pattern with
+ | None -> accu
+ | Some (start, stop) ->
+ if Gtk_parsing.starts_word start then
+ let ne = Gtk_parsing.find_word_end stop in
+ if ne#compare stop = 0 then get_aux accu stop
+ else
+ let proposal = buffer#get_text ~start ~stop:ne () in
+ let accu = Proposals.add proposal accu in
+ get_aux accu stop
+ else get_aux accu stop
+ in
+ get_aux accu buffer#start_iter
+
+(** Retrieve completion proposals in Coq libraries *)
+let get_semantic_completion pattern accu =
+ let flags = [Interface.Name_Pattern ("^" ^ pattern), true] in
+ (** Only get the last part of the qualified name *)
+ let rec last accu = function
+ | [] -> accu
+ | [basename] -> Proposals.add basename accu
+ | _ :: l -> last accu l
+ in
+ let next = function
+ | Interface.Good l ->
+ let fold accu elt = last accu elt.Interface.coq_object_qualid in
+ let ans = List.fold_left fold accu l in
+ Coq.return ans
+ | _ -> Coq.return accu
+ in
+ Coq.bind (Coq.search flags) next
+
+let is_substring s1 s2 =
+ let s1 = Glib.Utf8.to_unistring s1 in
+ let s2 = Glib.Utf8.to_unistring s2 in
+ let break = ref true in
+ let i = ref 0 in
+ let len1 = Array.length s1 in
+ let len2 = Array.length s2 in
+ while !break && !i < len1 && !i < len2 do
+ break := s1.(!i) = s2.(!i);
+ incr i;
+ done;
+ if !break then len2 - len1
+ else -1
+
+class type complete_model_signals =
+ object ('a)
+ method after : 'a
+ method disconnect : GtkSignal.id -> unit
+ method start_completion : callback:(int -> unit) -> GtkSignal.id
+ method update_completion : callback:(int * string * Proposals.t -> unit) -> GtkSignal.id
+ method end_completion : callback:(unit -> unit) -> GtkSignal.id
+ end
+
+let complete_model_signals
+ (start_s : int GUtil.signal)
+ (update_s : (int * string * Proposals.t) GUtil.signal)
+ (end_s : unit GUtil.signal) : complete_model_signals =
+let signals = [
+ start_s#disconnect;
+ update_s#disconnect;
+ end_s#disconnect;
+] in
+object (self : 'a)
+ inherit GUtil.ml_signals signals as super
+ method start_completion = start_s#connect ~after
+ method update_completion = update_s#connect ~after
+ method end_completion = end_s#connect ~after
+end
+
+class complete_model coqtop (buffer : GText.buffer) =
+ let cols = new GTree.column_list in
+ let column = cols#add Gobject.Data.string in
+ let store = GTree.list_store cols in
+ let filtered_store = GTree.model_filter store in
+ let start_completion_signal = new GUtil.signal () in
+ let update_completion_signal = new GUtil.signal () in
+ let end_completion_signal = new GUtil.signal () in
+object (self)
+
+ val signals = complete_model_signals
+ start_completion_signal update_completion_signal end_completion_signal
+ val mutable active = false
+ val mutable auto_complete_length = 3
+ (* this variable prevents CoqIDE from autocompleting when we have deleted something *)
+ val mutable is_auto_completing = false
+ (* this mutex ensure that CoqIDE will not try to autocomplete twice *)
+ val mutable cache = (-1, "", Proposals.empty)
+ val mutable insert_offset = -1
+ val mutable current_completion = ("", Proposals.empty)
+ val mutable lock_auto_completing = true
+
+ method connect = signals
+
+ method active = active
+
+ method set_active b = active <- b
+
+ method private handle_insert iter s =
+ (* we're inserting, so we may autocomplete *)
+ is_auto_completing <- true
+
+ method private handle_delete ~start ~stop =
+ (* disable autocomplete *)
+ is_auto_completing <- false
+
+ method store = filtered_store
+
+ method column = column
+
+ method handle_proposal path =
+ let row = filtered_store#get_iter path in
+ let proposal = filtered_store#get ~row ~column in
+ let (start_offset, _, _) = cache in
+ (* [iter] might be invalid now, get a new one to please gtk *)
+ let iter = buffer#get_iter `INSERT in
+ (* We cancel completion when the buffer has changed recently *)
+ if iter#offset = insert_offset then begin
+ let suffix =
+ let len1 = String.length proposal in
+ let len2 = insert_offset - start_offset in
+ String.sub proposal len2 (len1 - len2)
+ in
+ buffer#begin_user_action ();
+ ignore (buffer#insert_interactive ~iter suffix);
+ buffer#end_user_action ();
+ end
+
+ method private init_proposals pref props =
+ let () = store#clear () in
+ let iter prop =
+ let iter = store#append () in
+ store#set iter column prop
+ in
+ let () = current_completion <- (pref, props) in
+ Proposals.iter iter props
+
+ method private update_proposals pref =
+ let (_, _, props) = cache in
+ let filter prop = 0 <= is_substring pref prop in
+ let props = Proposals.filter filter props in
+ let () = current_completion <- (pref, props) in
+ let () = filtered_store#refilter () in
+ props
+
+ method private do_auto_complete k =
+ let iter = buffer#get_iter `INSERT in
+ let () = insert_offset <- iter#offset in
+ let log = Printf.sprintf "Completion at offset: %i" insert_offset in
+ let () = Minilib.log log in
+ let prefix =
+ if Gtk_parsing.ends_word iter#backward_char then
+ let start = Gtk_parsing.find_word_start iter in
+ let w = buffer#get_text ~start ~stop:iter () in
+ if String.length w >= auto_complete_length then Some (w, start)
+ else None
+ else None
+ in
+ match prefix with
+ | Some (w, start) ->
+ let () = Minilib.log ("Completion of prefix: '" ^ w ^ "'") in
+ let (off, prefix, props) = cache in
+ let start_offset = start#offset in
+ (* check whether we have the last request in cache *)
+ if (start_offset = off) && (0 <= is_substring prefix w) then
+ let props = self#update_proposals w in
+ let () = update_completion_signal#call (start_offset, w, props) in
+ k ()
+ else
+ let () = start_completion_signal#call start_offset in
+ let update props =
+ let () = cache <- (start_offset, w, props) in
+ let () = self#init_proposals w props in
+ update_completion_signal#call (start_offset, w, props)
+ in
+ (** If not in the cache, we recompute it: first syntactic *)
+ let synt = get_syntactic_completion buffer w Proposals.empty in
+ (** Then semantic *)
+ let next prop =
+ let () = update prop in
+ Coq.lift k
+ in
+ let query = Coq.bind (get_semantic_completion w synt) next in
+ (** If coqtop is computing, do the syntactic completion altogether *)
+ let occupied () =
+ let () = update synt in
+ k ()
+ in
+ Coq.try_grab coqtop query occupied
+ | None -> end_completion_signal#call (); k ()
+
+ method private may_auto_complete () =
+ if active && is_auto_completing && lock_auto_completing then begin
+ let () = lock_auto_completing <- false in
+ let unlock () = lock_auto_completing <- true in
+ self#do_auto_complete unlock
+ end
+
+ initializer
+ let filter_prop model row =
+ let (_, props) = current_completion in
+ let prop = store#get ~row ~column in
+ Proposals.mem prop props
+ in
+ let () = filtered_store#set_visible_func filter_prop in
+ (* Install auto-completion *)
+ ignore (buffer#connect#insert_text ~callback:self#handle_insert);
+ ignore (buffer#connect#delete_range ~callback:self#handle_delete);
+ ignore (buffer#connect#after#end_user_action ~callback:self#may_auto_complete);
+
+end
+
+class complete_popup (model : complete_model) (view : GText.view) =
+ let obj = GWindow.window ~kind:`POPUP ~show:false () in
+ let frame = GBin.scrolled_window
+ ~hpolicy:`NEVER ~vpolicy:`NEVER
+ ~shadow_type:`OUT ~packing:obj#add ()
+ in
+(* let frame = GBin.frame ~shadow_type:`OUT ~packing:obj#add () in *)
+ let data = GTree.view
+ ~vadjustment:frame#vadjustment ~hadjustment:frame#hadjustment
+ ~rules_hint:true ~headers_visible:false
+ ~model:model#store ~packing:frame#add ()
+ in
+ let renderer = GTree.cell_renderer_text [], ["text", model#column] in
+ let col = GTree.view_column ~renderer () in
+ let _ = data#append_column col in
+ let () = col#set_sizing `AUTOSIZE in
+ let page_size = 16 in
+
+object (self)
+
+ method coerce = view#coerce
+
+ method private refresh_style () =
+ let (renderer, _) = renderer in
+ let font = Preferences.current.Preferences.text_font in
+ renderer#set_properties [`FONT_DESC font; `XPAD 10]
+
+ method private coordinates pos =
+ (** Toplevel position w.r.t. screen *)
+ let (x, y) = Gdk.Window.get_position view#misc#toplevel#misc#window in
+ (** Position of view w.r.t. window *)
+ let (ux, uy) = Gdk.Window.get_position view#misc#window in
+ (** Relative buffer position to view *)
+ let (dx, dy) = view#window_to_buffer_coords `WIDGET 0 0 in
+ (** Iter position *)
+ let iter = view#buffer#get_iter pos in
+ let coords = view#get_iter_location iter in
+ let lx = Gdk.Rectangle.x coords in
+ let ly = Gdk.Rectangle.y coords in
+ let w = Gdk.Rectangle.width coords in
+ let h = Gdk.Rectangle.height coords in
+ (** Absolute position *)
+ (x + lx + ux - dx, y + ly + uy - dy, w, h)
+
+ method private select_any f =
+ let sel = data#selection#get_selected_rows in
+ let path = match sel with
+ | [] ->
+ begin match model#store#get_iter_first with
+ | None -> None
+ | Some iter -> Some (model#store#get_path iter)
+ end
+ | path :: _ -> Some path
+ in
+ match path with
+ | None -> ()
+ | Some path ->
+ let path = f path in
+ let _ = data#selection#select_path path in
+ data#scroll_to_cell ~align:(0.,0.) path col
+
+ method private select_previous () =
+ let prev path =
+ let copy = GTree.Path.copy path in
+ if GTree.Path.prev path then path
+ else copy
+ in
+ self#select_any prev
+
+ method private select_next () =
+ let next path =
+ let () = GTree.Path.next path in
+ path
+ in
+ self#select_any next
+
+ method private select_previous_page () =
+ let rec up i path =
+ if i = 0 then path
+ else
+ let copy = GTree.Path.copy path in
+ let has_prev = GTree.Path.prev path in
+ if has_prev then up (pred i) path
+ else copy
+ in
+ self#select_any (up page_size)
+
+ method private select_next_page () =
+ let rec down i path =
+ if i = 0 then path
+ else
+ let copy = GTree.Path.copy path in
+ let iter = model#store#get_iter path in
+ let has_next = model#store#iter_next iter in
+ if has_next then down (pred i) (model#store#get_path iter)
+ else copy
+ in
+ self#select_any (down page_size)
+
+ method private select_first () =
+ let rec up path =
+ let copy = GTree.Path.copy path in
+ let has_prev = GTree.Path.prev path in
+ if has_prev then up path
+ else copy
+ in
+ self#select_any up
+
+ method private select_last () =
+ let rec down path =
+ let copy = GTree.Path.copy path in
+ let iter = model#store#get_iter path in
+ let has_next = model#store#iter_next iter in
+ if has_next then down (model#store#get_path iter)
+ else copy
+ in
+ self#select_any down
+
+ method private select_enter () =
+ let sel = data#selection#get_selected_rows in
+ match sel with
+ | [] -> ()
+ | path :: _ ->
+ let () = model#handle_proposal path in
+ self#hide ()
+
+ method proposal =
+ let sel = data#selection#get_selected_rows in
+ if obj#misc#visible then match sel with
+ | [] -> None
+ | path :: _ ->
+ let row = model#store#get_iter path in
+ let column = model#column in
+ let proposal = model#store#get ~row ~column in
+ Some proposal
+ else None
+
+ method private manage_scrollbar () =
+ (** HACK: we don't have access to the treeview size because of the lack of
+ LablGTK binding for certain functions, so we bypass it by approximating
+ it through the size of the proposals *)
+ let height = match model#store#get_iter_first with
+ | None -> -1
+ | Some iter ->
+ let path = model#store#get_path iter in
+ let area = data#get_cell_area ~path ~col () in
+ let height = Gdk.Rectangle.height area in
+ let height = page_size * height in
+ height
+ in
+ let len = ref 0 in
+ let () = model#store#foreach (fun _ _ -> incr len; false) in
+ if !len > page_size then
+ let () = frame#set_vpolicy `ALWAYS in
+ data#misc#set_size_request ~height ()
+ else
+ data#misc#set_size_request ~height:(-1) ()
+
+ method private refresh () =
+ let () = frame#set_vpolicy `NEVER in
+ let () = self#select_first () in
+ let () = obj#misc#show () in
+ let () = self#manage_scrollbar () in
+ obj#resize 1 1
+
+ method private start_callback off =
+ let (x, y, w, h) = self#coordinates (`OFFSET off) in
+ let () = obj#move x (y + 3 * h / 2) in
+ ()
+
+ method private update_callback (off, word, props) =
+ if Proposals.is_empty props then self#hide ()
+ else if Proposals.mem word props then self#hide ()
+ else self#refresh ()
+
+ method private end_callback () =
+ obj#misc#hide ()
+
+ method private hide () = self#end_callback ()
+
+ initializer
+ let move_cb _ _ ~extend = self#hide () in
+ let key_cb ev =
+ let eval cb = cb (); true in
+ let ev_key = GdkEvent.Key.keyval ev in
+ if obj#misc#visible then
+ if ev_key = GdkKeysyms._Up then eval self#select_previous
+ else if ev_key = GdkKeysyms._Down then eval self#select_next
+ else if ev_key = GdkKeysyms._Tab then eval self#select_enter
+ else if ev_key = GdkKeysyms._Return then eval self#select_enter
+ else if ev_key = GdkKeysyms._Escape then eval self#hide
+ else if ev_key = GdkKeysyms._Page_Down then eval self#select_next_page
+ else if ev_key = GdkKeysyms._Page_Up then eval self#select_previous_page
+ else if ev_key = GdkKeysyms._Home then eval self#select_first
+ else if ev_key = GdkKeysyms._End then eval self#select_last
+ else false
+ else false
+ in
+ (** Style handling *)
+ let _ = view#misc#connect#style_set self#refresh_style in
+ let _ = self#refresh_style () in
+ let _ = data#set_resize_mode `PARENT in
+ let _ = frame#set_resize_mode `PARENT in
+ (** Callback to model *)
+ let _ = model#connect#start_completion self#start_callback in
+ let _ = model#connect#update_completion self#update_callback in
+ let _ = model#connect#end_completion self#end_callback in
+ (** Popup interaction *)
+ let _ = view#event#connect#key_press key_cb in
+ (** Hiding the popup when necessary*)
+ let _ = view#misc#connect#hide obj#misc#hide in
+ let _ = view#event#connect#button_press (fun _ -> self#hide (); false) in
+ let _ = view#connect#move_cursor move_cb in
+ let _ = view#event#connect#focus_out (fun _ -> self#hide (); false) in
+ ()
+
+end