diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index eff236042..30f5ed5f8 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -48,6 +48,7 @@ public class Euclide.Completion.Parser : GLib.Object { public bool get_for_word (string to_find, out List list) { list = prefix_tree.get_all_matches (to_find); + list.remove_link (list.find_custom (to_find, strcmp)); return list.first () != null; } @@ -86,7 +87,7 @@ public class Euclide.Completion.Parser : GLib.Object { parsing_cancelled = true; } - private bool parse_string (string text) { + public bool parse_string (string text) { parsing_cancelled = false; string [] word_array = text.split_set (DELIMITERS, MAX_TOKENS); foreach (var current_word in word_array ) { @@ -98,4 +99,43 @@ public class Euclide.Completion.Parser : GLib.Object { } return true; } + + public void delete_word (string word, string text) requires (word.length > 0) { + bool match_found = false; + uint word_end_index = word.length - 1; + + // Figure out if another instance of a word in another position before trying to delete it + // from the prefix tree + while (word_end_index > -1 && !match_found) { + match_found = prefix_in_text (word[0:word_end_index], text); + word_end_index--; + } + + // All possible prefixes of the word exist in the source view + if (match_found && word_end_index == word.length - 1) { + return; + } + + uint min_deletion_index = word_end_index + 1; + + lock (prefix_tree) { + prefix_tree.remove (word, (int) min_deletion_index); + } + } + + private bool prefix_in_text (string word, string text) { + // If there are at least two matches then the prefix + // still exists after the modifications made to the source view + + try { + var search_regex = new Regex ("\\b$word\\b"); + GLib.MatchInfo match_info; + search_regex.match_all (text, 0, out match_info); + return match_info.get_match_count () > 1; + } catch (GLib.Error err) { + critical ("Error while attempting regex search of prefix in document text: %s", err.message); + } + + return false; + } } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index d227d12a6..b6c018b53 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -76,7 +76,8 @@ 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.buffer.insert_text.connect (on_insert_text); + current_view.completion.show.connect (() => { completion_in_progress = true; }); @@ -120,53 +121,86 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { return false; } - private bool on_key_press (Gtk.Widget view, Gdk.EventKey event) { - var kv = event.keyval; - /* Pass through any modified keypress except Shift or Capslock */ - Gdk.ModifierType mods = event.state & Gdk.ModifierType.MODIFIER_MASK - & ~Gdk.ModifierType.SHIFT_MASK - & ~Gdk.ModifierType.LOCK_MASK; - if (mods > 0 ) { - /* Default key for USER_REQUESTED completion is ControlSpace - * but this is trapped elsewhere. Control + USER_REQUESTED_KEY acts as an - * alternative and also purges spelling mistakes and unused words from the list. - * If used when a word or part of a word is selected, the selection will be - * used as the word to find. */ - - if ((mods & Gdk.ModifierType.CONTROL_MASK) > 0 && - (kv == REFRESH_SHORTCUT)) { - - parser.rebuild_word_list (current_view); - current_view.show_completion (); - return true; - } + private void on_insert_text (Gtk.TextIter pos, string new_text, int new_text_length) { + + if (new_text.strip () == "") { + return; } - var uc = (unichar)(Gdk.keyval_to_unicode (kv)); - if (!completion_in_progress && Euclide.Completion.Parser.is_delimiter (uc) && - (uc.isprint () || uc.isspace ())) { + bool starts_word = pos.starts_word (); + bool ends_word = pos.ends_word (); + bool between_word = pos.inside_word () && !starts_word && !ends_word; - var buffer = current_view.buffer; - var mark = buffer.get_insert (); - Gtk.TextIter cursor_iter; - buffer.get_iter_at_mark (out cursor_iter, mark); + if (ends_word) { + this.handle_insert_at_phrase_end (pos, new_text, new_text_length); + } else if (between_word) { + this.handle_insert_between_phrase (pos, new_text, new_text_length); + } else { + this.handle_insert_not_at_word_boundary (pos, new_text, new_text_length); + } + } - var word_start = cursor_iter; - Euclide.Completion.Parser.back_to_word_start (ref word_start); + private void handle_insert_between_phrase (Gtk.TextIter pos, string new_text, int new_text_length) { + debug ("word-completion: Text inserted between word.\n"); + var word_start_iter = pos; + word_start_iter.backward_word_start (); + + var word_end_iter = pos; + word_end_iter.forward_word_end (); + + var old_word_to_delete = word_start_iter.get_text (word_end_iter); + parser.delete_word (old_word_to_delete, current_view.buffer.text); + + // Check if new text ends with whitespace + if (ends_with_whitespace (new_text)) { + // The text from the insert postiion to the end of the word needs to be added as its own word + var final_word_end_iter = pos; + final_word_end_iter.forward_word_end (); + + var extra_word_to_add = pos.get_text (final_word_end_iter); + parser.parse_string (extra_word_to_add); + } - string word = buffer.get_text (word_start, cursor_iter, false); - parser.add_word (word); + var full_phrases = word_start_iter.get_text (pos) + new_text; + parser.parse_string (full_phrases); + } + + private bool ends_with_whitespace (string str) { + if (str.length == 0) { + return false; + } + + + if (str.get_char (str.length - 1).isspace ()) { + return true; } return false; } + private void handle_insert_at_phrase_end (Gtk.TextIter pos, string new_text, int new_text_length) { + var text_start_iter = Gtk.TextIter (); + text_start_iter = pos; + text_start_iter.backward_word_start (); + + var text_end_iter = Gtk.TextIter (); + text_end_iter.assign (pos); + text_end_iter.forward_chars (new_text_length - 1); + + var full_phrases = text_start_iter.get_text (text_end_iter) + new_text; + parser.parse_string (full_phrases); + } + + private void handle_insert_not_at_word_boundary (Gtk.TextIter pos, string new_text, int new_text_length) { + parser.parse_string (new_text); + } + 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.buffer.insert_text.disconnect (on_insert_text); current_view.completion.get_providers ().foreach ((p) => { try { diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index ea5ca2d41..14e5fc787 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -2,7 +2,13 @@ namespace Scratch.Plugins { private class PrefixNode : Object { public GLib.List children; - public unichar value { get; set; } + public unichar value { get; construct; } + + public PrefixNode (unichar c = '\0') { + Object ( + value: c + ); + } construct { children = new List (); @@ -17,9 +23,7 @@ namespace Scratch.Plugins { } public void clear () { - root = new PrefixNode () { - value = '\0' - }; + root = new PrefixNode (); } public void insert (string word) { @@ -47,9 +51,7 @@ namespace Scratch.Plugins { } } - var new_child = new PrefixNode () { - value = curr - }; + var new_child = new PrefixNode (curr); node.children.insert_sorted (new_child, (c1, c2) => { if (c1.value > c2.value) { return 1; @@ -63,8 +65,40 @@ namespace Scratch.Plugins { } } + public void remove (string word, int min_deletion_index) { + if (word.length == 0) { + return; + } + + remove_at (word, root, min_deletion_index); + } + + private bool remove_at (string word, PrefixNode node, int min_deletion_index, int char_index = 0) { + unichar curr; + + word.get_next_char (ref char_index, out curr); + if (curr == '\0') { + return true; + } + + foreach (var child in node.children) { + if (child.value == curr) { + bool should_continue = this.remove_at (word, node, min_deletion_index, char_index + 1); + + if (should_continue && child.children.length () == 0) { + node.children.remove (child); + return char_index < min_deletion_index; + } + + break; + } + } + + return false; + } + public bool find_prefix (string prefix) { - return find_prefix_at (prefix, root) != null? true : false; + return find_prefix_at (prefix, root) != null ? true : false; } private PrefixNode? find_prefix_at (string prefix, PrefixNode node, int i = 0) {