diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index d9abbc7b77..e02445b683 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -1,4 +1,5 @@ /* + * Copyright 2024 elementary, Inc. * Copyright (c) 2013 Mario Guerriero * * This is a free software; you can redistribute it and/or @@ -25,43 +26,63 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, public const string COMPLETION_END_MARK_NAME = "ScratchWordCompletionEnd"; public const string COMPLETION_START_MARK_NAME = "ScratchWordCompletionStart"; - private Gtk.TextView? view; - private Gtk.TextBuffer? buffer; - private Euclide.Completion.Parser parser; + public Gtk.TextView? view { get; construct; } + public Euclide.Completion.Parser parser { get; construct; } + + private unowned Gtk.TextBuffer buffer { + get { + return view.buffer; + } + } + private Gtk.TextMark completion_end_mark; private Gtk.TextMark completion_start_mark; + private string current_text_to_find = ""; public signal void can_propose (bool b); - public CompletionProvider (Scratch.Plugins.Completion completion) { - this.view = completion.current_view as Gtk.TextView; - this.buffer = completion.current_view.buffer; - this.parser = completion.parser; + public CompletionProvider ( + Euclide.Completion.Parser _parser, + Gtk.TextView _view + ) { + + Object ( + parser: _parser, + view: _view + ); + } + + construct { Gtk.TextIter iter; - buffer.get_iter_at_offset (out iter, 0); + view.buffer.get_iter_at_offset (out iter, 0); completion_end_mark = buffer.create_mark (COMPLETION_END_MARK_NAME, iter, false); completion_start_mark = buffer.create_mark (COMPLETION_START_MARK_NAME, iter, false); } - public string get_name () { + public override string get_name () { return this.name; } - public int get_priority () { + public override int get_priority () { return this.priority; } - public bool match (Gtk.SourceCompletionContext context) { - Gtk.TextIter start, end; - buffer.get_iter_at_offset (out end, buffer.cursor_position); - start = end.copy (); - Euclide.Completion.Parser.back_to_word_start (ref start); - string text = buffer.get_text (start, end, true); + public override bool match (Gtk.SourceCompletionContext context) { + int end_pos = buffer.cursor_position; + int start_pos = end_pos; + bool found = false; + var text = buffer.text; - return parser.match (text); + var preceding_word = parser.get_word_immediately_before (text, start_pos); + if (preceding_word != "") { + found = parser.match (preceding_word); + current_text_to_find = found ? preceding_word : ""; + } + + return found; } - public void populate (Gtk.SourceCompletionContext context) { + public override 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); @@ -69,40 +90,45 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, context.add_proposals (this, file_props, true); } - public bool activate_proposal (Gtk.SourceCompletionProposal proposal, Gtk.TextIter iter) { + public override bool activate_proposal (Gtk.SourceCompletionProposal proposal, Gtk.TextIter iter) { Gtk.TextIter start; - Gtk.TextIter end; + Gtk.TextIter end_iter; Gtk.TextMark mark; mark = buffer.get_mark (COMPLETION_END_MARK_NAME); - buffer.get_iter_at_mark (out end, mark); + buffer.get_iter_at_mark (out end_iter, mark); + + // If inserting in middle of word then completion overwrites end of word + var end_pos = end_iter.get_offset (); + var text = buffer.text; + var following_word = parser.get_word_immediately_after (text, end_pos); + if (following_word != "") { + buffer.get_iter_at_offset (out end_iter, end_pos + following_word.length); + } mark = buffer.get_mark (COMPLETION_START_MARK_NAME); buffer.get_iter_at_mark (out start, mark); - buffer.@delete (ref start, ref end); + buffer.@delete (ref start, ref end_iter); buffer.insert (ref start, proposal.get_text (), proposal.get_text ().length); return true; } - public Gtk.SourceCompletionActivation get_activation () { + public override Gtk.SourceCompletionActivation get_activation () { return Gtk.SourceCompletionActivation.INTERACTIVE | Gtk.SourceCompletionActivation.USER_REQUESTED; } - public int get_interactive_delay () { - return 0; + public override int get_interactive_delay () { + return 500; } - public bool get_start_iter (Gtk.SourceCompletionContext context, + public override bool get_start_iter (Gtk.SourceCompletionContext context, Gtk.SourceCompletionProposal proposal, out Gtk.TextIter iter) { - var mark = buffer.get_insert (); - Gtk.TextIter cursor_iter; - buffer.get_iter_at_mark (out cursor_iter, mark); - iter = cursor_iter; - Euclide.Completion.Parser.back_to_word_start (ref iter); + var word_start = buffer.cursor_position; + buffer.get_iter_at_offset (out iter, word_start); return true; } @@ -118,32 +144,35 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, to_find = temp_buffer.get_text (start, end, true); if (to_find.length == 0) { - temp_buffer.get_iter_at_offset (out end, buffer.cursor_position); - - start = end; - Euclide.Completion.Parser.back_to_word_start (ref start); - - to_find = buffer.get_text (start, end, false); + to_find = current_text_to_find; } 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) { + if (no_minimum || to_find.length >= Euclide.Completion.Parser.MINIMUM_PREFIX_LENGTH) { /* Get proposals, if any */ - 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.append (item); + var completions = parser.get_completions_for_prefix (to_find); + if (completions.length () > 0) { + foreach (var completion in completions) { + if (completion.length > 0) { + var item = new Gtk.SourceCompletionItem (); + var word = to_find + completion; + item.label = word; + item.text = completion; + props.append (item); + } } return true; } } + return false; } + + // private bool is_delimiter (unichar uc) { + // return Euclide.Completion.Parser.is_delimiter (uc); + // } } diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index eff236042f..62f68ac6bf 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -1,6 +1,7 @@ /* - * Copyright (c) 2011 Lucas Baudin - * + * Copyright 2024 elementary, Inc. + * 2011 Lucas Baudin + * * * This is a free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the @@ -19,83 +20,351 @@ */ public class Euclide.Completion.Parser : GLib.Object { - public const int MINIMUM_WORD_LENGTH = 1; - public const int MAX_TOKENS = 1000000; + private class WordOccurrence { + private int occurrences; + + public WordOccurrence () { + occurrences = 1; + } - private Scratch.Plugins.PrefixTree prefix_tree; + public void increment () { + occurrences++; + } - public const string DELIMITERS = " .,;:?{}[]()0123456789+=&|<>*\\/\r\n\t\'\"`"; - public static bool is_delimiter (unichar c) { - return DELIMITERS.index_of_char (c) >= 0; + public void decrement () { + if (occurrences > 0) { + occurrences--; + } + } + + public bool occurs () { + return occurrences > 0; + } + } + // DELIMITERS used for word completion are not necessarily the same Pango word breaks + // Therefore, only use string.split_set () to identify words + public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t`\"\'"; + public const int MINIMUM_WORD_LENGTH = 4; + public const int MAXIMUM_WORD_LENGTH = 45; + public const int MINIMUM_PREFIX_LENGTH = 2; + public const int MAX_TOKENS = 100000; + public const string COMPLETED = "Completed"; + public const string WORDS_TO_REMOVE = "WordsToRemove"; + public static bool is_delimiter (unichar? uc) { + return uc == null || DELIMITERS.index_of_char (uc) > -1; } - public static void back_to_word_start (ref Gtk.TextIter iter) { - iter.backward_find_char (is_delimiter, null); - iter.forward_char (); + // Custom function required so that the tail_map iterator returns all words that have + // the target word as a prefix + public static int compare_words (string word_a, string word_b) { + // Only compare the first n characters + // Must return 0 when strings are the same! + var la = word_a.length; + var l_diff = word_b.length - la; + var scomp = Posix.strncmp (word_a, word_b, la); + if (scomp == 0 && l_diff > 0) { + return -l_diff; + } + + return scomp; } - public Gee.HashMap text_view_words; + private Gee.TreeMap? current_tree = null; + public Gee.HashMap text_view_words; public bool parsing_cancelled = false; public Parser () { - text_view_words = new Gee.HashMap (); - prefix_tree = new Scratch.Plugins.PrefixTree (); + text_view_words = new Gee.HashMap (); } - public bool match (string to_find) { - return prefix_tree.find_prefix (to_find); + public bool select_current_tree (Gtk.TextView view) { + bool pre_existing = true; + if (!text_view_words.has_key (view)) { + var new_treemap = new Gee.TreeMap (compare_words, null); + new_treemap.set_data (COMPLETED, false); + new_treemap.set_data> (WORDS_TO_REMOVE, new Gee.LinkedList ()); + text_view_words.@set (view, new_treemap); + pre_existing = false; + } + + lock (current_tree) { + current_tree = text_view_words.@get (view); + parsing_cancelled = false; + } + + return pre_existing && get_initial_parsing_completed (); } - 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 set_initial_parsing_completed (bool completed) requires (current_tree != null) { + lock (current_tree) { + current_tree.set_data (COMPLETED, completed); + } } - public void rebuild_word_list (Gtk.TextView view) { - prefix_tree.clear (); - parse_text_view (view); + public bool get_initial_parsing_completed () requires (current_tree != null) { + return current_tree.get_data (COMPLETED); } - public void parse_text_view (Gtk.TextView view) { - /* If this view has already been parsed, restore the word list */ - lock (prefix_tree) { - if (text_view_words.has_key (view)) { - prefix_tree = text_view_words.@get (view); - } else { - /* Else create a new word list and parse the buffer text */ - prefix_tree = new Scratch.Plugins.PrefixTree (); - } + public void initial_parse_buffer_text (string buffer_text) { + parsing_cancelled = false; + clear (); + if (buffer_text.length > 0) { + var parsed = parse_text_and_add (buffer_text); + set_initial_parsing_completed (parsed); + } else { + // Assume any buffer text would have been loaded when this is called + // so definitely no initial parse needed + set_initial_parsing_completed (true); } - if (view.buffer.text.length > 0) { - parse_string (view.buffer.text); - text_view_words.@set (view, prefix_tree); + debug ("initial parsing %s", get_initial_parsing_completed () ? "completed" : "INCOMPLETE"); + } + + // Returns true if text was completely parsed + public bool parse_text_and_add (string text) { + int index = 0; + string[] words = text.split_set (DELIMITERS); + uint n_words = words.length; + while (!parsing_cancelled && index < n_words) { + add_word (words[index++]); // only valid words will be added } + + return index == n_words; } - public void add_word (string word) { - if (word.length < MINIMUM_WORD_LENGTH) + public void parse_text_and_remove (string text) { + if (text.length < MINIMUM_WORD_LENGTH) { return; + } - lock (prefix_tree) { - prefix_tree.insert (word); + int index = 0; + string[] words = text.split_set (DELIMITERS); + uint n_words = words.length; + while (index < n_words) { + remove_word (words[index++]); } + + return; } - public void cancel_parsing () { - parsing_cancelled = true; + public void get_words_before_and_after_pos ( + string text, + int offset, + out string word_before, + out string word_after + ) { + word_before = get_word_immediately_before (text, offset); + word_after = get_word_immediately_after (text, offset); + } + + public string get_word_immediately_before (string text, int end_pos) { + if (end_pos < 1) { + return ""; + } + + int pos = end_pos; + unichar uc; + text.get_prev_char (ref pos, out uc); + if (is_delimiter (uc)) { + return ""; + } + + pos = (end_pos - MAXIMUM_WORD_LENGTH - 1).clamp (0, end_pos); + if (pos >= end_pos) { + critical ("pos after end_pos"); + return ""; + } + + var sliced_text = text.slice (pos, end_pos); + var words = sliced_text.split_set (DELIMITERS); + var previous_word = words[words.length - 1]; // Maybe "" + debug ("previous word %s", previous_word); + return previous_word.strip (); } - private bool parse_string (string text) { + public string get_word_immediately_after (string text, int start_pos) { + if (start_pos < 0 || start_pos > text.length - 1) { + return ""; + } + + int pos = start_pos; + unichar uc; + text.get_next_char (ref pos, out uc); + if (is_delimiter (uc)) { + return ""; + } + + pos = (start_pos + MAXIMUM_WORD_LENGTH + 1).clamp (start_pos, text.length); + if (start_pos >= pos) { + critical ("start pos after pos"); + return ""; + } + + var words = text.slice (start_pos, pos).split_set (DELIMITERS, 2); + var next_word = words[0]; // Maybe "" + debug ("next word %s", next_word); + return next_word.strip (); + } + + public void clear () requires (current_tree != null) { + cancel (); + lock (current_tree) { + current_tree.clear (); // Sets completed false + set_initial_parsing_completed (false); + + } + parsing_cancelled = false; - string [] word_array = text.split_set (DELIMITERS, MAX_TOKENS); - foreach (var current_word in word_array ) { - if (parsing_cancelled) { - debug ("Cancelling parse"); - return false; + } + + public void cancel () { + if (reaper_timeout_id > 0) { + Source.remove (reaper_timeout_id); + } + + reaping_cancelled = true; + parsing_cancelled = true; + } + + Gee.SortedMap sub_map; + public bool match (string prefix) requires (current_tree != null) { + // Keep sub map so do not need to reconstruct in get_completions_for_prefix. + sub_map = current_tree.tail_map (prefix); + bool found = false; + + sub_map.map_iterator ().foreach ((word, wo) => { + if (word.has_prefix (prefix)) { + if (wo.occurs ()) { + found = true; + return Source.REMOVE; // At least one match + } + } + + return Source.CONTINUE; + }); + + return found; + } + + public List get_completions_for_prefix (string prefix) requires (current_tree != null) { + var completions = new List (); + debug ("completions for %s", prefix); + var prefix_length = prefix.length; + // Sub map should always have been constructed in `match ()` function before coming here + assert (sub_map != null); + // Submap (tail_map) sometimes contains unexpected word *before* prefix_length + // Possibly a bug in Gee? It also contains words that do not start with prefix, + // but required words will be contiguous. So we continue loop if before prefix, + // and continue until not a completion + var count = 0; + sub_map.map_iterator ().@foreach ((word, wo) => { + if (word.has_prefix (prefix)) { + if (wo.occurs ()) { + var completion = word.slice (prefix_length, word.length); + completions.prepend (completion); + } + + count++; + return Source.CONTINUE; + } + + return count == 0; // Ignore words before prefix + }); + + sub_map = null; + return (owned)completions; + } + + // Only call if known that @word is a single word + public void add_word (string word_to_add) requires (current_tree != null) { + if (is_valid_word (word_to_add)) { + if (current_tree.has_key (word_to_add)) { + var wo = current_tree.@get (word_to_add); + debug ("incrementing"); + wo.increment (); + } else { + debug ("adding new %s length %u", word_to_add, word_to_add.length); + current_tree.@set (word_to_add, new WordOccurrence ()); } - add_word (current_word); + } else { + debug ("Not valid to add %s", word_to_add); } + } + + private uint reaper_timeout_id = 0; + private bool delay_reaping = false; + private const uint REAPING_THROTTLE_MS = 500; + private bool reaping_cancelled = false; + + // only call if known that @word is a single word + public void remove_word (string word_to_remove) requires (current_tree != null) { + if (current_tree.has_key (word_to_remove)) { + var word_occurrences = current_tree.@get (word_to_remove); + word_occurrences.decrement (); + if (!word_occurrences.occurs ()) { + var words_to_remove = get_words_to_remove (); + debug ("schedule remove %s", word_to_remove); + words_to_remove.add (word_to_remove); + schedule_reaping (); + } else { + debug ("not removing %s", word_to_remove); + } + } else { + debug ("%s not found in tree - length %u", word_to_remove, word_to_remove.length); + } + } + + private unowned Gee.LinkedList get_words_to_remove () { + return current_tree.get_data> (WORDS_TO_REMOVE); + } + + private void schedule_reaping () { + reaping_cancelled = false; + if (reaper_timeout_id > 0) { + delay_reaping = true; + return; + } else { + reaper_timeout_id = Timeout.add (REAPING_THROTTLE_MS, () => { + if (delay_reaping) { + delay_reaping = false; + return Source.CONTINUE; + } else { + reaper_timeout_id = 0; + var words_to_remove = get_words_to_remove (); + debug ("reaping"); + words_to_remove.foreach ((word) => { + if (reaping_cancelled) { + debug ("reaping was cancelled"); + return false; + } + + var wo = current_tree.@get (word); + if (wo != null && !wo.occurs ()) { + debug ("reaping %s", word); + current_tree.unset (word); + } + + return true; + }); + + // Cannot remove inside @foreach loop so do it now + words_to_remove.clear (); + return Source.REMOVE; + } + }); + } + } + + private bool is_valid_word (string word) { + if (word.strip ().length < MINIMUM_WORD_LENGTH) { + return false; + } + + // Exclude words beginning with digit + if (word.get_char (0).isdigit ()) { + return false; + } + return true; } } diff --git a/plugins/word-completion/meson.build b/plugins/word-completion/meson.build index 247b0e60ad..60390aec66 100644 --- a/plugins/word-completion/meson.build +++ b/plugins/word-completion/meson.build @@ -1,7 +1,8 @@ module_name = 'word-completion' module_files = [ - 'prefix-tree.vala', + # 'prefix-tree.vala', + # 'prefix-tree-node.vala', 'completion-provider.vala', 'engine.vala', 'plugin.vala' diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index d227d12a6a..1d3528e6ad 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -19,17 +19,7 @@ */ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { - public Object object { owned get; construct; } - - private List text_view_list = new List (); - public Euclide.Completion.Parser parser {get; private set;} - public Gtk.SourceView? current_view {get; private set;} - public Scratch.Services.Document current_document {get; private set;} - - private MainWindow main_window; - private Scratch.Services.Interface plugins; - private bool completion_in_progress = false; - + public const int MAX_TOKENS = 1000000; private const uint [] ACTIVATE_KEYS = { Gdk.Key.Return, Gdk.Key.KP_Enter, @@ -41,7 +31,16 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { private const uint REFRESH_SHORTCUT = Gdk.Key.bar; //"|" in combination with will cause refresh - private uint timeout_id = 0; + + public Object object { owned get; construct; } + + private List text_view_list = new List (); + private Euclide.Completion.Parser parser {get; private set;} + private Gtk.SourceView? current_view {get; private set;} + private Scratch.Services.Document current_document {get; private set;} + private MainWindow main_window; + private Scratch.Services.Interface plugins; + private uint initial_parse_timeout_id = 0; public void activate () { plugins = (Scratch.Services.Interface) object; @@ -62,33 +61,26 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } public void on_new_source_view (Scratch.Services.Document doc) { + // warning ("new source_view %s", doc.title); if (current_view != null) { - if (current_view == doc.source_view) + if (current_view == doc.source_view) { return; + } - parser.cancel_parsing (); - - if (timeout_id > 0) - GLib.Source.remove (timeout_id); - - cleanup (current_view); + parser.cancel (); // Stop any ongoing parsing or reaping + cleanup (); } current_document = doc; current_view = doc.source_view; - current_view.key_press_event.connect (on_key_press); - current_view.completion.show.connect (() => { - completion_in_progress = true; - }); - current_view.completion.hide.connect (() => { - completion_in_progress = false; - }); + current_view.buffer.insert_text.connect (on_insert_text); + current_view.buffer.delete_range.connect (on_delete_range); - - if (text_view_list.find (current_view) == null) + if (text_view_list.find (current_view) == null) { text_view_list.append (current_view); + } - var comp_provider = new Scratch.Plugins.CompletionProvider (this); + var comp_provider = new Scratch.Plugins.CompletionProvider (parser, current_view); comp_provider.priority = 1; comp_provider.name = provider_name_from_document (doc); @@ -96,77 +88,91 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_view.completion.add_provider (comp_provider); 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); - } catch (Error e) { - warning (e.message); + critical ( + "Could not add completion provider to %s. %s\n", + current_document.title, + e.message + ); + cleanup (); + return; } - } - private bool on_timeout_update () { - try { - new Thread.try ("word-completion-thread", () => { - if (current_view != null) - parser.parse_text_view (current_view as Gtk.TextView); + // Start initial parsing after timeout to ensure text loaded into buffer. + initial_parse_timeout_id = Timeout.add (1000, () => { + initial_parse_timeout_id = 0; + if (parser.select_current_tree (current_view)) { // Returns false if prefix tree new or parsing not completed + return Source.REMOVE; + } - return null; - }); - } catch (Error e) { - warning (e.message); - } - timeout_id = 0; - return false; - } + try { + new Thread.try ("word-completion-thread", () => { + if (current_view != null) { + parser.initial_parse_buffer_text (current_view.buffer.text); + } - 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; + return null; + }); + } catch (Error e) { + warning (e.message); } - } - var uc = (unichar)(Gdk.keyval_to_unicode (kv)); - if (!completion_in_progress && Euclide.Completion.Parser.is_delimiter (uc) && - (uc.isprint () || uc.isspace ())) { + return Source.REMOVE; + }); + } - var buffer = current_view.buffer; - var mark = buffer.get_insert (); - Gtk.TextIter cursor_iter; - buffer.get_iter_at_mark (out cursor_iter, mark); + // Runs before default handler so buffer text not yet modified. @pos must not be invalidated + private void on_insert_text (Gtk.TextIter iter, string new_text, int new_text_length) { + if (!parser.get_initial_parsing_completed ()) { + debug ("Ignoring spurious insertions when doc loading"); + return; + } + // Determine whether insertion point ends and/or starts a word + var text = current_view.buffer.text; + var insert_pos = iter.get_offset (); + string word_before, word_after; + parser.get_words_before_and_after_pos (text, insert_pos, out word_before, out word_after); + var text_to_add = (word_before + new_text + word_after).strip (); + var text_to_remove = (word_before + word_after).strip (); + // Only update if words have changed + if (text_to_add != text_to_remove) { + // Text to add may contain delimiters so parse + debug ("insert: add %s", text_to_add); + parser.parse_text_and_add (text_to_add); + // We know text to remove does not contain delimiters + debug ("insert: remove %s", text_to_remove); + parser.remove_word (text_to_remove); + } + } - var word_start = cursor_iter; - Euclide.Completion.Parser.back_to_word_start (ref word_start); + private void on_delete_range (Gtk.TextIter del_start_iter, Gtk.TextIter del_end_iter) { + var del_text = del_start_iter.get_text (del_end_iter); + var text = current_view.buffer.text; + var delete_start_pos = del_start_iter.get_offset (); + var delete_end_pos = del_end_iter.get_offset (); + var word_before = parser.get_word_immediately_before (text, delete_start_pos); + var word_after = parser.get_word_immediately_after (text, delete_end_pos); - string word = buffer.get_text (word_start, cursor_iter, false); - parser.add_word (word); - } + var to_remove = word_before + del_text + word_after; + parser.parse_text_and_remove (to_remove); - return false; + // A new word could have been created + var to_add = word_before + word_after; + parser.add_word (to_add); } 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); + private void cleanup () { + if (initial_parse_timeout_id > 0) { + GLib.Source.remove (initial_parse_timeout_id); + } + + current_view.buffer.insert_text.disconnect (on_insert_text); + current_view.buffer.delete_range.disconnect (on_delete_range); current_view.completion.get_providers ().foreach ((p) => { try { diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala deleted file mode 100644 index ea5ca2d414..0000000000 --- a/plugins/word-completion/prefix-tree.vala +++ /dev/null @@ -1,115 +0,0 @@ - -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); - } - } - } - } -}