diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index 7a9071a793..65bcb4649c 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -28,7 +28,6 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, private Gtk.TextView? view; private Gtk.TextBuffer? buffer; private Euclide.Completion.Parser parser; - private bool proposals_found = false; private Gtk.TextMark completion_end_mark; private Gtk.TextMark completion_start_mark; @@ -53,40 +52,36 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, } public bool match (Gtk.SourceCompletionContext context) { - return true; + Gtk.TextIter start, end; + buffer.get_iter_at_offset (out end, buffer.cursor_position); + start = end.copy (); + start.backward_word_start (); + string text = buffer.get_text (start, end, true); + + return parser.match (text); } public void populate (Gtk.SourceCompletionContext context) { /*Store current insertion point for use in activate_proposal */ GLib.List? file_props; bool no_minimum = (context.get_activation () == Gtk.SourceCompletionActivation.USER_REQUESTED); - proposals_found = get_proposals (out file_props, no_minimum); - - if (proposals_found) - context.add_proposals (this, file_props, true); - - /* Signal to plugin whether proposals are available - * If none, the completion will be active but not visible */ - can_propose (proposals_found); + get_proposals (out file_props, no_minimum); + context.add_proposals (this, file_props, true); } public bool activate_proposal (Gtk.SourceCompletionProposal proposal, Gtk.TextIter iter) { - if (proposals_found) { - /* Count backward from completion_mark instead of iter - * (avoids wrong insertion if the user is typing fast) */ - Gtk.TextIter start; - Gtk.TextIter end; - Gtk.TextMark mark; + Gtk.TextIter start; + Gtk.TextIter end; + Gtk.TextMark mark; - mark = buffer.get_mark (COMPLETION_END_MARK_NAME); - buffer.get_iter_at_mark (out end, mark); + mark = buffer.get_mark (COMPLETION_END_MARK_NAME); + buffer.get_iter_at_mark (out end, mark); - mark = buffer.get_mark (COMPLETION_START_MARK_NAME); - buffer.get_iter_at_mark (out start, mark); + mark = buffer.get_mark (COMPLETION_START_MARK_NAME); + buffer.get_iter_at_mark (out start, mark); - buffer.@delete (ref start, ref end); - buffer.insert (ref start, proposal.get_text (), proposal.get_text ().length); - } + buffer.@delete (ref start, ref end); + buffer.insert (ref start, proposal.get_text (), proposal.get_text ().length); return true; } @@ -95,11 +90,6 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, Gtk.SourceCompletionActivation.USER_REQUESTED; } - public unowned Gtk.Widget? get_info_widget (Gtk.SourceCompletionProposal proposal) { - /* As no additional info is provided no widget is needed */ - return null; - } - public int get_interactive_delay () { return 0; } @@ -116,14 +106,8 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, return true; } - public void update_info (Gtk.SourceCompletionProposal proposal, Gtk.SourceCompletionInfo info) { - /* No additional info provided on proposals */ - return; - } - private bool get_proposals (out GLib.List? props, bool no_minimum) { string to_find = ""; - Gtk.TextIter iter; Gtk.TextBuffer temp_buffer = buffer; props = null; @@ -133,38 +117,28 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, to_find = temp_buffer.get_text (start, end, true); if (to_find.length == 0) { - /* Find start of current word */ - temp_buffer.get_iter_at_offset (out iter, buffer.cursor_position); - /* Mark current insertion point as end point for use in activate proposal */ - buffer.move_mark_by_name (COMPLETION_END_MARK_NAME, iter); - /* TODO - Use iter.backward_word_start? */ - iter.backward_find_char ((c) => { - bool valid = parser.is_delimiter (c); - if (!valid) - to_find += c.to_string (); - - return valid; - }, null); - iter.forward_cursor_position (); - /* Mark start of delimited text as start point for use in activate proposal */ - buffer.move_mark_by_name (COMPLETION_START_MARK_NAME, iter); - to_find = to_find.reverse (); - } else { - /* mark start and end of the selection */ - buffer.move_mark_by_name (COMPLETION_END_MARK_NAME, end); - buffer.move_mark_by_name (COMPLETION_START_MARK_NAME, start); + temp_buffer.get_iter_at_offset (out end, buffer.cursor_position); + + start = end; + start.backward_word_start (); + + to_find = buffer.get_text (start, end, false); } + buffer.move_mark_by_name (COMPLETION_END_MARK_NAME, end); + buffer.move_mark_by_name (COMPLETION_START_MARK_NAME, start); + + /* There is no minimum length of word to find if the user requested a completion */ if (no_minimum || to_find.length >= Euclide.Completion.Parser.MINIMUM_WORD_LENGTH) { /* Get proposals, if any */ - Gee.TreeSet prop_word_list; + List prop_word_list; if (parser.get_for_word (to_find, out prop_word_list)) { foreach (var word in prop_word_list) { var item = new Gtk.SourceCompletionItem (); item.label = word; item.text = word; - props.prepend (item); + props.append (item); } return true; diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 55273ffa29..9d73598dc3 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -22,74 +22,58 @@ public class Euclide.Completion.Parser : GLib.Object { public const int MINIMUM_WORD_LENGTH = 1; public const int MAX_TOKENS = 1000000; - public const string DELIMITERS = " .,;:?{}[]()0123456789+-=&|-<>*\\/\n\t\'\""; - public bool is_delimiter (unichar c) { + private Scratch.Plugins.PrefixTree prefix_tree; + + public const string DELIMITERS = " .,;:?{}[]()0123456789+=&|<>*\\/\r\n\t\'\"`"; + public static bool is_delimiter (unichar c) { return DELIMITERS.index_of_char (c) >= 0; } - public Gee.HashMap> text_view_words; + public Gee.HashMap text_view_words; public bool parsing_cancelled = false; - private Gee.ArrayList words; - private string last_word = ""; - public Parser () { - text_view_words = new Gee.HashMap> (); + text_view_words = new Gee.HashMap (); + prefix_tree = new Scratch.Plugins.PrefixTree (); } - public void add_last_word () { - add_word (last_word); + public bool match (string to_find) { + return prefix_tree.find_prefix (to_find); } - public bool get_for_word (string to_find, out Gee.TreeSet list) { - uint length = to_find.length; - list = new Gee.TreeSet (); - last_word = to_find; - if (words != null) { - lock (words) { - foreach (var word in words) { - if (word.length > length && word.slice (0, length) == to_find) { - list.add (word); - } - } - } - } - - return !list.is_empty; + public bool get_for_word (string to_find, out List list) { + list = prefix_tree.get_all_matches (to_find); + return list.first () != null; } public void rebuild_word_list (Gtk.TextView view) { - lock (words) { - words.clear (); - } + prefix_tree.clear (); parse_text_view (view); } public void parse_text_view (Gtk.TextView view) { /* If this view has already been parsed, restore the word list */ - lock (words) { + lock (prefix_tree) { if (text_view_words.has_key (view)) { - words = text_view_words.@get (view); + prefix_tree = text_view_words.@get (view); } else { - /* Else create a new word list and parse the buffer text */ - words = new Gee.ArrayList (); + /* Else create a new word list and parse the buffer text */ + prefix_tree = new Scratch.Plugins.PrefixTree (); } } if (view.buffer.text.length > 0) { parse_string (view.buffer.text); - text_view_words.@set (view, words); + text_view_words.@set (view, prefix_tree); } } - private void add_word (string word) { + public void add_word (string word) { if (word.length < MINIMUM_WORD_LENGTH) return; - if (!(word in words)) { - lock (words) { - words.add (word); - } + lock (prefix_tree) { + prefix_tree.insert (word); } } diff --git a/plugins/word-completion/meson.build b/plugins/word-completion/meson.build index 6aa1ed0225..2887aef312 100644 --- a/plugins/word-completion/meson.build +++ b/plugins/word-completion/meson.build @@ -1,6 +1,7 @@ module_name = 'word-completion' module_files = [ + 'prefix-tree.vala', 'completion-provider.vala', 'engine.vala', 'plugin.vala' diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 0debc5c6b1..4a32dd093c 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -28,6 +28,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { private MainWindow main_window; private Scratch.Services.Interface plugins; + private bool completion_in_progress = false; private const uint [] ACTIVATE_KEYS = { Gdk.Key.Return, @@ -41,7 +42,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { private const uint USER_REQUESTED_KEY = Gdk.Key.backslash; private uint timeout_id = 0; - private bool completion_visible = false; public void activate () { plugins = (Scratch.Services.Interface) object; @@ -77,8 +77,13 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_document = doc; current_view = doc.source_view; current_view.key_press_event.connect (on_key_press); - current_view.completion.show.connect (on_completion_shown); - current_view.completion.hide.connect (on_completion_hidden); + current_view.completion.show.connect (() => { + completion_in_progress = true; + }); + current_view.completion.hide.connect (() => { + completion_in_progress = false; + }); + if (text_view_list.find (current_view) == null) text_view_list.append (current_view); @@ -86,11 +91,10 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { var comp_provider = new Scratch.Plugins.CompletionProvider (this); comp_provider.priority = 1; comp_provider.name = provider_name_from_document (doc); - comp_provider.can_propose.connect (on_propose); try { current_view.completion.add_provider (comp_provider); - current_view.completion.show_headers = false; + current_view.completion.show_headers = true; current_view.completion.show_icons = true; /* Wait a bit to allow text to load then run parser*/ timeout_id = Timeout.add (1000, on_timeout_update); @@ -134,49 +138,33 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { parser.rebuild_word_list (current_view); current_view.show_completion (); return true; - } else - return false; + } } - bool activating = kv in ACTIVATE_KEYS; + if (!completion_in_progress && parser.is_delimiter (uc) && + (uc.isprint () || uc.isspace ())) { - if (completion_visible && activating) { - current_view.completion.activate_proposal (); - parser.add_last_word (); - return true; - } + var buffer = current_view.buffer; + var mark = buffer.get_insert (); + Gtk.TextIter cursor_iter; + buffer.get_iter_at_mark (out cursor_iter, mark); + + var word_start = cursor_iter; + word_start.backward_word_start (); - if (activating || (uc.isprint () && parser.is_delimiter (uc) )) { - parser.add_last_word (); - current_view.completion.hide (); + string word = buffer.get_text (word_start, cursor_iter, false); + parser.add_word (word); } return false; } - private void on_completion_shown () { - completion_visible = true; - } - - private void on_completion_hidden () { - completion_visible = false; - } - - private void on_propose (bool can_propose) { - if (!can_propose) - current_view.completion.hide (); - - completion_visible = can_propose; - } - private string provider_name_from_document (Scratch.Services.Document doc) { return _("%s - Word Completion").printf (doc.get_basename ()); } private void cleanup (Gtk.SourceView view) { current_view.key_press_event.disconnect (on_key_press); - current_view.completion.show.disconnect (on_completion_shown); - current_view.completion.hide.disconnect (on_completion_hidden); current_view.completion.get_providers ().foreach ((p) => { try { @@ -189,8 +177,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { warning (e.message); } }); - - completion_visible = false; } } diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala new file mode 100644 index 0000000000..ea5ca2d414 --- /dev/null +++ b/plugins/word-completion/prefix-tree.vala @@ -0,0 +1,115 @@ + +namespace Scratch.Plugins { + private class PrefixNode : Object { + public GLib.List children; + public unichar value { get; set; } + + construct { + children = new List (); + } + } + + public class PrefixTree : Object { + private PrefixNode root; + + construct { + clear (); + } + + public void clear () { + root = new PrefixNode () { + value = '\0' + }; + } + + public void insert (string word) { + if (word.length == 0) { + return; + } + + this.insert_at (word, this.root); + } + + private void insert_at (string word, PrefixNode node, int i = 0) { + unichar curr = '\0'; + + bool has_next_character = false; + do { + has_next_character = word.get_next_char (ref i, out curr); + } while (has_next_character && Euclide.Completion.Parser.is_delimiter (curr)); + + foreach (var child in node.children) { + if (child.value == curr) { + if (curr != '\0') { + insert_at (word, child, i); + } + return; + } + } + + var new_child = new PrefixNode () { + value = curr + }; + node.children.insert_sorted (new_child, (c1, c2) => { + if (c1.value > c2.value) { + return 1; + } else if (c1.value == c2.value) { + return 0; + } + return -1; + }); + if (curr != '\0') { + insert_at (word, new_child, i); + } + } + + public bool find_prefix (string prefix) { + return find_prefix_at (prefix, root) != null? true : false; + } + + private PrefixNode? find_prefix_at (string prefix, PrefixNode node, int i = 0) { + unichar curr; + + prefix.get_next_char (ref i, out curr); + if (curr == '\0') { + return node; + } + + foreach (var child in node.children) { + if (child.value == curr) { + return find_prefix_at (prefix, child, i); + } + } + + return null; + } + + public List get_all_matches (string prefix) { + var list = new List (); + var node = find_prefix_at (prefix, root, 0); + if (node != null) { + var sb = new StringBuilder (prefix); + get_all_matches_rec (node, ref sb, ref list); + } + + return list; + } + + private void get_all_matches_rec ( + PrefixNode node, + ref StringBuilder sbuilder, + ref List matches) { + + foreach (var child in node.children) { + if (child.value == '\0') { + matches.append (sbuilder.str); + } else { + sbuilder.append_unichar (child.value); + get_all_matches_rec (child, ref sbuilder, ref matches); + var length = child.value.to_string ().length; + sbuilder.erase (sbuilder.len - length, -1); + } + } + } + } +}