From 7dbf009a050cae7533d2dfea473bad926aa5bcdf Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Sat, 2 Nov 2024 12:33:35 +0000 Subject: [PATCH 01/46] Start handling insert_text signal instead of key press signal in word completion plugin The 'insert_text' signal gives more detail about how the document contents have changed, giving us a way to more accurately update the prefix tree used for word completion --- plugins/word-completion/plugin.vala | 62 ++++++++++++----------------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index d227d12a6a..b70f2f11d1 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,45 +121,32 @@ 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 (completion_in_progress) { + return; } - var uc = (unichar)(Gdk.keyval_to_unicode (kv)); - if (!completion_in_progress && Euclide.Completion.Parser.is_delimiter (uc) && - (uc.isprint () || uc.isspace ())) { - - 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; - Euclide.Completion.Parser.back_to_word_start (ref word_start); - - string word = buffer.get_text (word_start, cursor_iter, false); - parser.add_word (word); + if (new_text.strip () == "") { + return; } - return false; + if (pos.ends_word ()) { + this.handle_insert_at_phrase_end (pos, new_text, new_text_length); + } + } + + 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 (); + + // Create a string of all the words from the first word start to the new text length + 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); + debug ("Full phrases:\n\n%s\n\n", full_phrases); } private string provider_name_from_document (Scratch.Services.Document doc) { @@ -166,7 +154,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } 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 { From ec2cc9ccb1f8f755d8aa2205cb6ce44bfbc4a7c2 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Sat, 2 Nov 2024 14:06:49 +0000 Subject: [PATCH 02/46] Word completion now adds words when multiple characters of text are added. Not just one character --- plugins/word-completion/engine.vala | 2 +- plugins/word-completion/plugin.vala | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index eff236042f..9b093d2ac2 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -86,7 +86,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 ) { diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index b70f2f11d1..8c7bd98483 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -76,7 +76,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_document = doc; current_view = doc.source_view; - current_view.buffer.insert_text.connect(on_insert_text); + current_view.buffer.insert_text.connect (on_insert_text); current_view.completion.show.connect (() => { completion_in_progress = true; @@ -134,19 +134,18 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { this.handle_insert_at_phrase_end (pos, new_text, new_text_length); } } - + 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 (); - - // Create a string of all the words from the first word start to the new text length + 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); - debug ("Full phrases:\n\n%s\n\n", full_phrases); + + var full_phrases = text_start_iter.get_text (text_end_iter) + new_text; + parser.parse_string (full_phrases); } private string provider_name_from_document (Scratch.Services.Document doc) { From 79e0429a9459f6003c390344beef7191d70b17b3 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Sat, 2 Nov 2024 22:06:11 +0000 Subject: [PATCH 03/46] Bring back handling of words from text inserted that isn't at a word boundaryto the Word Completion plugin --- plugins/word-completion/plugin.vala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 8c7bd98483..08726bb75f 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -132,6 +132,8 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { if (pos.ends_word ()) { this.handle_insert_at_phrase_end (pos, new_text, new_text_length); + } else { + this.handle_insert_not_at_word_boundary (pos, new_text, new_text_length); } } @@ -148,6 +150,10 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { 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 ()); } From 8a6edf0a1a9d1614efa4c828cf56ddbdf802dd4a Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Sun, 10 Nov 2024 23:43:04 +0000 Subject: [PATCH 04/46] Prepare for prefix tree word removal in word completion engine --- plugins/word-completion/engine.vala | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 9b093d2ac2..d4f23ef08b 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -98,4 +98,41 @@ 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 deletion_depth_level = word.length - word_end_index; + // TODO: Now delete word (prefix) from prefix tree with deletion_ + // depth level + } + + 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; + } } From 27d585f6732d8eb519b33ffecab27c03d596ec42 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Thu, 21 Nov 2024 20:29:33 +0000 Subject: [PATCH 05/46] word-completion: Add "remove" method to PrefixTree --- plugins/word-completion/engine.vala | 8 +++--- plugins/word-completion/prefix-tree.vala | 32 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index d4f23ef08b..c2aad1e0a3 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -115,9 +115,11 @@ public class Euclide.Completion.Parser : GLib.Object { return; } - uint deletion_depth_level = word.length - word_end_index; - // TODO: Now delete word (prefix) from prefix tree with deletion_ - // depth level + 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) { diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index ea5ca2d414..45806a35c9 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -63,6 +63,38 @@ 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; } From ab7fa7529949d0cce8c0b2792cf75c3f7b07585f Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Fri, 22 Nov 2024 21:14:46 +0000 Subject: [PATCH 06/46] Detect when text gets inserted between a word --- plugins/word-completion/plugin.vala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 08726bb75f..6498752382 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -130,8 +130,14 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { return; } - if (pos.ends_word ()) { + bool starts_word = pos.starts_word (); + bool ends_word = pos.ends_word (); + bool between_word = pos.inside_word () && !starts_word && !ends_word; + + if (ends_word) { this.handle_insert_at_phrase_end (pos, new_text, new_text_length); + } else if (between_word) { + debug ("word-completion: Text inserted between word.\n"); } else { this.handle_insert_not_at_word_boundary (pos, new_text, new_text_length); } From f748c017d6e517b54060051ed10cfe21942d9ef0 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Sun, 24 Nov 2024 18:21:25 +0000 Subject: [PATCH 07/46] word-completion: Support text being inserted between a word --- plugins/word-completion/plugin.vala | 39 ++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 6498752382..623aa1d9c5 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -137,12 +137,49 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { if (ends_word) { this.handle_insert_at_phrase_end (pos, new_text, new_text_length); } else if (between_word) { - debug ("word-completion: Text inserted between word.\n"); + 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); } } + 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); + } + + 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; From ddd0260a19bfa113b58e6b4bfd2ac96612480724 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 25 Nov 2024 21:47:13 +0000 Subject: [PATCH 08/46] Fix warning on startup --- src/MainWindow.vala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 34fe7d93db..c4d913ea2b 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -618,7 +618,10 @@ namespace Scratch { title = _("%s - %s").printf (doc.get_basename (), base_title); toolbar.set_document_focus (doc); - git_manager.active_project_path = doc.source_view.project.path; + if (doc.source_view.project != null) { + git_manager.active_project_path = doc.source_view.project.path; + } + folder_manager_view.select_path (doc.file.get_path ()); // Must follow setting focus document for editorconfig plug From 744a0f6bc93df32c89aaa26b49db2bf3aea5d363 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 25 Nov 2024 21:47:22 +0000 Subject: [PATCH 09/46] Fix warning on rename --- src/Widgets/SourceList/SourceList.vala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Widgets/SourceList/SourceList.vala b/src/Widgets/SourceList/SourceList.vala index 0ecff6daea..06fdcc783b 100644 --- a/src/Widgets/SourceList/SourceList.vala +++ b/src/Widgets/SourceList/SourceList.vala @@ -870,7 +870,11 @@ public class SourceList : Gtk.ScrolledWindow { return items.has_key (item); } - public void update_item (Item item) requires (has_item (item)) { + public void update_item (Item item) { + if (!has_item (item)) { + return; + } + assert (root != null); // Emitting row_changed() for this item's row in the child model causes the filter From 01af591de81889a3a8302482e94cef494d832331 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 26 Nov 2024 12:23:25 +0000 Subject: [PATCH 10/46] Fix minor code style --- plugins/word-completion/plugin.vala | 5 +++-- plugins/word-completion/prefix-tree.vala | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 623aa1d9c5..0874804b60 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -173,10 +173,11 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { return false; } - if (str.get_char(str.length - 1).isspace ()) { + + if (str.get_char (str.length - 1).isspace ()) { return true; } - + return false; } diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index 45806a35c9..6341949d13 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -96,7 +96,7 @@ namespace Scratch.Plugins { } 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) { From 12100cb04ded10f1e22cf3cb427e6225bfd24fea Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 26 Nov 2024 13:13:14 +0000 Subject: [PATCH 11/46] Continue to handle insertions even if no current completions --- plugins/word-completion/plugin.vala | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 0874804b60..b6c018b534 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -122,9 +122,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } private void on_insert_text (Gtk.TextIter pos, string new_text, int new_text_length) { - if (completion_in_progress) { - return; - } if (new_text.strip () == "") { return; From 3654d6030eb8ddaef4a1e4c858e7a9918ee0367d Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 26 Nov 2024 13:15:48 +0000 Subject: [PATCH 12/46] Do not show current word to find in completion list --- plugins/word-completion/engine.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index c2aad1e0a3..30f5ed5f8d 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; } From 6176df76aae015a7609bd96cb304336cf88b8c4e Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 26 Nov 2024 14:30:46 +0000 Subject: [PATCH 13/46] Ensure PrefixNode value is immutable --- plugins/word-completion/prefix-tree.vala | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index 6341949d13..14e5fc787c 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; From 7a5d7a1450f519e2ee8b248d1c6c9da4f266382a Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 27 Nov 2024 20:18:18 +0000 Subject: [PATCH 14/46] Move stuff from engine.vala to plugin.vala, rework. * Detect when cursor moves off insertion line --- .../word-completion/completion-provider.vala | 6 +- plugins/word-completion/engine.vala | 132 ++++----- plugins/word-completion/plugin.vala | 274 ++++++++++++++---- plugins/word-completion/prefix-tree.vala | 227 +++++++++++---- 4 files changed, 448 insertions(+), 191 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index d9abbc7b77..05468b0eb7 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -55,7 +55,7 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, 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); + start.backward_word_start (); string text = buffer.get_text (start, end, true); return parser.match (text); @@ -102,7 +102,7 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, buffer.get_iter_at_mark (out cursor_iter, mark); iter = cursor_iter; - Euclide.Completion.Parser.back_to_word_start (ref iter); + iter.backward_word_start (); return true; } @@ -121,7 +121,7 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, temp_buffer.get_iter_at_offset (out end, buffer.cursor_position); start = end; - Euclide.Completion.Parser.back_to_word_start (ref start); + start.backward_word_start (); to_find = buffer.get_text (start, end, false); } diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 30f5ed5f8d..bb56f45c7d 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -19,26 +19,13 @@ */ public class Euclide.Completion.Parser : GLib.Object { - public const int MINIMUM_WORD_LENGTH = 1; - public const int MAX_TOKENS = 1000000; - + public const uint MINIMUM_WORD_LENGTH = 3; 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 static void back_to_word_start (ref Gtk.TextIter iter) { - iter.backward_find_char (is_delimiter, null); - iter.forward_char (); - } - - public Gee.HashMap text_view_words; + public Gee.HashMap text_view_words; public bool parsing_cancelled = false; public Parser () { - text_view_words = new Gee.HashMap (); + text_view_words = new Gee.HashMap (); prefix_tree = new Scratch.Plugins.PrefixTree (); } @@ -46,96 +33,79 @@ public class Euclide.Completion.Parser : GLib.Object { return prefix_tree.find_prefix (to_find); } - 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; - } - - public void rebuild_word_list (Gtk.TextView view) { - 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 (prefix_tree) { + public void select_prefix_tree (Gtk.TextView view) { + 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 (); } - } + } + } - if (view.buffer.text.length > 0) { - parse_string (view.buffer.text); - text_view_words.@set (view, prefix_tree); - } + public void clear () requires (prefix_tree != null) { + prefix_tree.clear (); + } + + public void set_view_words (Gtk.TextView view) requires (prefix_tree != null) { + text_view_words.@set (view, prefix_tree); + } + + // Fills list with complete words having prefix + 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; } public void add_word (string word) { - if (word.length < MINIMUM_WORD_LENGTH) + if (!is_valid_word (word)) { + return; + } + + if (word.length < MINIMUM_WORD_LENGTH) { return; + } lock (prefix_tree) { prefix_tree.insert (word); } } - public void cancel_parsing () { - parsing_cancelled = true; - } - - 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 ) { - if (parsing_cancelled) { - debug ("Cancelling parse"); - return false; - } - add_word (current_word); + private bool is_valid_word (string word) { + // Exclude words beginning with digit + if (word.get_char (0).isdigit ()) { + return false; } + 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--; - } + public void cancel_parsing () { + parsing_cancelled = true; + } - // All possible prefixes of the word exist in the source view - if (match_found && word_end_index == word.length - 1) { - return; - } + public void delete_word (string word) requires (word.length > 0) { + // bool match_found = false; + // uint word_end_index = word.length - 1; - uint min_deletion_index = word_end_index + 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--; + // } - lock (prefix_tree) { - prefix_tree.remove (word, (int) min_deletion_index); - } - } + // // All possible prefixes of the word exist in the source view + // if (match_found && word_end_index == word.length - 1) { + // return; + // } - 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); - } + // uint min_deletion_index = word_end_index + 1; - return false; + // lock (prefix_tree) { + // prefix_tree.remove (word); + // } } } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index b6c018b534..17c8ff909e 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -17,6 +17,12 @@ * Boston, MA 02110-1301 USA. * */ +namespace Scratch { + // DELIMITERS used for word completion are not necessarily the same Pango word breaks + // Therefore, we reimplement some iter functions to move between words here below + public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t\'\"`"; + public const int MAX_TOKENS = 1000000; +} public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { public Object object { owned get; construct; } @@ -77,6 +83,8 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_document = doc; current_view = doc.source_view; current_view.buffer.insert_text.connect (on_insert_text); + current_view.buffer.delete_range.connect (on_delete_range); + current_view.buffer.notify["cursor-position"].connect (on_cursor_moved); current_view.completion.show.connect (() => { completion_in_progress = true; @@ -109,7 +117,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { try { new Thread.try ("word-completion-thread", () => { if (current_view != null) - parser.parse_text_view (current_view as Gtk.TextView); + parse_text_view (current_view as Gtk.TextView); return null; }); @@ -121,86 +129,254 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { return false; } + private void on_cursor_moved () { + var insert_offset = current_view.buffer.cursor_position; + Gtk.TextIter cursor_iter; + current_view.buffer.get_iter_at_offset (out cursor_iter, insert_offset); + if (current_insertion_line > -1 && current_insertion_line != cursor_iter.get_line ()) { + Gtk.TextIter line_start_iter, line_end_iter; + current_view.buffer.get_iter_at_line (out line_start_iter, current_insertion_line); + line_end_iter = line_start_iter; + line_end_iter.forward_to_line_end (); + var line_text = line_start_iter.get_text (line_end_iter); + var split_s = line_text.split_set (DELIMITERS, MAX_TOKENS); + foreach (string s in split_s) { + parser.add_word (s); //TODO remove deleted words + } + + current_insertion_line = -1; + } + } + // Runs before default handler so buffer text not yet modified. @pos must not be invalidated private void on_insert_text (Gtk.TextIter pos, string new_text, int new_text_length) { + // We need to process spaces and other delimiters too + // pos points to char immediately after where text will be inserted + var prev_iter = pos; + prev_iter.backward_char (); + if (is_delimiter (pos)) { + if (!is_delimiter (prev_iter)) { + warning ("Inserting postfix"); + handle_continue_word (pos, new_text, new_text_length); + return; + } else { + warning ("Inserting isolated"); - if (new_text.strip () == "") { + } + } else { + if (is_delimiter (prev_iter)) { + warning ("inserting prefixed"); + + } else { + warning ("inserting middle"); + } + } + current_insertion_line = pos.get_line (); + + return; // NOT HANDLED YET; + } + + private int current_insertion_line = -1; + private void handle_continue_word (Gtk.TextIter pos, string new_text, int new_text_length) { + current_insertion_line = pos.get_line (); + if (!contains_delimiter (new_text)) { return; } - bool starts_word = pos.starts_word (); - bool ends_word = pos.ends_word (); - bool between_word = pos.inside_word () && !starts_word && !ends_word; + // At least one complete word has been formed + var split_s = new_text.split_set (DELIMITERS, MAX_TOKENS); + assert (split_s.length > 1); - 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 text_start_iter = Gtk.TextIter (); + text_start_iter = pos; + backward_word_start (ref text_start_iter); + + var new_word = text_start_iter.get_text (pos) + split_s[0]; + parser.add_word (new_word); + // Add any other definitely complete words in new text + for (int i = 1; i < split_s.length - 1; i++) { + parser.add_word (split_s[i]); + } + + var temp = pos; + temp.forward_char (); + if (ends_with_delimiter (new_text) || is_delimiter (temp)) { + parser.add_word (split_s[split_s.length - 1]); } + + current_insertion_line = -1; } 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); - } - - var full_phrases = word_start_iter.get_text (pos) + new_text; - parser.parse_string (full_phrases); - } - - private bool ends_with_whitespace (string str) { + // warning ("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); + + // // 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.add_word (extra_word_to_add); + // } + + // var full_phrases = word_start_iter.get_text (pos) + new_text; + // parser.add_word (full_phrases); + } + + private bool ends_with_delimiter (string str) { if (str.length == 0) { return false; } - - if (str.get_char (str.length - 1).isspace ()) { + if (DELIMITERS.contains (str.slice (-1, str.length))) { 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); + private bool contains_delimiter (string str) { + int i = 0; + unichar curr; + bool found_delimiter = false; + bool has_next_character = false; + do { + has_next_character = str.get_next_char (ref i, out curr); + if (has_next_character) { + if (DELIMITERS.contains (curr.to_string ())) { + found_delimiter = true; + } + } + } while (has_next_character && !found_delimiter); - var full_phrases = text_start_iter.get_text (text_end_iter) + new_text; - parser.parse_string (full_phrases); + return found_delimiter; } + const char TEST_DELIMITER = '\t'; + + private void handle_insert_not_at_word_boundary (Gtk.TextIter pos, string new_text, int new_text_length) { - parser.parse_string (new_text); +// warning ("insert alone or at start"); +// parser.add_word (new_text); } private string provider_name_from_document (Scratch.Services.Document doc) { return _("%s - Word Completion").printf (doc.get_basename ()); } + private void on_delete_range (Gtk.TextIter del_start_iter, Gtk.TextIter del_end_iter) { + // var word_start_iter = del_start_iter; + // word_start_iter.backward_word_start (); + // var word_end_iter = del_end_iter; + // word_end_iter.forward_word_end (); + + // var word_to_delete = word_start_iter.get_text (word_end_iter); + // var word_to_add = word_start_iter.get_text (del_start_iter) + del_end_iter.get_text (word_end_iter); + + // warning ("word to delete %s", word_to_delete); + // warning ("word to add %s", word_to_add); + + // parser.delete_word (word_to_delete); + // parser.add_word (word_to_add); + } + + // Returns pointing to first char of word + private bool backward_word_start (ref Gtk.TextIter iter) { + while (is_delimiter (iter) && !iter.is_start ()) { + iter.backward_char (); + } + + while (!is_delimiter (iter)) { + iter.backward_char (); + } + + iter.forward_char (); + return !is_delimiter (iter); + } + + // Returns pointing to first char of word + private bool forward_word_start (ref Gtk.TextIter iter) { + while (!is_delimiter (iter)) { + iter.forward_char (); + } + + while (is_delimiter (iter) && !iter.is_end ()) { + iter.forward_char (); + } + + return !is_delimiter (iter); + } + + // Returns pointing to char after word + private bool forward_word_end (ref Gtk.TextIter iter) { + while (is_delimiter (iter) && !iter.is_end ()) { + iter.forward_char (); + } + + while (!is_delimiter (iter)) { + iter.forward_char (); + } + + return is_delimiter (iter); + } + + // Returns if pointing to pos immediately after possibly incomplete word + private bool is_immediate_word_end (Gtk.TextIter iter) { + var temp = iter; + temp.backward_char (); + + return !is_delimiter (temp); + } + + private bool is_delimiter (Gtk.TextIter iter) { + return iter.is_start () || iter.is_end () || DELIMITERS.index_of_char (iter.get_char ()) > -1; + } + + private void parse_text_view (Gtk.TextView view) { + /* If this view has already been parsed, restore the word list */ + parser.select_prefix_tree (view); + if (view.buffer.text.length > 0) { + parser.clear (); + parse_buffer (view.buffer); + parser.set_view_words (view); + } + } + + private void parse_buffer (Gtk.TextBuffer buff) { + Gtk.TextIter start_iter; + buff.get_start_iter (out start_iter); + string word; + while (get_next_word (ref start_iter, out word)) { + parser.add_word (word); + } + } + + private bool get_next_word (ref Gtk.TextIter iter, out string word) { + word = ""; + if (forward_word_start (ref iter)) { + var end_iter = iter; + forward_word_end (ref end_iter); + word = iter.get_visible_text (end_iter); + iter.assign (end_iter); // skip past found word + return true; + } + + return false; + } + private void cleanup (Gtk.SourceView view) { current_view.buffer.insert_text.disconnect (on_insert_text); + current_view.buffer.delete_range.disconnect (on_delete_range); + current_view.buffer.notify["cursor-position"].disconnect (on_cursor_moved); + // Disconnect show completion?? 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 14e5fc787c..99357ed6fb 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -1,18 +1,131 @@ namespace Scratch.Plugins { + private class PrefixNode : Object { + private enum NodeType { + ROOT, + CHAR, + WORD_END + } + public GLib.List children; - public unichar value { get; construct; } + private unichar? uc = null; + private NodeType type = ROOT; + public uint occurrences { get; set construct; default = 0; } + public PrefixNode? parent { get; construct; default = null; } + + public bool is_word_end { + get { + return type == WORD_END; + } + } - public PrefixNode (unichar c = '\0') { + public bool is_root { + get { + return type == ROOT; + } + } + + public uint length { + get { + return char_s.length; + } + } + + public string char_s { + owned get { + if (uc != null) { + return uc.to_string (); + } else { + return ""; + } + } + } + + public bool has_children { + get { + return type != WORD_END && children.first ().data != null; + } + } + + public PrefixNode.from_unichar (unichar c, PrefixNode? _parent) requires (c != '\0') { + Object ( + parent: _parent, + occurrences: 1 + ); + + uc = c; + type = CHAR; + } + + public PrefixNode.root () { + Object ( + parent: null, + occurrences: 0 + ); + + type = ROOT; + } + + public PrefixNode.word_end (PrefixNode _parent) { Object ( - value: c + parent: _parent, + occurrences: 1 ); + + uc = '\0'; + type = WORD_END; } construct { children = new List (); } + + public bool has_char (unichar c) { + return uc == c; + } + + private void increment () requires (type == WORD_END && occurrences < Scratch.MAX_TOKENS) { + occurrences++; + } + + public void decrement () requires (type != WORD_END && occurrences > 0) { + occurrences--; + } + + public void append_child (PrefixNode child) requires (type != WORD_END) { + children.append (child); + } + + public void remove_child (PrefixNode child) requires (this.has_children) { + children.remove (child); + if (children.length () < 1 && parent != null) { + parent.remove_child (this); + } + } + + public void insert_word_end () { + foreach (var child in children) { + if (child.is_word_end) { + child.increment (); + return; + } + } + + var new_child = new PrefixNode.word_end (this); + append_child (new_child); + } + + public void insert_char_child (unichar c) requires (!this.is_word_end) { + foreach (var child in children) { + if (child.has_char (c)) { + return; + } + } + + var new_child = new PrefixNode.word_end (this); + append_child (new_child); + } } public class PrefixTree : Object { @@ -23,79 +136,78 @@ namespace Scratch.Plugins { } public void clear () { - root = new PrefixNode (); + root = new PrefixNode.root (); } public void insert (string word) { if (word.length == 0) { return; } - +warning ("prefix tree insert %s", word); this.insert_at (word, this.root); } - private void insert_at (string word, PrefixNode node, int i = 0) { - unichar curr = '\0'; + public void decrement_word_occurrences (string word) { - 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)); + } + + private void insert_at (string word, PrefixNode node, int i = 0) requires (!node.is_word_end) { + unichar curr = '\0'; + if (!word.get_next_char (ref i, out curr) || curr == '\0') { + insert_word_end_at (node); + return; + } foreach (var child in node.children) { - if (child.value == curr) { - if (curr != '\0') { - insert_at (word, child, i); - } + if (child.has_char (curr)) { + insert_at (word, child, i); return; } } - var new_child = new PrefixNode (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); - } + assert (curr != '\0'); + var new_child = new PrefixNode.from_unichar (curr, node); + node.append_child (new_child); + insert_at (word, new_child, i); } - public void remove (string word, int min_deletion_index) { - if (word.length == 0) { - return; - } + private void insert_word_end_at (PrefixNode node) { + node.insert_word_end (); + } - remove_at (word, root, min_deletion_index); + public void remove (string word) requires (word.length > 0) { + // if (word.length == 0) { + // return; + // } + // var word_node = find_prefix_at (word, root); + // assert (word_node.occurrences > 0); + // word_node.decrement (); + // 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; + // 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; - } + // 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); + // 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; - } + // if (should_continue && child.children.length () == 0) { + // node.children.remove (child); + // return char_index < min_deletion_index; + // } - break; - } - } + // break; + // } + // } - return false; - } + // return false; + // } public bool find_prefix (string prefix) { return find_prefix_at (prefix, root) != null ? true : false; @@ -104,13 +216,13 @@ namespace Scratch.Plugins { 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') { + if (!prefix.get_next_char (ref i, out curr)) { + // if (curr == '\0') { return node; } foreach (var child in node.children) { - if (child.value == curr) { + if (child.has_char (curr)) { return find_prefix_at (prefix, child, i); } } @@ -121,7 +233,7 @@ namespace Scratch.Plugins { public List get_all_matches (string prefix) { var list = new List (); var node = find_prefix_at (prefix, root, 0); - if (node != null) { + if (node != null && !node.is_word_end) { var sb = new StringBuilder (prefix); get_all_matches_rec (node, ref sb, ref list); } @@ -135,13 +247,12 @@ namespace Scratch.Plugins { ref List matches) { foreach (var child in node.children) { - if (child.value == '\0') { + if (child.is_word_end) { matches.append (sbuilder.str); } else { - sbuilder.append_unichar (child.value); + sbuilder.append (child.char_s); get_all_matches_rec (child, ref sbuilder, ref matches); - var length = child.value.to_string ().length; - sbuilder.erase (sbuilder.len - length, -1); + sbuilder.erase (sbuilder.len - child.length, -1); } } } From 8bdba913689ff1aac2cffe3ae2f5322a5c1b543b Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 28 Nov 2024 02:09:14 +0000 Subject: [PATCH 15/46] More reworking --- plugins/word-completion/engine.vala | 56 +++--- plugins/word-completion/plugin.vala | 223 ++++++++++++++--------- plugins/word-completion/prefix-tree.vala | 135 +++++++------- 3 files changed, 224 insertions(+), 190 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index bb56f45c7d..e436c31e2b 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -21,45 +21,50 @@ public class Euclide.Completion.Parser : GLib.Object { public const uint MINIMUM_WORD_LENGTH = 3; private Scratch.Plugins.PrefixTree prefix_tree; + private Gtk.TextView current_view; 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 (); + // prefix_tree = new Scratch.Plugins.PrefixTree (); } + ~Parser () { + critical ("DESTRUCT parser"); + } public bool match (string to_find) { return prefix_tree.find_prefix (to_find); } public void select_prefix_tree (Gtk.TextView view) { - 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 (); + // lock (prefix_tree) { + if (!text_view_words.has_key (view)) { + warning ("creating new prefix tree for view"); + text_view_words.@set (view, new Scratch.Plugins.PrefixTree ()); } - } + // } + prefix_tree = text_view_words.@get (view); + current_view = view; } public void clear () requires (prefix_tree != null) { prefix_tree.clear (); } - public void set_view_words (Gtk.TextView view) requires (prefix_tree != null) { - text_view_words.@set (view, prefix_tree); - } + // public void set_view_words (Gtk.TextView view) requires (prefix_tree != null) { + // text_view_words.@set (view, prefix_tree); + // } // Fills list with complete words having prefix 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)); + // list.remove_link (list.find_custom (to_find, strcmp)); return list.first () != null; } public void add_word (string word) { + if (!is_valid_word (word)) { return; } @@ -69,6 +74,7 @@ public class Euclide.Completion.Parser : GLib.Object { } lock (prefix_tree) { +warning ("add word %s", word); prefix_tree.insert (word); } } @@ -86,26 +92,10 @@ public class Euclide.Completion.Parser : GLib.Object { parsing_cancelled = true; } - public void delete_word (string word) 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); - // } + public void remove_word (string word) requires (word.length > 0) { + lock (prefix_tree) { + warning ("remove %s", word); + prefix_tree.remove (word); + } } } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 17c8ff909e..28aae3a9b5 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -84,6 +84,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_view = doc.source_view; current_view.buffer.insert_text.connect (on_insert_text); current_view.buffer.delete_range.connect (on_delete_range); + current_view.buffer.delete_range.connect_after (after_delete_range); current_view.buffer.notify["cursor-position"].connect (on_cursor_moved); current_view.completion.show.connect (() => { @@ -114,96 +115,152 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } private bool on_timeout_update () { - try { - new Thread.try ("word-completion-thread", () => { - if (current_view != null) - parse_text_view (current_view as Gtk.TextView); + // try { + // new Thread.try ("word-completion-thread", () => { + if (current_view != null) { + warning ("parsing text view"); + parse_text_view (); + warning ("finished parsing"); + } - return null; - }); - } catch (Error e) { - warning (e.message); - } + // return null; + // }); + // } catch (Error e) { + // warning (e.message); + // } timeout_id = 0; - return false; + return Source.REMOVE; } private void on_cursor_moved () { var insert_offset = current_view.buffer.cursor_position; Gtk.TextIter cursor_iter; current_view.buffer.get_iter_at_offset (out cursor_iter, insert_offset); - if (current_insertion_line > -1 && current_insertion_line != cursor_iter.get_line ()) { + var temp_iter = cursor_iter; + temp_iter.backward_char (); + + if (current_insertion_line > -1 && (current_insertion_line != cursor_iter.get_line ())) { Gtk.TextIter line_start_iter, line_end_iter; current_view.buffer.get_iter_at_line (out line_start_iter, current_insertion_line); line_end_iter = line_start_iter; line_end_iter.forward_to_line_end (); var line_text = line_start_iter.get_text (line_end_iter); + + warning ("NEW LINE TEXT %s", line_text); var split_s = line_text.split_set (DELIMITERS, MAX_TOKENS); foreach (string s in split_s) { - parser.add_word (s); //TODO remove deleted words + if (s.length > 0) { + parser.add_word (s); + } } - current_insertion_line = -1; + warning ("original_text %s", original_text); + var orig_split_s = retrieve_original_text ().split_set (DELIMITERS, MAX_TOKENS); + foreach (string s in orig_split_s) { + if (s.length > 0) { + parser.remove_word (s); + } + } } } // Runs before default handler so buffer text not yet modified. @pos must not be invalidated private void on_insert_text (Gtk.TextIter pos, string new_text, int new_text_length) { // We need to process spaces and other delimiters too // pos points to char immediately after where text will be inserted - var prev_iter = pos; - prev_iter.backward_char (); - if (is_delimiter (pos)) { - if (!is_delimiter (prev_iter)) { - warning ("Inserting postfix"); - handle_continue_word (pos, new_text, new_text_length); - return; - } else { - warning ("Inserting isolated"); - - } - } else { - if (is_delimiter (prev_iter)) { - warning ("inserting prefixed"); - - } else { - warning ("inserting middle"); - } + if (contains_only_delimiters (new_text)) { + return; } - current_insertion_line = pos.get_line (); - return; // NOT HANDLED YET; + if (current_insertion_line == -1) { + record_original_line_at (pos); + } } private int current_insertion_line = -1; - private void handle_continue_word (Gtk.TextIter pos, string new_text, int new_text_length) { - current_insertion_line = pos.get_line (); - if (!contains_delimiter (new_text)) { - return; + private string original_text = ""; + private void record_original_line_at (Gtk.TextIter iter) requires (current_insertion_line < 0) { + current_insertion_line = iter.get_line (); + var start_iter = iter; + var end_iter = iter; + while (!start_iter.starts_line ()) { + start_iter.backward_char (); } + + end_iter.forward_to_line_end (); + original_text = start_iter.get_text (end_iter); + warning ("record original text %s", original_text); + } - // At least one complete word has been formed - var split_s = new_text.split_set (DELIMITERS, MAX_TOKENS); - assert (split_s.length > 1); + private string retrieve_original_text () { + var return_s = original_text; + original_text = ""; + current_insertion_line = -1; + // warning ("retrieved %s", return_s); + return return_s; + } + + int start_del_line = -1; + int end_del_line = -1; + 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_start_iter = Gtk.TextIter (); - text_start_iter = pos; - backward_word_start (ref text_start_iter); + if (contains_only_delimiters (del_text)) { + return; + } - var new_word = text_start_iter.get_text (pos) + split_s[0]; - parser.add_word (new_word); - // Add any other definitely complete words in new text - for (int i = 1; i < split_s.length - 1; i++) { - parser.add_word (split_s[i]); + start_del_line = del_start_iter.get_line (); + end_del_line = del_end_iter.get_line (); + if (end_del_line == start_del_line && current_insertion_line == -1) { + record_original_line_at (del_start_iter); //TODO Handle multiline delete ? rebuild } + } - var temp = pos; - temp.forward_char (); - if (ends_with_delimiter (new_text) || is_delimiter (temp)) { - parser.add_word (split_s[split_s.length - 1]); + private void after_delete_range (Gtk.TextIter del_start_iter, Gtk.TextIter del_end_iter) { + if (end_del_line > start_del_line) { + current_insertion_line = -1; + original_text = ""; + // warning ("parse view"); + // parse_text_view (); } - current_insertion_line = -1; + start_del_line = -1; + end_del_line = -1; + + } + + private void handle_continue_word (Gtk.TextIter pos, string new_text, int new_text_length) { + // if (!contains_delimiter (new_text)) { + // return; + // } + + // // At least one complete word has been formed + // var split_s = new_text.split_set (DELIMITERS, MAX_TOKENS); + // assert (split_s.length > 1); + + // var text_start_iter = Gtk.TextIter (); + // text_start_iter = pos; + // backward_word_start (ref text_start_iter); + + // var new_word = text_start_iter.get_text (pos) + split_s[0]; + // parser.add_word (new_word); + // // Add any other definitely complete words in new text + // for (int i = 1; i < split_s.length - 1; i++) { + // parser.add_word (split_s[i]); + // } + + // var temp = pos; + // temp.forward_char (); + // if (ends_with_delimiter (new_text) || is_delimiter (temp)) { + // parser.add_word (split_s[split_s.length - 1]); + // } + + // current_insertion_line = -1; + } + + private void handle_insert_not_at_word_boundary (Gtk.TextIter pos, string new_text, int new_text_length) { +// warning ("insert alone or at start"); +// parser.add_word (new_text); } private void handle_insert_between_phrase (Gtk.TextIter pos, string new_text, int new_text_length) { @@ -260,41 +317,34 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { return found_delimiter; } - const char TEST_DELIMITER = '\t'; - + private bool contains_only_delimiters (string str) { + int i = 0; + unichar curr; + bool found_char = false; + bool has_next_character = false; + do { + has_next_character = str.get_next_char (ref i, out curr); + if (has_next_character) { + if (!(DELIMITERS.contains (curr.to_string ()))) { + found_char = true; + } + } + } while (has_next_character && !found_char); - private void handle_insert_not_at_word_boundary (Gtk.TextIter pos, string new_text, int new_text_length) { -// warning ("insert alone or at start"); -// parser.add_word (new_text); + return !found_char; } private string provider_name_from_document (Scratch.Services.Document doc) { return _("%s - Word Completion").printf (doc.get_basename ()); } - private void on_delete_range (Gtk.TextIter del_start_iter, Gtk.TextIter del_end_iter) { - // var word_start_iter = del_start_iter; - // word_start_iter.backward_word_start (); - // var word_end_iter = del_end_iter; - // word_end_iter.forward_word_end (); - - // var word_to_delete = word_start_iter.get_text (word_end_iter); - // var word_to_add = word_start_iter.get_text (del_start_iter) + del_end_iter.get_text (word_end_iter); - - // warning ("word to delete %s", word_to_delete); - // warning ("word to add %s", word_to_add); - - // parser.delete_word (word_to_delete); - // parser.add_word (word_to_add); - } - // Returns pointing to first char of word private bool backward_word_start (ref Gtk.TextIter iter) { while (is_delimiter (iter) && !iter.is_start ()) { iter.backward_char (); } - while (!is_delimiter (iter)) { + while (!is_delimiter (iter) && !iter.is_start ()) { iter.backward_char (); } @@ -304,10 +354,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // Returns pointing to first char of word private bool forward_word_start (ref Gtk.TextIter iter) { - while (!is_delimiter (iter)) { - iter.forward_char (); - } - while (is_delimiter (iter) && !iter.is_end ()) { iter.forward_char (); } @@ -337,44 +383,53 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } private bool is_delimiter (Gtk.TextIter iter) { - return iter.is_start () || iter.is_end () || DELIMITERS.index_of_char (iter.get_char ()) > -1; + return DELIMITERS.index_of_char (iter.get_char ()) > -1; } - private void parse_text_view (Gtk.TextView view) { + private void parse_text_view (Gtk.TextView view = current_view) { /* If this view has already been parsed, restore the word list */ + warning ("parse text view"); parser.select_prefix_tree (view); + parser.clear (); if (view.buffer.text.length > 0) { - parser.clear (); parse_buffer (view.buffer); - parser.set_view_words (view); + // parser.set_view_words (view); } } private void parse_buffer (Gtk.TextBuffer buff) { + warning ("parse buffer"); Gtk.TextIter start_iter; buff.get_start_iter (out start_iter); string word; while (get_next_word (ref start_iter, out word)) { + warning ("found word %s", word); parser.add_word (word); } + + warning ("finished parse buffer"); } private bool get_next_word (ref Gtk.TextIter iter, out string word) { word = ""; + warning ("called with letter %s", iter.get_char ().to_string ()); if (forward_word_start (ref iter)) { + warning ("first letter of word is %s", iter.get_char ().to_string ()); var end_iter = iter; forward_word_end (ref end_iter); - word = iter.get_visible_text (end_iter); + word = iter.get_text (end_iter); iter.assign (end_iter); // skip past found word return true; } + warning ("next word returned false"); return false; } private void cleanup (Gtk.SourceView view) { current_view.buffer.insert_text.disconnect (on_insert_text); current_view.buffer.delete_range.disconnect (on_delete_range); + current_view.buffer.delete_range.disconnect (after_delete_range); current_view.buffer.notify["cursor-position"].disconnect (on_cursor_moved); // Disconnect show completion?? diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index 99357ed6fb..88a1f73860 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -1,14 +1,13 @@ namespace Scratch.Plugins { - - private class PrefixNode : Object { + public class PrefixNode : Object { private enum NodeType { ROOT, CHAR, WORD_END } - public GLib.List children; + public Gee.ArrayList children; private unichar? uc = null; private NodeType type = ROOT; public uint occurrences { get; set construct; default = 0; } @@ -44,7 +43,7 @@ namespace Scratch.Plugins { public bool has_children { get { - return type != WORD_END && children.first ().data != null; + return type != WORD_END && children.size > 0; } } @@ -77,8 +76,12 @@ namespace Scratch.Plugins { type = WORD_END; } + ~PrefixNode () { + critical ("DESTRUCT PrefixNode '%s' type %s", this.char_s, type.to_string ()); + } + construct { - children = new List (); + children = new Gee.ArrayList (); } public bool has_char (unichar c) { @@ -89,24 +92,34 @@ namespace Scratch.Plugins { occurrences++; } - public void decrement () requires (type != WORD_END && occurrences > 0) { + private void decrement () requires (type == WORD_END && occurrences > 0) { occurrences--; + if (occurrences == 0) { + critical ("remove after decrement"); + parent.remove_child (this); + } } - public void append_child (PrefixNode child) requires (type != WORD_END) { - children.append (child); + private void append_child (owned PrefixNode child) requires (type != WORD_END) { + children.add (child); } - public void remove_child (PrefixNode child) requires (this.has_children) { + private void remove_child (PrefixNode child) requires (type != WORD_END) { children.remove (child); - if (children.length () < 1 && parent != null) { - parent.remove_child (this); - } } - public void insert_word_end () { + public void remove_word_end () requires (this.has_children) { foreach (var child in children) { if (child.is_word_end) { + child.decrement (); + return; + } + } + } + + public void insert_word_end () requires (!this.is_word_end && !this.is_root) { + foreach (var child in children) { + if (child.type == WORD_END) { child.increment (); return; } @@ -116,26 +129,43 @@ namespace Scratch.Plugins { append_child (new_child); } - public void insert_char_child (unichar c) requires (!this.is_word_end) { + public PrefixNode append_char_child (unichar c) requires (!this.is_word_end) { foreach (var child in children) { if (child.has_char (c)) { - return; + return child; } } - var new_child = new PrefixNode.word_end (this); + var new_child = new PrefixNode.from_unichar (c, this); append_child (new_child); + return new_child; + } + + public PrefixNode? has_char_child (unichar c) requires (!this.is_word_end) { + foreach (var child in children) { + if (child.has_char (c)) { + return child; + } + } + + return null; } } public class PrefixTree : Object { - private PrefixNode root; + private PrefixNode? root = null; construct { + warning ("construct prefix tree"); clear (); } + ~PrefixTree () { + critical ("DESTRUCT PREFIXTREE"); + } + public void clear () { + warning ("clear prefix tree - new root"); root = new PrefixNode.root (); } @@ -143,71 +173,32 @@ namespace Scratch.Plugins { if (word.length == 0) { return; } -warning ("prefix tree insert %s", word); - this.insert_at (word, this.root); - } - - public void decrement_word_occurrences (string word) { + this.insert_at (word, this.root); } private void insert_at (string word, PrefixNode node, int i = 0) requires (!node.is_word_end) { unichar curr = '\0'; if (!word.get_next_char (ref i, out curr) || curr == '\0') { - insert_word_end_at (node); + node.insert_word_end (); return; } - foreach (var child in node.children) { - if (child.has_char (curr)) { - insert_at (word, child, i); - return; - } - } - - assert (curr != '\0'); - var new_child = new PrefixNode.from_unichar (curr, node); - node.append_child (new_child); - insert_at (word, new_child, i); - } - - private void insert_word_end_at (PrefixNode node) { - node.insert_word_end (); + var child = node.append_char_child (curr); + insert_at (word, child, i); } public void remove (string word) requires (word.length > 0) { - // if (word.length == 0) { - // return; - // } - // var word_node = find_prefix_at (word, root); - // assert (word_node.occurrences > 0); - // word_node.decrement (); - // 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; - // } + if (word.length == 0) { + return; + } - // break; - // } - // } + var word_node = find_prefix_at (word, root); - // return false; - // } + if (word_node != null) { + word_node.remove_word_end (); // Will autoremove unused parents + } + } public bool find_prefix (string prefix) { return find_prefix_at (prefix, root) != null ? true : false; @@ -217,14 +208,12 @@ warning ("prefix tree insert %s", word); unichar curr; if (!prefix.get_next_char (ref i, out curr)) { - // if (curr == '\0') { return node; } - foreach (var child in node.children) { - if (child.has_char (curr)) { - return find_prefix_at (prefix, child, i); - } + var child = node.has_char_child (curr); + if (child != null) { + return find_prefix_at (prefix, child, i); } return null; From fca35e565a606ebaa15591645708598d0da31f6e Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 28 Nov 2024 12:02:39 +0000 Subject: [PATCH 16/46] Reimplement threaded parse, use string not text iters --- plugins/word-completion/engine.vala | 72 ++++--- plugins/word-completion/plugin.vala | 246 ++++++++++------------- plugins/word-completion/prefix-tree.vala | 3 +- 3 files changed, 157 insertions(+), 164 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index e436c31e2b..2a0ceef86d 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -21,7 +21,6 @@ public class Euclide.Completion.Parser : GLib.Object { public const uint MINIMUM_WORD_LENGTH = 3; private Scratch.Plugins.PrefixTree prefix_tree; - private Gtk.TextView current_view; public Gee.HashMap text_view_words; public bool parsing_cancelled = false; @@ -37,19 +36,38 @@ public class Euclide.Completion.Parser : GLib.Object { return prefix_tree.find_prefix (to_find); } - public void select_prefix_tree (Gtk.TextView view) { - // lock (prefix_tree) { - if (!text_view_words.has_key (view)) { - warning ("creating new prefix tree for view"); - text_view_words.@set (view, new Scratch.Plugins.PrefixTree ()); - } - // } - prefix_tree = text_view_words.@get (view); - current_view = view; + public bool select_prefix_tree (Gtk.TextView view) { + bool pre_existing = true; + + if (!text_view_words.has_key (view)) { + text_view_words.@set (view, new Scratch.Plugins.PrefixTree ()); + pre_existing = false; + } + + lock (prefix_tree) { + prefix_tree = text_view_words.@get (view); + } + + return pre_existing; } public void clear () requires (prefix_tree != null) { - prefix_tree.clear (); + lock (prefix_tree) { + prefix_tree.clear (); + prefix_tree.initial_parse_complete = false; + } + + parsing_cancelled = false; + } + + public void set_initial_parsing_complete () { + lock (prefix_tree) { + prefix_tree.initial_parse_complete = true; + } + } + + public bool get_initial_parsing_complete () { + return prefix_tree.initial_parse_complete; } // public void set_view_words (Gtk.TextView view) requires (prefix_tree != null) { @@ -64,22 +82,27 @@ public class Euclide.Completion.Parser : GLib.Object { } public void add_word (string word) { - - if (!is_valid_word (word)) { - return; - } - - if (word.length < MINIMUM_WORD_LENGTH) { - return; + if (is_valid_word (word)) { + lock (prefix_tree) { + prefix_tree.insert (word); + } } + } - lock (prefix_tree) { -warning ("add word %s", word); - prefix_tree.insert (word); + public void remove_word (string word) requires (word.length > 0) { + if (is_valid_word (word)) { + lock (prefix_tree) { + warning ("remove %s", word); + prefix_tree.remove (word); + } } } 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; @@ -91,11 +114,4 @@ warning ("add word %s", word); public void cancel_parsing () { parsing_cancelled = true; } - - public void remove_word (string word) requires (word.length > 0) { - lock (prefix_tree) { - warning ("remove %s", word); - prefix_tree.remove (word); - } - } } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 28aae3a9b5..92d0337995 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -107,32 +107,92 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { 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); - + parser.cancel_parsing (); + if (!parser.select_prefix_tree (current_view) || !parser.get_initial_parsing_complete ()) { + // No pre-existing parsed tree - start parsing after timeout + timeout_id = Timeout.add (1000, on_timeout_update); + } } catch (Error e) { warning (e.message); } } private bool on_timeout_update () { - // try { - // new Thread.try ("word-completion-thread", () => { + try { + timeout_id = 0; + new Thread.try ("word-completion-thread", () => { if (current_view != null) { warning ("parsing text view"); parse_text_view (); warning ("finished parsing"); } - // return null; - // }); - // } catch (Error e) { - // warning (e.message); - // } + return null; + }); + } catch (Error e) { + warning (e.message); + } - timeout_id = 0; return Source.REMOVE; } + private void parse_text_view (Gtk.TextView view = current_view) { + parser.clear (); + if (view.buffer.text.length > 0) { + parse_buffer_text (view.buffer.text); + } + } + + private void parse_buffer_text (string text) { + int start_pos = 0; + string word = ""; + while (!parser.parsing_cancelled && get_next_word (text, ref start_pos, out word)) { + parser.add_word (word); + } + + parser.set_initial_parsing_complete (); + } + + // private bool get_next_word (ref Gtk.TextIter iter, out string word) { + private bool get_next_word (string text, ref int pos, out string word) { + word = ""; + if (forward_word_start (text, ref pos)) { + + var end_pos = pos; + forward_word_end (text, ref end_pos); + word = text.slice (pos, end_pos); + pos = end_pos; + return true; + } + + return false; + } + + // Returns pointing to first char of word + private bool forward_word_start (string text, ref int pos) { + unichar? uc; + while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) {} + pos--; + return uc != null && !is_delimiter (uc); + } + + // Returns pointing to char after word + private bool forward_word_end (string text, ref int pos) { + unichar? uc; + while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) { + } + + while (text.get_next_char (ref pos, out uc) && !is_delimiter (uc)) { + } + + pos--; + return uc == null || is_delimiter (uc); + } + + private bool is_delimiter (unichar uc) { + return DELIMITERS.index_of_char (uc) > -1; + } + private void on_cursor_moved () { var insert_offset = current_view.buffer.cursor_position; Gtk.TextIter cursor_iter; @@ -145,22 +205,18 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_view.buffer.get_iter_at_line (out line_start_iter, current_insertion_line); line_end_iter = line_start_iter; line_end_iter.forward_to_line_end (); - var line_text = line_start_iter.get_text (line_end_iter); + var line_text = line_start_iter.get_text (line_end_iter).strip (); warning ("NEW LINE TEXT %s", line_text); var split_s = line_text.split_set (DELIMITERS, MAX_TOKENS); foreach (string s in split_s) { - if (s.length > 0) { - parser.add_word (s); - } + parser.add_word (s); // Ignores invalid words } - warning ("original_text %s", original_text); - var orig_split_s = retrieve_original_text ().split_set (DELIMITERS, MAX_TOKENS); + var retrieved_original_text = retrieve_original_text ().strip (); + var orig_split_s = retrieved_original_text.split_set (DELIMITERS, MAX_TOKENS); foreach (string s in orig_split_s) { - if (s.length > 0) { - parser.remove_word (s); - } + parser.remove_word (s); } } } @@ -186,7 +242,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { while (!start_iter.starts_line ()) { start_iter.backward_char (); } - + end_iter.forward_to_line_end (); original_text = start_iter.get_text (end_iter); warning ("record original text %s", original_text); @@ -216,7 +272,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } } - private void after_delete_range (Gtk.TextIter del_start_iter, Gtk.TextIter del_end_iter) { + private void after_delete_range (Gtk.TextIter del_start_iter, Gtk.TextIter del_end_iter) { if (end_del_line > start_del_line) { current_insertion_line = -1; original_text = ""; @@ -288,34 +344,34 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // parser.add_word (full_phrases); } - private bool ends_with_delimiter (string str) { - if (str.length == 0) { - return false; - } - - if (DELIMITERS.contains (str.slice (-1, str.length))) { - return true; - } - - return false; - } - - private bool contains_delimiter (string str) { - int i = 0; - unichar curr; - bool found_delimiter = false; - bool has_next_character = false; - do { - has_next_character = str.get_next_char (ref i, out curr); - if (has_next_character) { - if (DELIMITERS.contains (curr.to_string ())) { - found_delimiter = true; - } - } - } while (has_next_character && !found_delimiter); - - return found_delimiter; - } + // private bool ends_with_delimiter (string str) { + // if (str.length == 0) { + // return false; + // } + + // if (DELIMITERS.contains (str.slice (-1, str.length))) { + // return true; + // } + + // return false; + // } + + // private bool contains_delimiter (string str) { + // int i = 0; + // unichar curr; + // bool found_delimiter = false; + // bool has_next_character = false; + // do { + // has_next_character = str.get_next_char (ref i, out curr); + // if (has_next_character) { + // if (DELIMITERS.contains (curr.to_string ())) { + // found_delimiter = true; + // } + // } + // } while (has_next_character && !found_delimiter); + + // return found_delimiter; + // } private bool contains_only_delimiters (string str) { int i = 0; @@ -338,93 +394,13 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { return _("%s - Word Completion").printf (doc.get_basename ()); } - // Returns pointing to first char of word - private bool backward_word_start (ref Gtk.TextIter iter) { - while (is_delimiter (iter) && !iter.is_start ()) { - iter.backward_char (); - } + // // Returns if pointing to pos immediately after possibly incomplete word + // private bool is_immediate_word_end (Gtk.TextIter iter) { + // var temp = iter; + // temp.backward_char (); - while (!is_delimiter (iter) && !iter.is_start ()) { - iter.backward_char (); - } - - iter.forward_char (); - return !is_delimiter (iter); - } - - // Returns pointing to first char of word - private bool forward_word_start (ref Gtk.TextIter iter) { - while (is_delimiter (iter) && !iter.is_end ()) { - iter.forward_char (); - } - - return !is_delimiter (iter); - } - - // Returns pointing to char after word - private bool forward_word_end (ref Gtk.TextIter iter) { - while (is_delimiter (iter) && !iter.is_end ()) { - iter.forward_char (); - } - - while (!is_delimiter (iter)) { - iter.forward_char (); - } - - return is_delimiter (iter); - } - - // Returns if pointing to pos immediately after possibly incomplete word - private bool is_immediate_word_end (Gtk.TextIter iter) { - var temp = iter; - temp.backward_char (); - - return !is_delimiter (temp); - } - - private bool is_delimiter (Gtk.TextIter iter) { - return DELIMITERS.index_of_char (iter.get_char ()) > -1; - } - - private void parse_text_view (Gtk.TextView view = current_view) { - /* If this view has already been parsed, restore the word list */ - warning ("parse text view"); - parser.select_prefix_tree (view); - parser.clear (); - if (view.buffer.text.length > 0) { - parse_buffer (view.buffer); - // parser.set_view_words (view); - } - } - - private void parse_buffer (Gtk.TextBuffer buff) { - warning ("parse buffer"); - Gtk.TextIter start_iter; - buff.get_start_iter (out start_iter); - string word; - while (get_next_word (ref start_iter, out word)) { - warning ("found word %s", word); - parser.add_word (word); - } - - warning ("finished parse buffer"); - } - - private bool get_next_word (ref Gtk.TextIter iter, out string word) { - word = ""; - warning ("called with letter %s", iter.get_char ().to_string ()); - if (forward_word_start (ref iter)) { - warning ("first letter of word is %s", iter.get_char ().to_string ()); - var end_iter = iter; - forward_word_end (ref end_iter); - word = iter.get_text (end_iter); - iter.assign (end_iter); // skip past found word - return true; - } - - warning ("next word returned false"); - return false; - } + // return !is_delimiter (temp); + // } private void cleanup (Gtk.SourceView view) { current_view.buffer.insert_text.disconnect (on_insert_text); diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index 88a1f73860..327a96613b 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -140,7 +140,7 @@ namespace Scratch.Plugins { append_child (new_child); return new_child; } - + public PrefixNode? has_char_child (unichar c) requires (!this.is_word_end) { foreach (var child in children) { if (child.has_char (c)) { @@ -154,6 +154,7 @@ namespace Scratch.Plugins { public class PrefixTree : Object { private PrefixNode? root = null; + public bool initial_parse_complete = false; construct { warning ("construct prefix tree"); From d18f6622f42504e348d62106b7eb4616cba8069e Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 28 Nov 2024 18:06:21 +0000 Subject: [PATCH 17/46] Move some code into parser engine; remove unneeded code --- plugins/word-completion/engine.vala | 70 +++++- plugins/word-completion/plugin.vala | 280 ++++++----------------- plugins/word-completion/prefix-tree.vala | 12 +- 3 files changed, 129 insertions(+), 233 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 2a0ceef86d..1cdf96c77e 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -29,9 +29,66 @@ public class Euclide.Completion.Parser : GLib.Object { // prefix_tree = new Scratch.Plugins.PrefixTree (); } - ~Parser () { - critical ("DESTRUCT parser"); + public void initial_parse_buffer_text (string buffer_text) { + parsing_cancelled = false; + + clear (); + if (buffer_text.length > 0) { + set_initial_parsing_completed (parse_text (buffer_text)); + } else { + set_initial_parsing_completed (false); + } + } + + // Returns whether text was completely parsed + private bool parse_text (string text) { + int start_pos = 0; + string word = ""; + while (!parsing_cancelled && get_next_word (text, ref start_pos, out word)) { + add_word (word); + } + + return parsing_cancelled; + } + + private bool get_next_word (string text, ref int pos, out string word) { + word = ""; + if (forward_word_start (text, ref pos)) { + var end_pos = pos; + forward_word_end (text, ref end_pos); + word = text.slice (pos, end_pos); + pos = end_pos; + return true; + } + + return false; } + + // Returns pointing to first char of word + private bool forward_word_start (string text, ref int pos) { + unichar? uc; + while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) {} + pos--; + return uc != null && !is_delimiter (uc); + } + + // Returns pointing to char after word + private bool forward_word_end (string text, ref int pos) { + unichar? uc; + while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) { + } + + while (text.get_next_char (ref pos, out uc) && !is_delimiter (uc)) { + } + + pos--; + return uc == null || is_delimiter (uc); + } + + private bool is_delimiter (unichar uc) { + return Scratch.Plugins.Completion.DELIMITERS.index_of_char (uc) > -1; + } + public bool match (string to_find) { return prefix_tree.find_prefix (to_find); } @@ -53,20 +110,19 @@ public class Euclide.Completion.Parser : GLib.Object { public void clear () requires (prefix_tree != null) { lock (prefix_tree) { - prefix_tree.clear (); - prefix_tree.initial_parse_complete = false; + prefix_tree.clear (); // Sets completed false } parsing_cancelled = false; } - public void set_initial_parsing_complete () { + public void set_initial_parsing_completed (bool completed) { lock (prefix_tree) { - prefix_tree.initial_parse_complete = true; + prefix_tree.initial_parse_complete = completed; } } - public bool get_initial_parsing_complete () { + public bool get_initial_parsing_completed () { return prefix_tree.initial_parse_complete; } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 92d0337995..da030bd198 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -17,14 +17,13 @@ * Boston, MA 02110-1301 USA. * */ -namespace Scratch { + +public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // DELIMITERS used for word completion are not necessarily the same Pango word breaks // Therefore, we reimplement some iter functions to move between words here below public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t\'\"`"; public const int MAX_TOKENS = 1000000; -} -public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { public Object object { owned get; construct; } private List text_view_list = new List (); @@ -68,16 +67,19 @@ 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) + if (timeout_id > 0) { GLib.Source.remove (timeout_id); + } - cleanup (current_view); + cleanup (); } current_document = doc; @@ -90,13 +92,14 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { 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) + if (text_view_list.find (current_view) == null) { text_view_list.append (current_view); + } var comp_provider = new Scratch.Plugins.CompletionProvider (this); comp_provider.priority = 1; @@ -106,91 +109,46 @@ 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*/ - parser.cancel_parsing (); - if (!parser.select_prefix_tree (current_view) || !parser.get_initial_parsing_complete ()) { - // No pre-existing parsed tree - start parsing after timeout - timeout_id = Timeout.add (1000, on_timeout_update); - } } catch (Error e) { - warning (e.message); + warning ("Could not add completion provider to %s. %s\n", current_document.title, e.message); + warning ("Not parsing"); + cleanup (); + return; } - } - private bool on_timeout_update () { - try { - timeout_id = 0; - new Thread.try ("word-completion-thread", () => { - if (current_view != null) { - warning ("parsing text view"); - parse_text_view (); - warning ("finished parsing"); + /* Wait a bit to allow text to load then run parser*/ + if (!parser.select_prefix_tree (current_view)) { // Returns whether selected prefix tree has been initially parsed + // Start initial parsing after timeout to ensure text loaded + timeout_id = Timeout.add (1000,() => { + timeout_id = 0; + try { + new Thread.try ("word-completion-thread", () => { + if (current_view != null) { + warning ("%s - initial parsing", provider_name_from_document (current_document)); + parser.initial_parse_buffer_text (current_view.buffer.text); + warning ("%s - finished initial parsing", provider_name_from_document (current_document)); + } + + return null; + }); + } catch (Error e) { + warning (e.message); } - return null; + return Source.REMOVE; }); - } catch (Error e) { - warning (e.message); - } - - return Source.REMOVE; - } - - private void parse_text_view (Gtk.TextView view = current_view) { - parser.clear (); - if (view.buffer.text.length > 0) { - parse_buffer_text (view.buffer.text); - } - } - - private void parse_buffer_text (string text) { - int start_pos = 0; - string word = ""; - while (!parser.parsing_cancelled && get_next_word (text, ref start_pos, out word)) { - parser.add_word (word); } - - parser.set_initial_parsing_complete (); - } - - // private bool get_next_word (ref Gtk.TextIter iter, out string word) { - private bool get_next_word (string text, ref int pos, out string word) { - word = ""; - if (forward_word_start (text, ref pos)) { - - var end_pos = pos; - forward_word_end (text, ref end_pos); - word = text.slice (pos, end_pos); - pos = end_pos; - return true; - } - - return false; } - // Returns pointing to first char of word - private bool forward_word_start (string text, ref int pos) { - unichar? uc; - while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) {} - pos--; - return uc != null && !is_delimiter (uc); - } - - // Returns pointing to char after word - private bool forward_word_end (string text, ref int pos) { - unichar? uc; - while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) { + // Runs before default handler so buffer text not yet modified. @pos must not be invalidated + private void on_insert_text (Gtk.TextIter pos, string new_text, int new_text_length) { + if (contains_only_delimiters (new_text)) { + return; } - while (text.get_next_char (ref pos, out uc) && !is_delimiter (uc)) { + if (current_insertion_line == -1) { + record_original_line_at (pos); } - - pos--; - return uc == null || is_delimiter (uc); - } - - private bool is_delimiter (unichar uc) { - return DELIMITERS.index_of_char (uc) > -1; } private void on_cursor_moved () { @@ -207,7 +165,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { line_end_iter.forward_to_line_end (); var line_text = line_start_iter.get_text (line_end_iter).strip (); - warning ("NEW LINE TEXT %s", line_text); var split_s = line_text.split_set (DELIMITERS, MAX_TOKENS); foreach (string s in split_s) { parser.add_word (s); // Ignores invalid words @@ -220,41 +177,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } } } - // Runs before default handler so buffer text not yet modified. @pos must not be invalidated - private void on_insert_text (Gtk.TextIter pos, string new_text, int new_text_length) { - // We need to process spaces and other delimiters too - // pos points to char immediately after where text will be inserted - if (contains_only_delimiters (new_text)) { - return; - } - - if (current_insertion_line == -1) { - record_original_line_at (pos); - } - } - - private int current_insertion_line = -1; - private string original_text = ""; - private void record_original_line_at (Gtk.TextIter iter) requires (current_insertion_line < 0) { - current_insertion_line = iter.get_line (); - var start_iter = iter; - var end_iter = iter; - while (!start_iter.starts_line ()) { - start_iter.backward_char (); - } - - end_iter.forward_to_line_end (); - original_text = start_iter.get_text (end_iter); - warning ("record original text %s", original_text); - } - - private string retrieve_original_text () { - var return_s = original_text; - original_text = ""; - current_insertion_line = -1; - // warning ("retrieved %s", return_s); - return return_s; - } int start_del_line = -1; int end_del_line = -1; @@ -272,7 +194,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } } - private void after_delete_range (Gtk.TextIter del_start_iter, Gtk.TextIter del_end_iter) { + private void after_delete_range () { if (end_del_line > start_del_line) { current_insertion_line = -1; original_text = ""; @@ -282,97 +204,8 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { start_del_line = -1; end_del_line = -1; - - } - - private void handle_continue_word (Gtk.TextIter pos, string new_text, int new_text_length) { - // if (!contains_delimiter (new_text)) { - // return; - // } - - // // At least one complete word has been formed - // var split_s = new_text.split_set (DELIMITERS, MAX_TOKENS); - // assert (split_s.length > 1); - - // var text_start_iter = Gtk.TextIter (); - // text_start_iter = pos; - // backward_word_start (ref text_start_iter); - - // var new_word = text_start_iter.get_text (pos) + split_s[0]; - // parser.add_word (new_word); - // // Add any other definitely complete words in new text - // for (int i = 1; i < split_s.length - 1; i++) { - // parser.add_word (split_s[i]); - // } - - // var temp = pos; - // temp.forward_char (); - // if (ends_with_delimiter (new_text) || is_delimiter (temp)) { - // parser.add_word (split_s[split_s.length - 1]); - // } - - // current_insertion_line = -1; - } - - private void handle_insert_not_at_word_boundary (Gtk.TextIter pos, string new_text, int new_text_length) { -// warning ("insert alone or at start"); -// parser.add_word (new_text); } - private void handle_insert_between_phrase (Gtk.TextIter pos, string new_text, int new_text_length) { - // warning ("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); - - // // 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.add_word (extra_word_to_add); - // } - - // var full_phrases = word_start_iter.get_text (pos) + new_text; - // parser.add_word (full_phrases); - } - - // private bool ends_with_delimiter (string str) { - // if (str.length == 0) { - // return false; - // } - - // if (DELIMITERS.contains (str.slice (-1, str.length))) { - // return true; - // } - - // return false; - // } - - // private bool contains_delimiter (string str) { - // int i = 0; - // unichar curr; - // bool found_delimiter = false; - // bool has_next_character = false; - // do { - // has_next_character = str.get_next_char (ref i, out curr); - // if (has_next_character) { - // if (DELIMITERS.contains (curr.to_string ())) { - // found_delimiter = true; - // } - // } - // } while (has_next_character && !found_delimiter); - - // return found_delimiter; - // } - private bool contains_only_delimiters (string str) { int i = 0; unichar curr; @@ -390,19 +223,34 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { return !found_char; } - private string provider_name_from_document (Scratch.Services.Document doc) { - return _("%s - Word Completion").printf (doc.get_basename ()); + private int current_insertion_line = -1; + private string original_text = ""; + private void record_original_line_at (Gtk.TextIter iter) requires (current_insertion_line < 0) { + current_insertion_line = iter.get_line (); + var start_iter = iter; + var end_iter = iter; + while (!start_iter.starts_line ()) { + start_iter.backward_char (); + } + + end_iter.forward_to_line_end (); + original_text = start_iter.get_text (end_iter); + warning ("record original text %s", original_text); } - // // Returns if pointing to pos immediately after possibly incomplete word - // private bool is_immediate_word_end (Gtk.TextIter iter) { - // var temp = iter; - // temp.backward_char (); + private string retrieve_original_text () { + var return_s = original_text; + original_text = ""; + current_insertion_line = -1; + // warning ("retrieved %s", return_s); + return return_s; + } - // return !is_delimiter (temp); - // } + private string provider_name_from_document (Scratch.Services.Document doc) { + return _("%s - Word Completion").printf (doc.get_basename ()); + } - private void cleanup (Gtk.SourceView view) { + private void cleanup () { current_view.buffer.insert_text.disconnect (on_insert_text); current_view.buffer.delete_range.disconnect (on_delete_range); current_view.buffer.delete_range.disconnect (after_delete_range); diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index 327a96613b..fd17933b3e 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -76,10 +76,6 @@ namespace Scratch.Plugins { type = WORD_END; } - ~PrefixNode () { - critical ("DESTRUCT PrefixNode '%s' type %s", this.char_s, type.to_string ()); - } - construct { children = new Gee.ArrayList (); } @@ -88,7 +84,7 @@ namespace Scratch.Plugins { return uc == c; } - private void increment () requires (type == WORD_END && occurrences < Scratch.MAX_TOKENS) { + private void increment () requires (type == WORD_END) { occurrences++; } @@ -161,13 +157,9 @@ namespace Scratch.Plugins { clear (); } - ~PrefixTree () { - critical ("DESTRUCT PREFIXTREE"); - } - public void clear () { - warning ("clear prefix tree - new root"); root = new PrefixNode.root (); + initial_parse_complete = false; } public void insert (string word) { From e3e68ba1d06d41e0cea3df90a59389be224262ac Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 28 Nov 2024 18:27:37 +0000 Subject: [PATCH 18/46] Split out PrefixNode into separate file; renaming and cleanup --- plugins/word-completion/engine.vala | 54 ++-- plugins/word-completion/meson.build | 1 + plugins/word-completion/plugin.vala | 4 +- plugins/word-completion/prefix-tree-node.vala | 168 ++++++++++ plugins/word-completion/prefix-tree.vala | 298 +++++------------- 5 files changed, 280 insertions(+), 245 deletions(-) create mode 100644 plugins/word-completion/prefix-tree-node.vala diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 1cdf96c77e..8c15dd9a02 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 @@ -20,13 +21,12 @@ public class Euclide.Completion.Parser : GLib.Object { public const uint MINIMUM_WORD_LENGTH = 3; - private Scratch.Plugins.PrefixTree prefix_tree; + private Scratch.Plugins.PrefixTree? 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 (); } public void initial_parse_buffer_text (string buffer_text) { @@ -89,11 +89,11 @@ public class Euclide.Completion.Parser : GLib.Object { return Scratch.Plugins.Completion.DELIMITERS.index_of_char (uc) > -1; } - public bool match (string to_find) { - return prefix_tree.find_prefix (to_find); + public bool match (string to_find) requires (current_tree != null) { + return current_tree.find_prefix (to_find); } - public bool select_prefix_tree (Gtk.TextView view) { + public bool select_current_tree (Gtk.TextView view) { bool pre_existing = true; if (!text_view_words.has_key (view)) { @@ -101,55 +101,51 @@ public class Euclide.Completion.Parser : GLib.Object { pre_existing = false; } - lock (prefix_tree) { - prefix_tree = text_view_words.@get (view); + lock (current_tree) { + current_tree = text_view_words.@get (view); } return pre_existing; } - public void clear () requires (prefix_tree != null) { - lock (prefix_tree) { - prefix_tree.clear (); // Sets completed false + public void clear () requires (current_tree != null) { + lock (current_tree) { + current_tree.clear (); // Sets completed false } parsing_cancelled = false; } - public void set_initial_parsing_completed (bool completed) { - lock (prefix_tree) { - prefix_tree.initial_parse_complete = completed; + public void set_initial_parsing_completed (bool completed) requires (current_tree != null) { + lock (current_tree) { + current_tree.initial_parse_complete = completed; } } - public bool get_initial_parsing_completed () { - return prefix_tree.initial_parse_complete; + public bool get_initial_parsing_completed () requires (current_tree != null) { + return current_tree.initial_parse_complete; } - // public void set_view_words (Gtk.TextView view) requires (prefix_tree != null) { - // text_view_words.@set (view, prefix_tree); - // } - // Fills list with complete words having prefix - public bool get_for_word (string to_find, out List list) { - list = prefix_tree.get_all_matches (to_find); + public bool get_for_word (string to_find, out List list) requires (current_tree != null) { + list = current_tree.get_all_matches (to_find); // list.remove_link (list.find_custom (to_find, strcmp)); return list.first () != null; } - public void add_word (string word) { + public void add_word (string word) requires (current_tree != null) { if (is_valid_word (word)) { - lock (prefix_tree) { - prefix_tree.insert (word); + lock (current_tree) { + current_tree.insert (word); } } } - public void remove_word (string word) requires (word.length > 0) { + public void remove_word (string word) requires (word.length > 0 && current_tree != null) { if (is_valid_word (word)) { - lock (prefix_tree) { + lock (current_tree) { warning ("remove %s", word); - prefix_tree.remove (word); + current_tree.remove (word); } } } diff --git a/plugins/word-completion/meson.build b/plugins/word-completion/meson.build index 247b0e60ad..23a75eee23 100644 --- a/plugins/word-completion/meson.build +++ b/plugins/word-completion/meson.build @@ -2,6 +2,7 @@ module_name = 'word-completion' module_files = [ '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 da030bd198..b7ce4fc093 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -117,9 +117,9 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } /* Wait a bit to allow text to load then run parser*/ - if (!parser.select_prefix_tree (current_view)) { // Returns whether selected prefix tree has been initially parsed + if (!parser.select_current_tree (current_view)) { // Returns whether selected prefix tree has been initially parsed // Start initial parsing after timeout to ensure text loaded - timeout_id = Timeout.add (1000,() => { + timeout_id = Timeout.add (1000, () => { timeout_id = 0; try { new Thread.try ("word-completion-thread", () => { diff --git a/plugins/word-completion/prefix-tree-node.vala b/plugins/word-completion/prefix-tree-node.vala new file mode 100644 index 0000000000..994ac432d7 --- /dev/null +++ b/plugins/word-completion/prefix-tree-node.vala @@ -0,0 +1,168 @@ +/* + * 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 + * License, or (at your option) any later version. + * + * This is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; see the file COPYING. If not, + * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +public class Scratch.Plugins.PrefixNode : Object { + private enum NodeType { + ROOT, + CHAR, + WORD_END + } + + public Gee.ArrayList children; + private unichar? uc = null; + private NodeType type = ROOT; + public uint occurrences { get; set construct; default = 0; } + public PrefixNode? parent { get; construct; default = null; } + + public bool is_word_end { + get { + return type == WORD_END; + } + } + + public bool is_root { + get { + return type == ROOT; + } + } + + public uint length { + get { + return char_s.length; + } + } + + public string char_s { + owned get { + if (uc != null) { + return uc.to_string (); + } else { + return ""; + } + } + } + + public bool has_children { + get { + return type != WORD_END && children.size > 0; + } + } + + public PrefixNode.from_unichar (unichar c, PrefixNode? _parent) requires (c != '\0') { + Object ( + parent: _parent, + occurrences: 1 + ); + + uc = c; + type = CHAR; + } + + public PrefixNode.root () { + Object ( + parent: null, + occurrences: 0 + ); + + type = ROOT; + } + + public PrefixNode.word_end (PrefixNode _parent) { + Object ( + parent: _parent, + occurrences: 1 + ); + + uc = '\0'; + type = WORD_END; + } + + construct { + children = new Gee.ArrayList (); + } + + public bool has_char (unichar c) { + return uc == c; + } + + private void increment () requires (type == WORD_END) { + occurrences++; + } + + private void decrement () requires (type == WORD_END && occurrences > 0) { + occurrences--; + if (occurrences == 0) { + critical ("remove after decrement"); + parent.remove_child (this); + } + } + + private void append_child (owned PrefixNode child) requires (type != WORD_END) { + children.add (child); + } + + private void remove_child (PrefixNode child) requires (type != WORD_END) { + children.remove (child); + } + + public void remove_word_end () requires (this.has_children) { + foreach (var child in children) { + if (child.is_word_end) { + child.decrement (); + return; + } + } + } + + public void insert_word_end () requires (!this.is_word_end && !this.is_root) { + foreach (var child in children) { + if (child.type == WORD_END) { + child.increment (); + return; + } + } + + var new_child = new PrefixNode.word_end (this); + append_child (new_child); + } + + public PrefixNode append_char_child (unichar c) requires (!this.is_word_end) { + foreach (var child in children) { + if (child.has_char (c)) { + return child; + } + } + + var new_child = new PrefixNode.from_unichar (c, this); + append_child (new_child); + return new_child; + } + + public PrefixNode? has_char_child (unichar c) requires (!this.is_word_end) { + foreach (var child in children) { + if (child.has_char (c)) { + return child; + } + } + + return null; + } +} diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index fd17933b3e..5d6eb7556d 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -1,241 +1,111 @@ +/* + * 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 + * License, or (at your option) any later version. + * + * This is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; see the file COPYING. If not, + * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + + public class Scratch.Plugins.PrefixTree : Object { + private PrefixNode? root = null; + public bool initial_parse_complete = false; + + construct { + warning ("construct prefix tree"); + clear (); + } -namespace Scratch.Plugins { - public class PrefixNode : Object { - private enum NodeType { - ROOT, - CHAR, - WORD_END - } - - public Gee.ArrayList children; - private unichar? uc = null; - private NodeType type = ROOT; - public uint occurrences { get; set construct; default = 0; } - public PrefixNode? parent { get; construct; default = null; } - - public bool is_word_end { - get { - return type == WORD_END; - } - } - - public bool is_root { - get { - return type == ROOT; - } - } - - public uint length { - get { - return char_s.length; - } - } - - public string char_s { - owned get { - if (uc != null) { - return uc.to_string (); - } else { - return ""; - } - } - } - - public bool has_children { - get { - return type != WORD_END && children.size > 0; - } - } - - public PrefixNode.from_unichar (unichar c, PrefixNode? _parent) requires (c != '\0') { - Object ( - parent: _parent, - occurrences: 1 - ); - - uc = c; - type = CHAR; - } - - public PrefixNode.root () { - Object ( - parent: null, - occurrences: 0 - ); - - type = ROOT; - } - - public PrefixNode.word_end (PrefixNode _parent) { - Object ( - parent: _parent, - occurrences: 1 - ); - - uc = '\0'; - type = WORD_END; - } - - construct { - children = new Gee.ArrayList (); - } - - public bool has_char (unichar c) { - return uc == c; - } - - private void increment () requires (type == WORD_END) { - occurrences++; - } - - private void decrement () requires (type == WORD_END && occurrences > 0) { - occurrences--; - if (occurrences == 0) { - critical ("remove after decrement"); - parent.remove_child (this); - } - } - - private void append_child (owned PrefixNode child) requires (type != WORD_END) { - children.add (child); - } - - private void remove_child (PrefixNode child) requires (type != WORD_END) { - children.remove (child); - } - - public void remove_word_end () requires (this.has_children) { - foreach (var child in children) { - if (child.is_word_end) { - child.decrement (); - return; - } - } - } - - public void insert_word_end () requires (!this.is_word_end && !this.is_root) { - foreach (var child in children) { - if (child.type == WORD_END) { - child.increment (); - return; - } - } - - var new_child = new PrefixNode.word_end (this); - append_child (new_child); - } - - public PrefixNode append_char_child (unichar c) requires (!this.is_word_end) { - foreach (var child in children) { - if (child.has_char (c)) { - return child; - } - } + public void clear () { + root = new PrefixNode.root (); + initial_parse_complete = false; + } - var new_child = new PrefixNode.from_unichar (c, this); - append_child (new_child); - return new_child; + public void insert (string word) { + if (word.length == 0) { + return; } - public PrefixNode? has_char_child (unichar c) requires (!this.is_word_end) { - foreach (var child in children) { - if (child.has_char (c)) { - return child; - } - } - - return null; - } + this.insert_at (word, this.root); } - public class PrefixTree : Object { - private PrefixNode? root = null; - public bool initial_parse_complete = false; - - construct { - warning ("construct prefix tree"); - clear (); + private void insert_at (string word, PrefixNode node, int i = 0) requires (!node.is_word_end) { + unichar curr = '\0'; + if (!word.get_next_char (ref i, out curr) || curr == '\0') { + node.insert_word_end (); + return; } - public void clear () { - root = new PrefixNode.root (); - initial_parse_complete = false; - } - - public void insert (string word) { - if (word.length == 0) { - return; - } + var child = node.append_char_child (curr); + insert_at (word, child, i); + } - this.insert_at (word, this.root); + public void remove (string word) requires (word.length > 0) { + if (word.length == 0) { + return; } - private void insert_at (string word, PrefixNode node, int i = 0) requires (!node.is_word_end) { - unichar curr = '\0'; - if (!word.get_next_char (ref i, out curr) || curr == '\0') { - node.insert_word_end (); - return; - } + var word_node = find_prefix_at (word, root); - var child = node.append_char_child (curr); - insert_at (word, child, i); + if (word_node != null) { + word_node.remove_word_end (); // Will autoremove unused parents } + } - public void remove (string word) requires (word.length > 0) { - if (word.length == 0) { - return; - } + public bool find_prefix (string prefix) { + return find_prefix_at (prefix, root) != null ? true : false; + } - var word_node = find_prefix_at (word, root); + private PrefixNode? find_prefix_at (string prefix, PrefixNode node, int i = 0) { + unichar curr; - if (word_node != null) { - word_node.remove_word_end (); // Will autoremove unused parents - } + if (!prefix.get_next_char (ref i, out curr)) { + return node; } - public bool find_prefix (string prefix) { - return find_prefix_at (prefix, root) != null ? true : false; + var child = node.has_char_child (curr); + if (child != null) { + return find_prefix_at (prefix, child, i); } - private PrefixNode? find_prefix_at (string prefix, PrefixNode node, int i = 0) { - unichar curr; - - if (!prefix.get_next_char (ref i, out curr)) { - return node; - } - - var child = node.has_char_child (curr); - if (child != null) { - return find_prefix_at (prefix, child, i); - } + return null; + } - return null; + public List get_all_matches (string prefix) { + var list = new List (); + var node = find_prefix_at (prefix, root, 0); + if (node != null && !node.is_word_end) { + var sb = new StringBuilder (prefix); + get_all_matches_rec (node, ref sb, ref list); } - public List get_all_matches (string prefix) { - var list = new List (); - var node = find_prefix_at (prefix, root, 0); - if (node != null && !node.is_word_end) { - var sb = new StringBuilder (prefix); - get_all_matches_rec (node, ref sb, ref list); - } - - return 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.is_word_end) { - matches.append (sbuilder.str); - } else { - sbuilder.append (child.char_s); - get_all_matches_rec (child, ref sbuilder, ref matches); - sbuilder.erase (sbuilder.len - child.length, -1); - } + private void get_all_matches_rec ( + PrefixNode node, + ref StringBuilder sbuilder, + ref List matches) { + + foreach (var child in node.children) { + if (child.is_word_end) { + matches.append (sbuilder.str); + } else { + sbuilder.append (child.char_s); + get_all_matches_rec (child, ref sbuilder, ref matches); + sbuilder.erase (sbuilder.len - child.length, -1); } } } From e742e07f683e6aeefde38f0e51f657f61b5ad5e2 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 28 Nov 2024 20:43:19 +0000 Subject: [PATCH 19/46] Start to fix update after deletions --- plugins/word-completion/engine.vala | 32 +++++--- plugins/word-completion/plugin.vala | 117 ++++++++++++++++------------ 2 files changed, 89 insertions(+), 60 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 8c15dd9a02..b9cae0179d 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -34,20 +34,32 @@ public class Euclide.Completion.Parser : GLib.Object { clear (); if (buffer_text.length > 0) { - set_initial_parsing_completed (parse_text (buffer_text)); + set_initial_parsing_completed (parse_text_and_add (buffer_text)); } else { set_initial_parsing_completed (false); } } - // Returns whether text was completely parsed - private bool parse_text (string text) { + // Returns true if text was completely parsed + public bool parse_text_and_add (string text) { int start_pos = 0; string word = ""; while (!parsing_cancelled && get_next_word (text, ref start_pos, out word)) { + warning ("adding word %s", word); add_word (word); } + return !parsing_cancelled; + } + + // Returns whether text was completely parsed + public bool parse_text_and_remove (string text) { + int start_pos = 0; + string word = ""; + while (!parsing_cancelled && get_next_word (text, ref start_pos, out word)) { + remove_word (word); + } + return parsing_cancelled; } @@ -56,7 +68,7 @@ public class Euclide.Completion.Parser : GLib.Object { if (forward_word_start (text, ref pos)) { var end_pos = pos; forward_word_end (text, ref end_pos); - word = text.slice (pos, end_pos); + word = text.slice (pos, end_pos).strip (); pos = end_pos; return true; } @@ -72,7 +84,7 @@ public class Euclide.Completion.Parser : GLib.Object { return uc != null && !is_delimiter (uc); } - // Returns pointing to char after word + // Returns pointing to char after last char of word private bool forward_word_end (string text, ref int pos) { unichar? uc; while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) { @@ -81,7 +93,6 @@ public class Euclide.Completion.Parser : GLib.Object { while (text.get_next_char (ref pos, out uc) && !is_delimiter (uc)) { } - pos--; return uc == null || is_delimiter (uc); } @@ -133,20 +144,23 @@ public class Euclide.Completion.Parser : GLib.Object { return list.first () != null; } - public void add_word (string word) requires (current_tree != null) { + private void add_word (string word) requires (current_tree != null) { if (is_valid_word (word)) { lock (current_tree) { current_tree.insert (word); } + } else { + warning ("'%s' not added", word); } } - public void remove_word (string word) requires (word.length > 0 && current_tree != null) { + private void remove_word (string word) requires (current_tree != null) { if (is_valid_word (word)) { lock (current_tree) { - warning ("remove %s", word); current_tree.remove (word); } + } else { + warning ("'%s' not removed", word); } } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index b7ce4fc093..02a0df0066 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -21,7 +21,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // DELIMITERS used for word completion are not necessarily the same Pango word breaks // Therefore, we reimplement some iter functions to move between words here below - public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t\'\"`"; + public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t`"; public const int MAX_TOKENS = 1000000; public Object object { owned get; construct; } @@ -74,11 +74,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } parser.cancel_parsing (); - - if (timeout_id > 0) { - GLib.Source.remove (timeout_id); - } - cleanup (); } @@ -117,16 +112,17 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } /* Wait a bit to allow text to load then run parser*/ - if (!parser.select_current_tree (current_view)) { // Returns whether selected prefix tree has been initially parsed + if (!parser.select_current_tree (current_view)) { // Returns false if prefix tree new or parsing not completed // Start initial parsing after timeout to ensure text loaded + warning ("start timeout for %s", current_document.title); timeout_id = Timeout.add (1000, () => { timeout_id = 0; try { new Thread.try ("word-completion-thread", () => { if (current_view != null) { - warning ("%s - initial parsing", provider_name_from_document (current_document)); + warning ("%s - initial parsing", current_document.title); parser.initial_parse_buffer_text (current_view.buffer.text); - warning ("%s - finished initial parsing", provider_name_from_document (current_document)); + warning ("%s - finished initial parsing - completed %s", current_document.title, parser. get_initial_parsing_completed ().to_string ()); } return null; @@ -151,61 +147,78 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } } + Gtk.TextMark start_del_mark = new Gtk.TextMark ("StartDelete", true); + 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); + + if (contains_only_delimiters (del_text)) { + return; + } else { + parser.parse_text_and_remove (del_text); + } + + current_view.buffer.add_mark (start_del_mark, del_start_iter); + } + + private void after_delete_range () { + Gtk.TextIter? iter = null; + // start_del_mark may not have been added + if (!start_del_mark.get_deleted ()) { + current_view.buffer.get_iter_at_mark (out iter, start_del_mark); + var word = ""; + if (iter != null) { + if (iter.starts_word ()) { + var start_iter = iter; + iter.forward_word_end (); + word = start_iter.get_text (iter); + } else if (iter.ends_word ()) { + var end_iter = iter; + iter.backward_word_start (); + word = iter.get_text (end_iter); + } else if (iter.inside_word ()) { + var start_iter = iter; + start_iter.backward_word_start (); + var end_iter = iter; + end_iter.forward_word_end (); + word = start_iter.get_text (end_iter); + } + + if (word != "") { + warning ("got possible new word %s", word); + parser.parse_text_and_add (word); + } + } + + current_view.buffer.delete_mark (start_del_mark); + } + } + private void on_cursor_moved () { + // Ignore moves within when no insertions or we are deleting + // Deletions are handled in `after_delete_range` + if (current_insertion_line < 0 || current_view.buffer.get_mark ("StartDelete") != null) { + return; + } + var insert_offset = current_view.buffer.cursor_position; Gtk.TextIter cursor_iter; current_view.buffer.get_iter_at_offset (out cursor_iter, insert_offset); var temp_iter = cursor_iter; temp_iter.backward_char (); - if (current_insertion_line > -1 && (current_insertion_line != cursor_iter.get_line ())) { + if (current_insertion_line != cursor_iter.get_line ()) { Gtk.TextIter line_start_iter, line_end_iter; current_view.buffer.get_iter_at_line (out line_start_iter, current_insertion_line); line_end_iter = line_start_iter; line_end_iter.forward_to_line_end (); - var line_text = line_start_iter.get_text (line_end_iter).strip (); - - var split_s = line_text.split_set (DELIMITERS, MAX_TOKENS); - foreach (string s in split_s) { - parser.add_word (s); // Ignores invalid words - } - - var retrieved_original_text = retrieve_original_text ().strip (); - var orig_split_s = retrieved_original_text.split_set (DELIMITERS, MAX_TOKENS); - foreach (string s in orig_split_s) { - parser.remove_word (s); - } - } - } - - int start_del_line = -1; - int end_del_line = -1; - 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 line_text = line_start_iter.get_text (line_end_iter); + parser.parse_text_and_add (line_text); - if (contains_only_delimiters (del_text)) { - return; - } - - start_del_line = del_start_iter.get_line (); - end_del_line = del_end_iter.get_line (); - if (end_del_line == start_del_line && current_insertion_line == -1) { - record_original_line_at (del_start_iter); //TODO Handle multiline delete ? rebuild + var retrieved_original_text = retrieve_original_text (); + parser.parse_text_and_remove (retrieved_original_text); } } - private void after_delete_range () { - if (end_del_line > start_del_line) { - current_insertion_line = -1; - original_text = ""; - // warning ("parse view"); - // parse_text_view (); - } - - start_del_line = -1; - end_del_line = -1; - } - private bool contains_only_delimiters (string str) { int i = 0; unichar curr; @@ -235,14 +248,12 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { end_iter.forward_to_line_end (); original_text = start_iter.get_text (end_iter); - warning ("record original text %s", original_text); } private string retrieve_original_text () { var return_s = original_text; original_text = ""; current_insertion_line = -1; - // warning ("retrieved %s", return_s); return return_s; } @@ -251,6 +262,10 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } private void cleanup () { + if (timeout_id > 0) { + GLib.Source.remove (timeout_id); + } + current_view.buffer.insert_text.disconnect (on_insert_text); current_view.buffer.delete_range.disconnect (on_delete_range); current_view.buffer.delete_range.disconnect (after_delete_range); From f0199fac0662dbd05ea0e666a948704fb445ef08 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 29 Nov 2024 11:29:51 +0000 Subject: [PATCH 20/46] Use same delimiters in completionprovider; cleanup --- .../word-completion/completion-provider.vala | 32 ++++++++++++------- plugins/word-completion/plugin.vala | 10 +++--- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index 05468b0eb7..d2fd294c50 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -51,14 +51,29 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, return this.priority; } + private string current_text_to_find = ""; public bool match (Gtk.SourceCompletionContext context) { 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); + int start_pos = buffer.cursor_position; + back_to_word_start (buffer.text, ref start_pos); + current_text_to_find = buffer.text.slice (start_pos, buffer.cursor_position); + var found = parser.match (current_text_to_find); - return parser.match (text); + return found; + } + + private void back_to_word_start (string text, ref int pos) { + unichar uc; + while (text.get_prev_char (ref pos, out uc) && !is_delimiter (uc)) { + } + + pos++; + + return; + } + + private bool is_delimiter (unichar uc) { + return Scratch.Plugins.Completion.DELIMITERS.index_of_char (uc) > -1; } public void populate (Gtk.SourceCompletionContext context) { @@ -118,12 +133,7 @@ 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; - start.backward_word_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); diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 02a0df0066..5c32fb7052 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -105,8 +105,11 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_view.completion.show_headers = true; current_view.completion.show_icons = true; } catch (Error e) { - warning ("Could not add completion provider to %s. %s\n", current_document.title, e.message); - warning ("Not parsing"); + warning ( + "Could not add completion provider to %s. %s\n", + current_document.title, + e.message + ); cleanup (); return; } @@ -114,15 +117,12 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { /* Wait a bit to allow text to load then run parser*/ if (!parser.select_current_tree (current_view)) { // Returns false if prefix tree new or parsing not completed // Start initial parsing after timeout to ensure text loaded - warning ("start timeout for %s", current_document.title); timeout_id = Timeout.add (1000, () => { timeout_id = 0; try { new Thread.try ("word-completion-thread", () => { if (current_view != null) { - warning ("%s - initial parsing", current_document.title); parser.initial_parse_buffer_text (current_view.buffer.text); - warning ("%s - finished initial parsing - completed %s", current_document.title, parser. get_initial_parsing_completed ().to_string ()); } return null; From 8670ef3aed8833d40dada3633a39e4ae82585b33 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 29 Nov 2024 13:27:24 +0000 Subject: [PATCH 21/46] Rework getting completions --- .../word-completion/completion-provider.vala | 17 +++++++----- plugins/word-completion/engine.vala | 16 +++++------- plugins/word-completion/plugin.vala | 1 - plugins/word-completion/prefix-tree-node.vala | 22 ++++++++++++++-- plugins/word-completion/prefix-tree.vala | 26 +++++-------------- 5 files changed, 43 insertions(+), 39 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index d2fd294c50..e51ee98737 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -142,13 +142,16 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, /* 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 */ - 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); + List completions; + if (parser.get_completions_for_prefix (to_find, out completions)) { + 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; diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index b9cae0179d..d2e187bada 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -38,6 +38,8 @@ public class Euclide.Completion.Parser : GLib.Object { } else { set_initial_parsing_completed (false); } + + warning ("initial parsing %s", get_initial_parsing_completed () ? "completed" : "INCOMPLETE"); } // Returns true if text was completely parsed @@ -45,7 +47,6 @@ public class Euclide.Completion.Parser : GLib.Object { int start_pos = 0; string word = ""; while (!parsing_cancelled && get_next_word (text, ref start_pos, out word)) { - warning ("adding word %s", word); add_word (word); } @@ -138,19 +139,18 @@ public class Euclide.Completion.Parser : GLib.Object { } // Fills list with complete words having prefix - public bool get_for_word (string to_find, out List list) requires (current_tree != null) { - list = current_tree.get_all_matches (to_find); - // list.remove_link (list.find_custom (to_find, strcmp)); - return list.first () != null; + public bool get_completions_for_prefix (string prefix, out List completions) requires (current_tree != null) { + completions = current_tree.get_all_completions (prefix); +warning ("engine got %u completions", completions.length ()); + return completions.first () != null; } private void add_word (string word) requires (current_tree != null) { if (is_valid_word (word)) { lock (current_tree) { + warning ("add word %s", word); current_tree.insert (word); } - } else { - warning ("'%s' not added", word); } } @@ -159,8 +159,6 @@ public class Euclide.Completion.Parser : GLib.Object { lock (current_tree) { current_tree.remove (word); } - } else { - warning ("'%s' not removed", word); } } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 5c32fb7052..afa028ac0e 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -184,7 +184,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } if (word != "") { - warning ("got possible new word %s", word); parser.parse_text_and_add (word); } } diff --git a/plugins/word-completion/prefix-tree-node.vala b/plugins/word-completion/prefix-tree-node.vala index 994ac432d7..602cfc6aab 100644 --- a/plugins/word-completion/prefix-tree-node.vala +++ b/plugins/word-completion/prefix-tree-node.vala @@ -26,7 +26,7 @@ public class Scratch.Plugins.PrefixNode : Object { WORD_END } - public Gee.ArrayList children; + private Gee.ArrayList children; private unichar? uc = null; private NodeType type = ROOT; public uint occurrences { get; set construct; default = 0; } @@ -110,7 +110,6 @@ public class Scratch.Plugins.PrefixNode : Object { private void decrement () requires (type == WORD_END && occurrences > 0) { occurrences--; if (occurrences == 0) { - critical ("remove after decrement"); parent.remove_child (this); } } @@ -165,4 +164,23 @@ public class Scratch.Plugins.PrefixNode : Object { return null; } + + // First could with node at the last char of the prefix + public void get_all_completions (ref List completions, ref StringBuilder sb) { + var initial_sb_str = sb.str; + warning ("get all completions for %s", initial_sb_str); + foreach (var child in children) { + if (child.is_word_end) { + if (sb.str.length > 0) { + warning ("word end - appending completion %s", sb.str); + completions.prepend (sb.str); + } + } else { + sb.append (child.char_s); + child.get_all_completions (ref completions, ref sb); + } + + sb.assign (initial_sb_str); + } + } } diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index 5d6eb7556d..b94c8d4905 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -24,7 +24,6 @@ public bool initial_parse_complete = false; construct { - warning ("construct prefix tree"); clear (); } @@ -83,30 +82,17 @@ return null; } - public List get_all_matches (string prefix) { + public List get_all_completions (string prefix) { var list = new List (); var node = find_prefix_at (prefix, root, 0); + warning ("found node for %s - letter is %s", prefix, node.char_s); if (node != null && !node.is_word_end) { - var sb = new StringBuilder (prefix); - get_all_matches_rec (node, ref sb, ref list); + warning ("looking for completions for %s", prefix); + var sb = new StringBuilder (""); + node.get_all_completions (ref list, ref sb); + warning ("got %u completions",list.length ()); } return list; } - - private void get_all_matches_rec ( - PrefixNode node, - ref StringBuilder sbuilder, - ref List matches) { - - foreach (var child in node.children) { - if (child.is_word_end) { - matches.append (sbuilder.str); - } else { - sbuilder.append (child.char_s); - get_all_matches_rec (child, ref sbuilder, ref matches); - sbuilder.erase (sbuilder.len - child.length, -1); - } - } - } } From 95483771287012498a3f4c20c75c85559f7dcba4 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 29 Nov 2024 15:01:01 +0000 Subject: [PATCH 22/46] Reword plugin.after_delete_range; Use fewer iters; Use own delimiters --- .../word-completion/completion-provider.vala | 8 +--- plugins/word-completion/engine.vala | 13 +++-- plugins/word-completion/plugin.vala | 47 ++++++++++++------- plugins/word-completion/prefix-tree.vala | 2 +- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index e51ee98737..14fc20a50f 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -53,7 +53,6 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, private string current_text_to_find = ""; public bool match (Gtk.SourceCompletionContext context) { - Gtk.TextIter start, end; int start_pos = buffer.cursor_position; back_to_word_start (buffer.text, ref start_pos); current_text_to_find = buffer.text.slice (start_pos, buffer.cursor_position); @@ -112,12 +111,9 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, public 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; - iter.backward_word_start (); + var word_start = buffer.cursor_position; + buffer.get_iter_at_offset (out iter, word_start); return true; } diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index d2e187bada..8327b15d53 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -78,15 +78,22 @@ public class Euclide.Completion.Parser : GLib.Object { } // Returns pointing to first char of word - private bool forward_word_start (string text, ref int pos) { + public bool forward_word_start (string text, ref int pos) { unichar? uc; while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) {} pos--; return uc != null && !is_delimiter (uc); } + // Returns pointing to first char of word + public bool backward_word_start (string text, ref int pos) { + unichar? uc; + while (text.get_prev_char (ref pos, out uc) && !is_delimiter (uc)) {} + pos++; + return uc != null && !is_delimiter (uc); + } // Returns pointing to char after last char of word - private bool forward_word_end (string text, ref int pos) { + public bool forward_word_end (string text, ref int pos) { unichar? uc; while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) { } @@ -98,7 +105,7 @@ public class Euclide.Completion.Parser : GLib.Object { } private bool is_delimiter (unichar uc) { - return Scratch.Plugins.Completion.DELIMITERS.index_of_char (uc) > -1; + return Scratch.Plugins.Completion.is_delimiter (uc); } public bool match (string to_find) requires (current_tree != null) { diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index afa028ac0e..0ea2a74d35 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -23,6 +23,9 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // Therefore, we reimplement some iter functions to move between words here below public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t`"; public const int MAX_TOKENS = 1000000; + public static bool is_delimiter (unichar? uc) { + return uc == null || DELIMITERS.index_of_char (uc) > -1; + } public Object object { owned get; construct; } @@ -137,13 +140,13 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } // Runs before default handler so buffer text not yet modified. @pos must not be invalidated - private void on_insert_text (Gtk.TextIter pos, string new_text, int new_text_length) { + private void on_insert_text (Gtk.TextIter iter, string new_text, int new_text_length) { if (contains_only_delimiters (new_text)) { return; } if (current_insertion_line == -1) { - record_original_line_at (pos); + record_original_line_at (iter); } } @@ -166,21 +169,33 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { if (!start_del_mark.get_deleted ()) { current_view.buffer.get_iter_at_mark (out iter, start_del_mark); var word = ""; + unichar? curr = null; + unichar? prev = null; if (iter != null) { - if (iter.starts_word ()) { - var start_iter = iter; - iter.forward_word_end (); - word = start_iter.get_text (iter); - } else if (iter.ends_word ()) { - var end_iter = iter; - iter.backward_word_start (); - word = iter.get_text (end_iter); - } else if (iter.inside_word ()) { - var start_iter = iter; - start_iter.backward_word_start (); - var end_iter = iter; - end_iter.forward_word_end (); - word = start_iter.get_text (end_iter); + var text = current_view.buffer.text; + var pos = iter.get_offset (); + text.get_prev_char (ref pos, out prev); + pos = iter.get_offset (); + text.get_next_char (ref pos, out curr); + + if (is_delimiter (prev) && !is_delimiter (curr)) { // starts word + var start_pos = iter.get_offset (); + pos = start_pos; + if (parser.forward_word_end (text, ref pos)) { + word = text.slice (start_pos, pos); + } + } else if (!is_delimiter (prev) && is_delimiter (curr)) { + var end_pos = iter.get_offset (); + pos = end_pos; + if (parser.backward_word_start (text, ref pos)) { + word = text.slice (pos, end_pos); + } + } else if (!is_delimiter (prev) && !is_delimiter (prev)) { + var start_pos = iter.get_offset (); + var end_pos = start_pos; + if (parser.backward_word_start (text, ref start_pos) && parser.forward_word_end (text, ref end_pos)) { + word = text.slice (start_pos, end_pos); + } } if (word != "") { diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index b94c8d4905..fcce15cfc4 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -90,7 +90,7 @@ warning ("looking for completions for %s", prefix); var sb = new StringBuilder (""); node.get_all_completions (ref list, ref sb); - warning ("got %u completions",list.length ()); + warning ("got %u completions", list.length ()); } return list; From 03d01e21aa64ae972f9a5aa74d2de4adc2ed0bcc Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 29 Nov 2024 15:13:38 +0000 Subject: [PATCH 23/46] Fix initial parsing --- plugins/word-completion/engine.vala | 6 ++++-- plugins/word-completion/prefix-tree.vala | 3 --- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 8327b15d53..46986d3fdf 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -124,7 +124,8 @@ public class Euclide.Completion.Parser : GLib.Object { current_tree = text_view_words.@get (view); } - return pre_existing; + warning ("selected tree for view %s", ((Scratch.Widgets.SourceView)view).location.get_basename ()); + return pre_existing && get_initial_parsing_completed (); } public void clear () requires (current_tree != null) { @@ -137,6 +138,7 @@ public class Euclide.Completion.Parser : GLib.Object { public void set_initial_parsing_completed (bool completed) requires (current_tree != null) { lock (current_tree) { + warning ("setting current tree completed %s", completed.to_string ()); current_tree.initial_parse_complete = completed; } } @@ -155,7 +157,7 @@ warning ("engine got %u completions", completions.length ()); private void add_word (string word) requires (current_tree != null) { if (is_valid_word (word)) { lock (current_tree) { - warning ("add word %s", word); + // warning ("add word %s", word); current_tree.insert (word); } } diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index fcce15cfc4..d0a1d1fd52 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -85,12 +85,9 @@ public List get_all_completions (string prefix) { var list = new List (); var node = find_prefix_at (prefix, root, 0); - warning ("found node for %s - letter is %s", prefix, node.char_s); if (node != null && !node.is_word_end) { - warning ("looking for completions for %s", prefix); var sb = new StringBuilder (""); node.get_all_completions (ref list, ref sb); - warning ("got %u completions", list.length ()); } return list; From b23d9c7a61b4f87b7dea493d31f5a4e7000561e5 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 29 Nov 2024 17:20:50 +0000 Subject: [PATCH 24/46] Different min lengths for stored words and prefixes to trigger completion --- plugins/word-completion/completion-provider.vala | 2 +- plugins/word-completion/engine.vala | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index 14fc20a50f..6ca9204811 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -136,7 +136,7 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, 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 completions; if (parser.get_completions_for_prefix (to_find, out completions)) { diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 46986d3fdf..77a306ac3b 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -20,7 +20,8 @@ */ public class Euclide.Completion.Parser : GLib.Object { - public const uint MINIMUM_WORD_LENGTH = 3; + public const uint MINIMUM_WORD_LENGTH = 4; + public const uint MINIMUM_PREFIX_LENGTH = 1; private Scratch.Plugins.PrefixTree? current_tree = null; public Gee.HashMap text_view_words; public bool parsing_cancelled = false; From 982cf6aaf370ccb6ee74e5a5ab660c6cee39c4d4 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 29 Nov 2024 17:21:37 +0000 Subject: [PATCH 25/46] Handle delimiters inserted after word --- plugins/word-completion/plugin.vala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 0ea2a74d35..bc485567a1 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -142,6 +142,12 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // 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 (contains_only_delimiters (new_text)) { + var text = current_view.buffer.text; + var pos = iter.get_offset (); + parser.backward_word_start (text, ref pos); + var word = text.slice (pos, iter.get_offset ()); + warning ("inserting word %s after insert delimiter", word); + parser.parse_text_and_add (word); return; } From 3c2c42af39ec0d6e54685cafe8f1b1b707957b21 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 29 Nov 2024 18:26:46 +0000 Subject: [PATCH 26/46] Rework completionprovider - overwrite remainder of word --- .../word-completion/completion-provider.vala | 47 ++++++++++++++----- plugins/word-completion/plugin.vala | 31 ++++++------ plugins/word-completion/prefix-tree-node.vala | 2 - 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index 6ca9204811..7ae306f646 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -25,20 +25,35 @@ 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); } @@ -51,7 +66,6 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, return this.priority; } - private string current_text_to_find = ""; public bool match (Gtk.SourceCompletionContext context) { int start_pos = buffer.cursor_position; back_to_word_start (buffer.text, ref start_pos); @@ -85,16 +99,25 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, public 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 (); + unichar? uc; + if (buffer.text.get_next_char (ref end_pos, out uc) && !is_delimiter (uc)) { + warning ("inserting in word"); + parser.forward_word_end (buffer.text, ref end_pos); + buffer.get_iter_at_offset (out end_iter, end_pos); + } 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; } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index bc485567a1..42c33128be 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -23,6 +23,17 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // Therefore, we reimplement some iter functions to move between words here below public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t`"; public const int MAX_TOKENS = 1000000; + private const uint [] ACTIVATE_KEYS = { + Gdk.Key.Return, + Gdk.Key.KP_Enter, + Gdk.Key.ISO_Enter, + Gdk.Key.Tab, + Gdk.Key.KP_Tab, + Gdk.Key.ISO_Left_Tab, + }; + + private const uint REFRESH_SHORTCUT = Gdk.Key.bar; //"|" in combination with will cause refresh + public static bool is_delimiter (unichar? uc) { return uc == null || DELIMITERS.index_of_char (uc) > -1; } @@ -30,24 +41,15 @@ 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 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 bool completion_in_progress = false; - private const uint [] ACTIVATE_KEYS = { - Gdk.Key.Return, - Gdk.Key.KP_Enter, - Gdk.Key.ISO_Enter, - Gdk.Key.Tab, - Gdk.Key.KP_Tab, - Gdk.Key.ISO_Left_Tab, - }; - private const uint REFRESH_SHORTCUT = Gdk.Key.bar; //"|" in combination with will cause refresh + Gtk.TextMark start_del_mark = new Gtk.TextMark ("StartDelete", true); private uint timeout_id = 0; @@ -99,7 +101,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { 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); @@ -156,7 +158,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } } - Gtk.TextMark start_del_mark = new Gtk.TextMark ("StartDelete", true); 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); diff --git a/plugins/word-completion/prefix-tree-node.vala b/plugins/word-completion/prefix-tree-node.vala index 602cfc6aab..6d418b1d70 100644 --- a/plugins/word-completion/prefix-tree-node.vala +++ b/plugins/word-completion/prefix-tree-node.vala @@ -168,11 +168,9 @@ public class Scratch.Plugins.PrefixNode : Object { // First could with node at the last char of the prefix public void get_all_completions (ref List completions, ref StringBuilder sb) { var initial_sb_str = sb.str; - warning ("get all completions for %s", initial_sb_str); foreach (var child in children) { if (child.is_word_end) { if (sb.str.length > 0) { - warning ("word end - appending completion %s", sb.str); completions.prepend (sb.str); } } else { From edee39355f29b40606270afded1f4fce0caa934d Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 29 Nov 2024 20:14:50 +0000 Subject: [PATCH 27/46] Fix delete middle of words --- plugins/word-completion/engine.vala | 11 +- plugins/word-completion/plugin.vala | 143 +++++++++++------------ plugins/word-completion/prefix-tree.vala | 2 +- 3 files changed, 71 insertions(+), 85 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 77a306ac3b..bfd853b663 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -40,7 +40,7 @@ public class Euclide.Completion.Parser : GLib.Object { set_initial_parsing_completed (false); } - warning ("initial parsing %s", get_initial_parsing_completed () ? "completed" : "INCOMPLETE"); + debug ("initial parsing %s", get_initial_parsing_completed () ? "completed" : "INCOMPLETE"); } // Returns true if text was completely parsed @@ -89,8 +89,7 @@ public class Euclide.Completion.Parser : GLib.Object { public bool backward_word_start (string text, ref int pos) { unichar? uc; while (text.get_prev_char (ref pos, out uc) && !is_delimiter (uc)) {} - pos++; - return uc != null && !is_delimiter (uc); + return uc != null && is_delimiter (uc); } // Returns pointing to char after last char of word @@ -110,7 +109,7 @@ public class Euclide.Completion.Parser : GLib.Object { } public bool match (string to_find) requires (current_tree != null) { - return current_tree.find_prefix (to_find); + return current_tree.has_prefix (to_find); } public bool select_current_tree (Gtk.TextView view) { @@ -125,7 +124,6 @@ public class Euclide.Completion.Parser : GLib.Object { current_tree = text_view_words.@get (view); } - warning ("selected tree for view %s", ((Scratch.Widgets.SourceView)view).location.get_basename ()); return pre_existing && get_initial_parsing_completed (); } @@ -139,7 +137,7 @@ public class Euclide.Completion.Parser : GLib.Object { public void set_initial_parsing_completed (bool completed) requires (current_tree != null) { lock (current_tree) { - warning ("setting current tree completed %s", completed.to_string ()); + debug ("setting current tree completed %s", completed.to_string ()); current_tree.initial_parse_complete = completed; } } @@ -151,7 +149,6 @@ public class Euclide.Completion.Parser : GLib.Object { // Fills list with complete words having prefix public bool get_completions_for_prefix (string prefix, out List completions) requires (current_tree != null) { completions = current_tree.get_all_completions (prefix); -warning ("engine got %u completions", completions.length ()); return completions.first () != null; } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 42c33128be..4381c07525 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -72,7 +72,7 @@ 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); + debug ("new source_view %s", doc.title); if (current_view != null) { if (current_view == doc.source_view) { return; @@ -87,7 +87,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_view.buffer.insert_text.connect (on_insert_text); current_view.buffer.delete_range.connect (on_delete_range); current_view.buffer.delete_range.connect_after (after_delete_range); - current_view.buffer.notify["cursor-position"].connect (on_cursor_moved); + // current_view.buffer.notify["cursor-position"].connect (on_cursor_moved); current_view.completion.show.connect (() => { completion_in_progress = true; @@ -143,101 +143,91 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // 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 (contains_only_delimiters (new_text)) { - var text = current_view.buffer.text; - var pos = iter.get_offset (); - parser.backward_word_start (text, ref pos); - var word = text.slice (pos, iter.get_offset ()); - warning ("inserting word %s after insert delimiter", word); - parser.parse_text_and_add (word); - 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; + get_words_before_and_after_pos (text, insert_pos, out word_before, out word_after); + + var text_to_parse = word_before + new_text + word_after; + parser.parse_text_and_add (text_to_parse); + + if (word_before != "" && word_after != "") { + // Word has been broken up and potentially requires removal + parser.parse_text_and_remove (word_before + word_after); + } + } + + // Used by both insertions and deletion handlers + private void get_words_before_and_after_pos ( + string text, + int offset, + out string word_before, + out string word_after + ) { + var pos = offset; + unichar? prev_char = null; + unichar? following_char = null; + word_before = ""; + word_after = ""; + text.get_prev_char (ref pos, out prev_char); + pos = offset; + text.get_next_char (ref pos, out following_char); + warning ("prev char %s, next char %s", prev_char.to_string (), following_char.to_string ()); + var is_word_before = !is_delimiter (prev_char); + var is_word_after = !is_delimiter (following_char); + + if (is_word_before) { + warning ("got word before"); + pos = offset; + warning ("offset %i", offset); + if (parser.backward_word_start (text, ref pos)) { + warning ("pos word start %i", pos); + word_before = text.slice (pos, offset); + } } - if (current_insertion_line == -1) { - record_original_line_at (iter); + if (is_word_after) { + warning ("got word_after"); + pos = offset; + if (parser.forward_word_end (text, ref pos)) { + word_after = text.slice (offset, pos); + } } } 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); - if (contains_only_delimiters (del_text)) { - return; - } else { + if (!contains_only_delimiters (del_text)) { parser.parse_text_and_remove (del_text); } + // Mark for after_delete handler where deletion occurred current_view.buffer.add_mark (start_del_mark, del_start_iter); } private void after_delete_range () { Gtk.TextIter? iter = null; - // start_del_mark may not have been added - if (!start_del_mark.get_deleted ()) { - current_view.buffer.get_iter_at_mark (out iter, start_del_mark); - var word = ""; - unichar? curr = null; - unichar? prev = null; - if (iter != null) { - var text = current_view.buffer.text; - var pos = iter.get_offset (); - text.get_prev_char (ref pos, out prev); - pos = iter.get_offset (); - text.get_next_char (ref pos, out curr); - - if (is_delimiter (prev) && !is_delimiter (curr)) { // starts word - var start_pos = iter.get_offset (); - pos = start_pos; - if (parser.forward_word_end (text, ref pos)) { - word = text.slice (start_pos, pos); - } - } else if (!is_delimiter (prev) && is_delimiter (curr)) { - var end_pos = iter.get_offset (); - pos = end_pos; - if (parser.backward_word_start (text, ref pos)) { - word = text.slice (pos, end_pos); - } - } else if (!is_delimiter (prev) && !is_delimiter (prev)) { - var start_pos = iter.get_offset (); - var end_pos = start_pos; - if (parser.backward_word_start (text, ref start_pos) && parser.forward_word_end (text, ref end_pos)) { - word = text.slice (start_pos, end_pos); - } - } - - if (word != "") { - parser.parse_text_and_add (word); - } - } - - current_view.buffer.delete_mark (start_del_mark); + if (start_del_mark.get_deleted ()) { + critical ("No DeleteMark after deletion"); + return; } - } - private void on_cursor_moved () { - // Ignore moves within when no insertions or we are deleting - // Deletions are handled in `after_delete_range` - if (current_insertion_line < 0 || current_view.buffer.get_mark ("StartDelete") != null) { + // The deleted text has already been parsed and removed from prefix tree + // Need to check whether a new word has been created by deletion + current_view.buffer.get_iter_at_mark (out iter, start_del_mark); + if (iter == null) { + critical ("Unable to get iter from deletion mark"); return; } - var insert_offset = current_view.buffer.cursor_position; - Gtk.TextIter cursor_iter; - current_view.buffer.get_iter_at_offset (out cursor_iter, insert_offset); - var temp_iter = cursor_iter; - temp_iter.backward_char (); - - if (current_insertion_line != cursor_iter.get_line ()) { - Gtk.TextIter line_start_iter, line_end_iter; - current_view.buffer.get_iter_at_line (out line_start_iter, current_insertion_line); - line_end_iter = line_start_iter; - line_end_iter.forward_to_line_end (); - var line_text = line_start_iter.get_text (line_end_iter); - parser.parse_text_and_add (line_text); - - var retrieved_original_text = retrieve_original_text (); - parser.parse_text_and_remove (retrieved_original_text); - } + var delete_pos = iter.get_offset (); + string word_before, word_after; + get_words_before_and_after_pos (current_view.buffer.text, delete_pos, out word_before, out word_after); + // A new word could have been created + parser.parse_text_and_add (word_before + word_after); + current_view.buffer.delete_mark (start_del_mark); } private bool contains_only_delimiters (string str) { @@ -290,7 +280,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_view.buffer.insert_text.disconnect (on_insert_text); current_view.buffer.delete_range.disconnect (on_delete_range); current_view.buffer.delete_range.disconnect (after_delete_range); - current_view.buffer.notify["cursor-position"].disconnect (on_cursor_moved); // Disconnect show completion?? current_view.completion.get_providers ().foreach ((p) => { diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index d0a1d1fd52..501f1c4d1f 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -63,7 +63,7 @@ } } - public bool find_prefix (string prefix) { + public bool has_prefix (string prefix) { return find_prefix_at (prefix, root) != null ? true : false; } From 23ab6a6798e3c2346c8a1db721185d489f0957ce Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 30 Nov 2024 00:27:44 +0000 Subject: [PATCH 28/46] Fix parsing into words --- plugins/word-completion/engine.vala | 70 +++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index bfd853b663..644f29921d 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -44,10 +44,18 @@ public class Euclide.Completion.Parser : GLib.Object { } // Returns true if text was completely parsed - public bool parse_text_and_add (string text) { + public bool parse_text_and_add (string text) requires (text.length > 0) { + if (text.length < MINIMUM_WORD_LENGTH) { + return false; + } + int start_pos = 0; string word = ""; - while (!parsing_cancelled && get_next_word (text, ref start_pos, out word)) { + // Ensure text starts and ends with delimiter - easier to parse; + string to_parse = " " + text + " "; + + while (!parsing_cancelled && get_next_word (to_parse, ref start_pos, out word)) { + debug ("engine add word %s", word); add_word (word); } @@ -58,6 +66,10 @@ public class Euclide.Completion.Parser : GLib.Object { public bool parse_text_and_remove (string text) { int start_pos = 0; string word = ""; + if (text.length < MINIMUM_WORD_LENGTH) { + return false; + } + while (!parsing_cancelled && get_next_word (text, ref start_pos, out word)) { remove_word (word); } @@ -81,29 +93,61 @@ public class Euclide.Completion.Parser : GLib.Object { // Returns pointing to first char of word public bool forward_word_start (string text, ref int pos) { unichar? uc; + while (text.get_next_char (ref pos, out uc) && !is_delimiter (uc)) {} + + if (uc == null) { + return false; + } + + pos--; while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) {} + + if (uc == null) { + return false; + } + pos--; - return uc != null && !is_delimiter (uc); - } - // Returns pointing to first char of word - public bool backward_word_start (string text, ref int pos) { - unichar? uc; - while (text.get_prev_char (ref pos, out uc) && !is_delimiter (uc)) {} - return uc != null && is_delimiter (uc); + return pos < text.length - MINIMUM_WORD_LENGTH; } - // Returns pointing to char after last char of word + // Returns pointing to delimiter (or end of text) after last char of word public bool forward_word_end (string text, ref int pos) { + unichar? uc = text.get_char ((long)pos); + while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) {} + if (uc == null) { + return false; + } + + pos--; + while (text.get_next_char (ref pos, out uc) && !is_delimiter (uc)) {} + if (uc == null) { + return false; + } + + pos--; + return pos < text.length; + } + + // Returns pointing to first char of word + public bool backward_word_start (string text, ref int pos) { unichar? uc; - while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) { + while (text.get_prev_char (ref pos, out uc) && is_delimiter (uc)) {} + if (uc == null) { + return false; } - while (text.get_next_char (ref pos, out uc) && !is_delimiter (uc)) { + pos++; + while (text.get_prev_char (ref pos, out uc) && !is_delimiter (uc)) {} + if (uc == null) { + return false; } - return uc == null || is_delimiter (uc); + pos++; + return true; } + + private bool is_delimiter (unichar uc) { return Scratch.Plugins.Completion.is_delimiter (uc); } From c9f14f03902405d9df640eeaec9789e11e090c6d Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 30 Nov 2024 00:28:39 +0000 Subject: [PATCH 29/46] Fix insertion and deletion --- .../word-completion/completion-provider.vala | 1 - plugins/word-completion/plugin.vala | 41 ++++++++++++------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index 7ae306f646..11724cd295 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -109,7 +109,6 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, var end_pos = end_iter.get_offset (); unichar? uc; if (buffer.text.get_next_char (ref end_pos, out uc) && !is_delimiter (uc)) { - warning ("inserting in word"); parser.forward_word_end (buffer.text, ref end_pos); buffer.get_iter_at_offset (out end_iter, end_pos); } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 4381c07525..afff1130b9 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -72,7 +72,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } public void on_new_source_view (Scratch.Services.Document doc) { - debug ("new source_view %s", doc.title); + debug ("new source_view %s", doc.title); if (current_view != null) { if (current_view == doc.source_view) { return; @@ -152,9 +152,14 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { var text_to_parse = word_before + new_text + word_after; parser.parse_text_and_add (text_to_parse); - if (word_before != "" && word_after != "") { - // Word has been broken up and potentially requires removal - parser.parse_text_and_remove (word_before + word_after); + if (word_before != "" || word_after != "") { + // Word has been altered and potentially requires removal and readding + var to_remove = word_before + word_after; + var to_add = word_before + new_text + word_after; + debug ("remove %s", to_remove); + parser.parse_text_and_remove (to_remove); + debug ("add %s", to_add); + parser.parse_text_and_add (to_add); } } @@ -170,38 +175,43 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { unichar? following_char = null; word_before = ""; word_after = ""; + following_char = text.get_char ((long)offset); text.get_prev_char (ref pos, out prev_char); pos = offset; - text.get_next_char (ref pos, out following_char); - warning ("prev char %s, next char %s", prev_char.to_string (), following_char.to_string ()); var is_word_before = !is_delimiter (prev_char); var is_word_after = !is_delimiter (following_char); + debug ("curr '%s' prev '%s'", following_char.to_string (), prev_char.to_string ()); if (is_word_before) { - warning ("got word before"); pos = offset; - warning ("offset %i", offset); if (parser.backward_word_start (text, ref pos)) { - warning ("pos word start %i", pos); word_before = text.slice (pos, offset); } } if (is_word_after) { - warning ("got word_after"); pos = offset; if (parser.forward_word_end (text, ref pos)) { word_after = text.slice (offset, pos); } } + + debug ("word before %s, after %s", word_before, word_after); } 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); - - if (!contains_only_delimiters (del_text)) { - parser.parse_text_and_remove (del_text); - } + var text = current_view.buffer.text; + var delete_start_pos = del_start_iter.get_offset (); + var delete_end_pos = del_end_iter.get_offset (); + string before, after; + get_words_before_and_after_pos (text, delete_start_pos, out before, out after); + var word_before = before; + get_words_before_and_after_pos (text, delete_end_pos, out before, out after); + var word_after = after; + var to_remove = word_before + del_text + word_after; + warning ("parse and remove %s", to_remove); + parser.parse_text_and_remove (to_remove); // Mark for after_delete handler where deletion occurred current_view.buffer.add_mark (start_del_mark, del_start_iter); @@ -226,7 +236,8 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { string word_before, word_after; get_words_before_and_after_pos (current_view.buffer.text, delete_pos, out word_before, out word_after); // A new word could have been created - parser.parse_text_and_add (word_before + word_after); + var to_add = word_before + word_after; + parser.parse_text_and_add (to_add); current_view.buffer.delete_mark (start_del_mark); } From 5dcac6733c22e730a086c90bbc14bc2d6c175e09 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 30 Nov 2024 19:34:28 +0000 Subject: [PATCH 30/46] Move some code back to engine; move some code into PrefixNode --- .../word-completion/completion-provider.vala | 14 +--- plugins/word-completion/engine.vala | 53 +++++++++++-- plugins/word-completion/plugin.vala | 75 ++++--------------- plugins/word-completion/prefix-tree-node.vala | 75 ++++++++++++++++--- plugins/word-completion/prefix-tree.vala | 56 +++++++------- 5 files changed, 152 insertions(+), 121 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index 11724cd295..fc08a08c0d 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -68,25 +68,15 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, public bool match (Gtk.SourceCompletionContext context) { int start_pos = buffer.cursor_position; - back_to_word_start (buffer.text, ref start_pos); + parser.backward_word_start (buffer.text, ref start_pos); current_text_to_find = buffer.text.slice (start_pos, buffer.cursor_position); var found = parser.match (current_text_to_find); return found; } - private void back_to_word_start (string text, ref int pos) { - unichar uc; - while (text.get_prev_char (ref pos, out uc) && !is_delimiter (uc)) { - } - - pos++; - - return; - } - private bool is_delimiter (unichar uc) { - return Scratch.Plugins.Completion.DELIMITERS.index_of_char (uc) > -1; + return Euclide.Completion.Parser.is_delimiter (uc); } public void populate (Gtk.SourceCompletionContext context) { diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 644f29921d..109274e4d2 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -20,8 +20,15 @@ */ public class Euclide.Completion.Parser : GLib.Object { + // DELIMITERS used for word completion are not necessarily the same Pango word breaks + // Therefore, we reimplement some iter functions to move between words here below + public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t`\"\'"; public const uint MINIMUM_WORD_LENGTH = 4; public const uint MINIMUM_PREFIX_LENGTH = 1; + public static bool is_delimiter (unichar? uc) { + return uc == null || DELIMITERS.index_of_char (uc) > -1; + } + private Scratch.Plugins.PrefixTree? current_tree = null; public Gee.HashMap text_view_words; public bool parsing_cancelled = false; @@ -55,7 +62,7 @@ public class Euclide.Completion.Parser : GLib.Object { string to_parse = " " + text + " "; while (!parsing_cancelled && get_next_word (to_parse, ref start_pos, out word)) { - debug ("engine add word %s", word); + warning ("engine add word %s", word); add_word (word); } @@ -112,7 +119,7 @@ public class Euclide.Completion.Parser : GLib.Object { // Returns pointing to delimiter (or end of text) after last char of word public bool forward_word_end (string text, ref int pos) { - unichar? uc = text.get_char ((long)pos); + unichar? uc; while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) {} if (uc == null) { return false; @@ -146,12 +153,6 @@ public class Euclide.Completion.Parser : GLib.Object { return true; } - - - private bool is_delimiter (unichar uc) { - return Scratch.Plugins.Completion.is_delimiter (uc); - } - public bool match (string to_find) requires (current_tree != null) { return current_tree.has_prefix (to_find); } @@ -196,6 +197,42 @@ public class Euclide.Completion.Parser : GLib.Object { return completions.first () != null; } + public void get_words_before_and_after_pos ( + string text, + int offset, + out string word_before, + out string word_after + ) { + var pos = offset; + unichar? prev_char = null; + unichar? following_char = null; + word_before = ""; + word_after = ""; + text.get_next_char (ref pos, out following_char); + pos = offset; + text.get_prev_char (ref pos, out prev_char); + pos = offset; + var is_word_before = !is_delimiter (prev_char); + var is_word_after = !is_delimiter (following_char); + + debug ("curr '%s' prev '%s'", following_char.to_string (), prev_char.to_string ()); + if (is_word_before) { + pos = offset; + if (backward_word_start (text, ref pos)) { + word_before = text.slice (pos, offset); + } + } + + if (is_word_after) { + pos = offset; + if (forward_word_end (text, ref pos)) { + word_after = text.slice (offset, pos); + } + } + + debug ("word before %s, after %s", word_before, word_after); + } + private void add_word (string word) requires (current_tree != null) { if (is_valid_word (word)) { lock (current_tree) { diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index afff1130b9..49aec9db7f 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -19,9 +19,7 @@ */ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { - // DELIMITERS used for word completion are not necessarily the same Pango word breaks - // Therefore, we reimplement some iter functions to move between words here below - public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t`"; + public const int MAX_TOKENS = 1000000; private const uint [] ACTIVATE_KEYS = { Gdk.Key.Return, @@ -34,9 +32,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { private const uint REFRESH_SHORTCUT = Gdk.Key.bar; //"|" in combination with will cause refresh - public static bool is_delimiter (unichar? uc) { - return uc == null || DELIMITERS.index_of_char (uc) > -1; - } public Object object { owned get; construct; } @@ -147,7 +142,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { var text = current_view.buffer.text; var insert_pos = iter.get_offset (); string word_before, word_after; - get_words_before_and_after_pos (text, insert_pos, out word_before, out word_after); + parser.get_words_before_and_after_pos (text, insert_pos, out word_before, out word_after); var text_to_parse = word_before + new_text + word_after; parser.parse_text_and_add (text_to_parse); @@ -164,51 +159,22 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } // Used by both insertions and deletion handlers - private void get_words_before_and_after_pos ( - string text, - int offset, - out string word_before, - out string word_after - ) { - var pos = offset; - unichar? prev_char = null; - unichar? following_char = null; - word_before = ""; - word_after = ""; - following_char = text.get_char ((long)offset); - text.get_prev_char (ref pos, out prev_char); - pos = offset; - var is_word_before = !is_delimiter (prev_char); - var is_word_after = !is_delimiter (following_char); - - debug ("curr '%s' prev '%s'", following_char.to_string (), prev_char.to_string ()); - if (is_word_before) { - pos = offset; - if (parser.backward_word_start (text, ref pos)) { - word_before = text.slice (pos, offset); - } - } - if (is_word_after) { - pos = offset; - if (parser.forward_word_end (text, ref pos)) { - word_after = text.slice (offset, pos); - } - } - - debug ("word before %s, after %s", word_before, word_after); - } 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 (); + string before, after; - get_words_before_and_after_pos (text, delete_start_pos, out before, out after); + parser.get_words_before_and_after_pos (text, delete_start_pos, out before, out after); var word_before = before; - get_words_before_and_after_pos (text, delete_end_pos, out before, out after); + + // We do not want word in deleted text so get word after delete end separately + parser.get_words_before_and_after_pos (text, delete_end_pos, out before, out after); var word_after = after; + var to_remove = word_before + del_text + word_after; warning ("parse and remove %s", to_remove); parser.parse_text_and_remove (to_remove); @@ -234,30 +200,19 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { var delete_pos = iter.get_offset (); string word_before, word_after; - get_words_before_and_after_pos (current_view.buffer.text, delete_pos, out word_before, out word_after); + parser. get_words_before_and_after_pos ( + current_view.buffer.text, + delete_pos, + out word_before, + out word_after + ); + // A new word could have been created var to_add = word_before + word_after; parser.parse_text_and_add (to_add); current_view.buffer.delete_mark (start_del_mark); } - private bool contains_only_delimiters (string str) { - int i = 0; - unichar curr; - bool found_char = false; - bool has_next_character = false; - do { - has_next_character = str.get_next_char (ref i, out curr); - if (has_next_character) { - if (!(DELIMITERS.contains (curr.to_string ()))) { - found_char = true; - } - } - } while (has_next_character && !found_char); - - return !found_char; - } - private int current_insertion_line = -1; private string original_text = ""; private void record_original_line_at (Gtk.TextIter iter) requires (current_insertion_line < 0) { diff --git a/plugins/word-completion/prefix-tree-node.vala b/plugins/word-completion/prefix-tree-node.vala index 6d418b1d70..818a9fb113 100644 --- a/plugins/word-completion/prefix-tree-node.vala +++ b/plugins/word-completion/prefix-tree-node.vala @@ -122,18 +122,21 @@ public class Scratch.Plugins.PrefixNode : Object { children.remove (child); } - public void remove_word_end () requires (this.has_children) { + private bool remove_or_decrement_word_end () requires (this.has_children) { foreach (var child in children) { if (child.is_word_end) { child.decrement (); - return; + return true; } } + + return false; } - public void insert_word_end () requires (!this.is_word_end && !this.is_root) { + private void append_or_increment_word_end () requires (!this.is_word_end && !this.is_root) { foreach (var child in children) { if (child.type == WORD_END) { + warning ("word end incremented"); child.increment (); return; } @@ -141,18 +144,71 @@ public class Scratch.Plugins.PrefixNode : Object { var new_child = new PrefixNode.word_end (this); append_child (new_child); + warning ("added new word end"); } - public PrefixNode append_char_child (unichar c) requires (!this.is_word_end) { + private PrefixNode? find_or_append_char_child ( + unichar c, + bool append_if_not_found = false + ) requires (!this.is_word_end) { + foreach (var child in children) { if (child.has_char (c)) { return child; } } - var new_child = new PrefixNode.from_unichar (c, this); - append_child (new_child); - return new_child; + if (append_if_not_found) { + var new_child = new PrefixNode.from_unichar (c, this); + append_child (new_child); + return new_child; + } else { + return null; + } + } + + public void insert_word (string text) { + int index = 0; + insert_word_internal (text, ref index); + } + + protected void insert_word_internal (string text, ref int index) { + warning ("insert word internal %s, index %i", text, index); + unichar? uc = null; + if (text.get_next_char (ref index, out uc)) { + var child = find_or_append_char_child (uc, true); // Appends if not found + warning ("recurse"); + child.insert_word_internal (text, ref index); + } else { + append_or_increment_word_end (); + } + } + + public PrefixNode? find_last_node_for (string text) { + warning ("find last node for %s", text); + int index = 0; + return find_last_node_for_internal (text, ref index); + } + + protected PrefixNode? find_last_node_for_internal (string text, ref int index) requires (!this.is_word_end) { + unichar? uc = null; + if (text.get_next_char (ref index, out uc)) { + var child = find_or_append_char_child (uc, false); + if (child == null ) { + critical ("Unable to find node for suffix %s - not found char '%s'", text, uc.to_string ()); + return null; + } else { + return child.find_last_node_for_internal (text, ref index); + } + } else { + warning ("RETURNING THIS"); + return this; + } + } + + public bool remove_word (string text) { + var node = find_last_node_for (text); + return this.remove_or_decrement_word_end (); } public PrefixNode? has_char_child (unichar c) requires (!this.is_word_end) { @@ -168,11 +224,10 @@ public class Scratch.Plugins.PrefixNode : Object { // First could with node at the last char of the prefix public void get_all_completions (ref List completions, ref StringBuilder sb) { var initial_sb_str = sb.str; + warning ("initial sb str %s", initial_sb_str); foreach (var child in children) { if (child.is_word_end) { - if (sb.str.length > 0) { - completions.prepend (sb.str); - } + completions.prepend (sb.str); } else { sb.append (child.char_s); child.get_all_completions (ref completions, ref sb); diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index 501f1c4d1f..6e1fa2967b 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -21,10 +21,12 @@ public class Scratch.Plugins.PrefixTree : Object { private PrefixNode? root = null; + private GLib.StringBuilder sb; public bool initial_parse_complete = false; construct { clear (); + sb = new GLib.StringBuilder (""); } public void clear () { @@ -37,18 +39,7 @@ return; } - this.insert_at (word, this.root); - } - - private void insert_at (string word, PrefixNode node, int i = 0) requires (!node.is_word_end) { - unichar curr = '\0'; - if (!word.get_next_char (ref i, out curr) || curr == '\0') { - node.insert_word_end (); - return; - } - - var child = node.append_char_child (curr); - insert_at (word, child, i); + root.insert_word (word); } public void remove (string word) requires (word.length > 0) { @@ -56,40 +47,43 @@ return; } - var word_node = find_prefix_at (word, root); - - if (word_node != null) { - word_node.remove_word_end (); // Will autoremove unused parents - } + root.remove_word (word); } public bool has_prefix (string prefix) { - return find_prefix_at (prefix, root) != null ? true : false; + return root.find_last_node_for (prefix) != null ? true : false; } - private PrefixNode? find_prefix_at (string prefix, PrefixNode node, int i = 0) { - unichar curr; + // private PrefixNode? find_prefix_at (string prefix, PrefixNode node, int i = 0) { + // unichar curr; - if (!prefix.get_next_char (ref i, out curr)) { - return node; - } + // if (!prefix.get_next_char (ref i, out curr)) { + // return node; + // } - var child = node.has_char_child (curr); - if (child != null) { - return find_prefix_at (prefix, child, i); - } + // var child = node.has_char_child (curr); + // if (child != null) { + // return find_prefix_at (prefix, child, i); + // } - return null; - } + // return null; + // } public List get_all_completions (string prefix) { +warning ("prefix tree get_all_completions for %s", prefix); var list = new List (); - var node = find_prefix_at (prefix, root, 0); + // var node = find_prefix_at (prefix, root, 0); + var node = root.find_last_node_for (prefix); + warning ("node found is %s null", node != null ? "NOT" : ""); if (node != null && !node.is_word_end) { - var sb = new StringBuilder (""); + warning ("erase string builder"); + sb.erase (); node.get_all_completions (ref list, ref sb); + } else { + warning ("node is word end %s", node.is_word_end.to_string ()); } + warning ("returning list length %u", list.length ()); return list; } } From 4b1ffbf3d2153e3f1725365b9a6f095ff64912d8 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 30 Nov 2024 20:37:17 +0000 Subject: [PATCH 31/46] Fix deleting single words --- plugins/word-completion/engine.vala | 45 ++++++++++++------- plugins/word-completion/prefix-tree-node.vala | 27 ++++++----- plugins/word-completion/prefix-tree.vala | 27 +---------- 3 files changed, 45 insertions(+), 54 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 109274e4d2..62455b189d 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -39,7 +39,6 @@ public class Euclide.Completion.Parser : GLib.Object { public void initial_parse_buffer_text (string buffer_text) { parsing_cancelled = false; - clear (); if (buffer_text.length > 0) { set_initial_parsing_completed (parse_text_and_add (buffer_text)); @@ -56,13 +55,11 @@ public class Euclide.Completion.Parser : GLib.Object { return false; } - int start_pos = 0; - string word = ""; // Ensure text starts and ends with delimiter - easier to parse; string to_parse = " " + text + " "; - + int start_pos = 0; + string word = ""; while (!parsing_cancelled && get_next_word (to_parse, ref start_pos, out word)) { - warning ("engine add word %s", word); add_word (word); } @@ -71,13 +68,15 @@ public class Euclide.Completion.Parser : GLib.Object { // Returns whether text was completely parsed public bool parse_text_and_remove (string text) { - int start_pos = 0; - string word = ""; if (text.length < MINIMUM_WORD_LENGTH) { return false; } - while (!parsing_cancelled && get_next_word (text, ref start_pos, out word)) { + // Ensure text starts and ends with delimiter - easier to parse; + string to_parse = " " + text + " "; + int start_pos = 0; + string word = ""; + while (!parsing_cancelled && get_next_word (to_parse, ref start_pos, out word)) { remove_word (word); } @@ -100,14 +99,18 @@ public class Euclide.Completion.Parser : GLib.Object { // Returns pointing to first char of word public bool forward_word_start (string text, ref int pos) { unichar? uc; - while (text.get_next_char (ref pos, out uc) && !is_delimiter (uc)) {} + while (text.get_next_char (ref pos, out uc) && uc != null && !is_delimiter (uc)) { + // warning ("forward word start while not delimiter - pos %i", pos); + } if (uc == null) { return false; } pos--; - while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) {} + while (text.get_next_char (ref pos, out uc) && uc != null && is_delimiter (uc)) { + // warning ("forward word start while is delimiter - pos %i", pos); + } if (uc == null) { return false; @@ -120,13 +123,19 @@ public class Euclide.Completion.Parser : GLib.Object { // Returns pointing to delimiter (or end of text) after last char of word public bool forward_word_end (string text, ref int pos) { unichar? uc; - while (text.get_next_char (ref pos, out uc) && is_delimiter (uc)) {} + while (text.get_next_char (ref pos, out uc) && uc != null && is_delimiter (uc)) { + // warning ("forward word end while delimiter - pos %i", pos); + } + if (uc == null) { return false; } pos--; - while (text.get_next_char (ref pos, out uc) && !is_delimiter (uc)) {} + while (text.get_next_char (ref pos, out uc) && uc != null && !is_delimiter (uc)) { + // warning ("forward word end while not delimiter - pos %i", pos); + } + if (uc == null) { return false; } @@ -138,13 +147,18 @@ public class Euclide.Completion.Parser : GLib.Object { // Returns pointing to first char of word public bool backward_word_start (string text, ref int pos) { unichar? uc; - while (text.get_prev_char (ref pos, out uc) && is_delimiter (uc)) {} + while (text.get_prev_char (ref pos, out uc) && uc != null && is_delimiter (uc)) { + // warning ("backward word start while is delimiter pos %i", pos); + } if (uc == null) { return false; } pos++; - while (text.get_prev_char (ref pos, out uc) && !is_delimiter (uc)) {} + while (text.get_prev_char (ref pos, out uc) && uc != null && !is_delimiter (uc)) { + // warning ("backward word start while is not delimiter pos %i", pos); + } + if (uc == null) { return false; } @@ -215,7 +229,6 @@ public class Euclide.Completion.Parser : GLib.Object { var is_word_before = !is_delimiter (prev_char); var is_word_after = !is_delimiter (following_char); - debug ("curr '%s' prev '%s'", following_char.to_string (), prev_char.to_string ()); if (is_word_before) { pos = offset; if (backward_word_start (text, ref pos)) { @@ -236,7 +249,7 @@ public class Euclide.Completion.Parser : GLib.Object { private void add_word (string word) requires (current_tree != null) { if (is_valid_word (word)) { lock (current_tree) { - // warning ("add word %s", word); + warning ("add word %s", word); current_tree.insert (word); } } diff --git a/plugins/word-completion/prefix-tree-node.vala b/plugins/word-completion/prefix-tree-node.vala index 818a9fb113..5d188c6793 100644 --- a/plugins/word-completion/prefix-tree-node.vala +++ b/plugins/word-completion/prefix-tree-node.vala @@ -99,7 +99,7 @@ public class Scratch.Plugins.PrefixNode : Object { children = new Gee.ArrayList (); } - public bool has_char (unichar c) { + private bool has_char (unichar c) { return uc == c; } @@ -110,6 +110,7 @@ public class Scratch.Plugins.PrefixNode : Object { private void decrement () requires (type == WORD_END && occurrences > 0) { occurrences--; if (occurrences == 0) { + warning ("removing child no longer used"); parent.remove_child (this); } } @@ -118,25 +119,31 @@ public class Scratch.Plugins.PrefixNode : Object { children.add (child); } - private void remove_child (PrefixNode child) requires (type != WORD_END) { + private void remove_child (PrefixNode child) requires (type == CHAR) { children.remove (child); + if (children.is_empty) { + parent.remove_child (this); + } } private bool remove_or_decrement_word_end () requires (this.has_children) { foreach (var child in children) { if (child.is_word_end) { + debug ("found word end - occurrences %u - decrementing", child.occurrences); child.decrement (); + return true; } } + critical ("No word end node found when removing"); + return false; } private void append_or_increment_word_end () requires (!this.is_word_end && !this.is_root) { foreach (var child in children) { if (child.type == WORD_END) { - warning ("word end incremented"); child.increment (); return; } @@ -144,7 +151,6 @@ public class Scratch.Plugins.PrefixNode : Object { var new_child = new PrefixNode.word_end (this); append_child (new_child); - warning ("added new word end"); } private PrefixNode? find_or_append_char_child ( @@ -173,11 +179,9 @@ public class Scratch.Plugins.PrefixNode : Object { } protected void insert_word_internal (string text, ref int index) { - warning ("insert word internal %s, index %i", text, index); unichar? uc = null; if (text.get_next_char (ref index, out uc)) { var child = find_or_append_char_child (uc, true); // Appends if not found - warning ("recurse"); child.insert_word_internal (text, ref index); } else { append_or_increment_word_end (); @@ -185,9 +189,9 @@ public class Scratch.Plugins.PrefixNode : Object { } public PrefixNode? find_last_node_for (string text) { - warning ("find last node for %s", text); int index = 0; - return find_last_node_for_internal (text, ref index); + var res = find_last_node_for_internal (text, ref index); + return res; } protected PrefixNode? find_last_node_for_internal (string text, ref int index) requires (!this.is_word_end) { @@ -195,20 +199,20 @@ public class Scratch.Plugins.PrefixNode : Object { if (text.get_next_char (ref index, out uc)) { var child = find_or_append_char_child (uc, false); if (child == null ) { - critical ("Unable to find node for suffix %s - not found char '%s'", text, uc.to_string ()); return null; } else { return child.find_last_node_for_internal (text, ref index); } } else { - warning ("RETURNING THIS"); return this; } } public bool remove_word (string text) { var node = find_last_node_for (text); - return this.remove_or_decrement_word_end (); + var res = node.remove_or_decrement_word_end (); + warning ("remove %s result %s", text, res.to_string ()); + return res; } public PrefixNode? has_char_child (unichar c) requires (!this.is_word_end) { @@ -224,7 +228,6 @@ public class Scratch.Plugins.PrefixNode : Object { // First could with node at the last char of the prefix public void get_all_completions (ref List completions, ref StringBuilder sb) { var initial_sb_str = sb.str; - warning ("initial sb str %s", initial_sb_str); foreach (var child in children) { if (child.is_word_end) { completions.prepend (sb.str); diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index 6e1fa2967b..4cbd60b20d 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -43,47 +43,22 @@ } public void remove (string word) requires (word.length > 0) { - if (word.length == 0) { - return; - } - - root.remove_word (word); + var res = root.remove_word (word); } public bool has_prefix (string prefix) { return root.find_last_node_for (prefix) != null ? true : false; } - // private PrefixNode? find_prefix_at (string prefix, PrefixNode node, int i = 0) { - // unichar curr; - - // if (!prefix.get_next_char (ref i, out curr)) { - // return node; - // } - - // var child = node.has_char_child (curr); - // if (child != null) { - // return find_prefix_at (prefix, child, i); - // } - - // return null; - // } - public List get_all_completions (string prefix) { -warning ("prefix tree get_all_completions for %s", prefix); var list = new List (); // var node = find_prefix_at (prefix, root, 0); var node = root.find_last_node_for (prefix); - warning ("node found is %s null", node != null ? "NOT" : ""); if (node != null && !node.is_word_end) { - warning ("erase string builder"); sb.erase (); node.get_all_completions (ref list, ref sb); - } else { - warning ("node is word end %s", node.is_word_end.to_string ()); } - warning ("returning list length %u", list.length ()); return list; } } From 178724c081906d5f2d1b7073c4c85a84bade6f44 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sun, 1 Dec 2024 16:31:56 +0000 Subject: [PATCH 32/46] Fix finding words in string --- .../word-completion/completion-provider.vala | 28 +- plugins/word-completion/engine.vala | 245 +++++++++++------- plugins/word-completion/plugin.vala | 111 ++++---- plugins/word-completion/prefix-tree-node.vala | 68 +++-- plugins/word-completion/prefix-tree.vala | 11 +- 5 files changed, 283 insertions(+), 180 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index fc08a08c0d..24d217bd6c 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -67,10 +67,20 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, } public bool match (Gtk.SourceCompletionContext context) { - int start_pos = buffer.cursor_position; - parser.backward_word_start (buffer.text, ref start_pos); - current_text_to_find = buffer.text.slice (start_pos, buffer.cursor_position); - var found = parser.match (current_text_to_find); + int end_pos = buffer.cursor_position; + int start_pos = end_pos; + bool found = false; + var text = buffer.text; + + // parser.backward_word_start (text, ref start_pos); + var preceding_word = parser.get_word_immediately_before (text, start_pos); + if (preceding_word != "") { + // warning ("preceding word found %s", preceding_word); + // current_text_to_find = text.slice (start_pos, end_pos); + found = parser.match (preceding_word); + // warning ("parser match returned %s", found.to_string ()); + current_text_to_find = found ? preceding_word : ""; + } return found; } @@ -80,6 +90,7 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, } public void populate (Gtk.SourceCompletionContext context) { +// warning ("populate"); /*Store current insertion point for use in activate_proposal */ GLib.List? file_props; bool no_minimum = (context.get_activation () == Gtk.SourceCompletionActivation.USER_REQUESTED); @@ -97,10 +108,11 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, // If inserting in middle of word then completion overwrites end of word var end_pos = end_iter.get_offset (); - unichar? uc; - if (buffer.text.get_next_char (ref end_pos, out uc) && !is_delimiter (uc)) { - parser.forward_word_end (buffer.text, ref end_pos); - buffer.get_iter_at_offset (out end_iter, end_pos); + var text = buffer.text; + // If word immediately follows then find offset of end and reset end_iter there + 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); diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 62455b189d..7fc82950f4 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -23,8 +23,9 @@ public class Euclide.Completion.Parser : GLib.Object { // DELIMITERS used for word completion are not necessarily the same Pango word breaks // Therefore, we reimplement some iter functions to move between words here below public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t`\"\'"; - public const uint MINIMUM_WORD_LENGTH = 4; + public const uint MINIMUM_WORD_LENGTH = 3; public const uint MINIMUM_PREFIX_LENGTH = 1; + public const int MAX_TOKENS = 100000; public static bool is_delimiter (unichar? uc) { return uc == null || DELIMITERS.index_of_char (uc) > -1; } @@ -38,6 +39,7 @@ public class Euclide.Completion.Parser : GLib.Object { } public void initial_parse_buffer_text (string buffer_text) { + // warning ("initial parse buffer text %s", buffer_text); parsing_cancelled = false; clear (); if (buffer_text.length > 0) { @@ -46,20 +48,18 @@ public class Euclide.Completion.Parser : GLib.Object { set_initial_parsing_completed (false); } - debug ("initial parsing %s", get_initial_parsing_completed () ? "completed" : "INCOMPLETE"); + warning ("initial parsing %s", get_initial_parsing_completed () ? "completed" : "INCOMPLETE"); } // Returns true if text was completely parsed - public bool parse_text_and_add (string text) requires (text.length > 0) { + public bool parse_text_and_add (string text) { if (text.length < MINIMUM_WORD_LENGTH) { return false; } - // Ensure text starts and ends with delimiter - easier to parse; - string to_parse = " " + text + " "; - int start_pos = 0; + int index = 0; string word = ""; - while (!parsing_cancelled && get_next_word (to_parse, ref start_pos, out word)) { + while (!parsing_cancelled && get_next_word (text, ref index, out word)) { add_word (word); } @@ -83,87 +83,189 @@ public class Euclide.Completion.Parser : GLib.Object { return parsing_cancelled; } + public void get_words_before_and_after_pos ( + string text, + int offset, + out string word_before, + out string word_after + ) { + +// warning ("get words before and after pos %i", offset); + var pos = offset; + word_before = ""; + word_after = ""; + + // Words must be contiguous with start point + if (backward_word_start (text, ref pos, true) && offset >= pos) { + word_before = text.slice (pos, offset); + } + + if (forward_word_end (text, ref pos, true) && pos >= offset) { + word_after = text.slice (offset, pos); + } + +// warning ("got word before %s, word after %s", word_before, word_after); + } + + public string get_word_immediately_before (string text, int end_pos) { + int start_pos = end_pos; + if (backward_word_start (text, ref start_pos, true) && end_pos > start_pos) { + return text.slice (start_pos, end_pos); + } + + return ""; + } + + public string get_word_immediately_after (string text, int start_pos) { + int end_pos = start_pos; + if (forward_word_start (text, ref end_pos, true) && end_pos > start_pos) { + return text.slice (start_pos, end_pos); + } + + return ""; + } + private bool get_next_word (string text, ref int pos, out string word) { word = ""; + // May be delimiters after start point if (forward_word_start (text, ref pos)) { - var end_pos = pos; - forward_word_end (text, ref end_pos); - word = text.slice (pos, end_pos).strip (); - pos = end_pos; + var start = pos; + forward_word_end (text, ref pos); + word = text.slice (start, pos).strip (); +// warning ("found %s", word); return true; } + // warning ("Next word not found"); return false; } - // Returns pointing to first char of word - public bool forward_word_start (string text, ref int pos) { - unichar? uc; - while (text.get_next_char (ref pos, out uc) && uc != null && !is_delimiter (uc)) { - // warning ("forward word start while not delimiter - pos %i", pos); - } - - if (uc == null) { + // Pos could point to beginning, middle or end of text and point + // at a delimiter or non-delimiter. Moves pointer forward to start of next + // Returns pointing BEFORE first char of next word + // Offset is in bytes NOT unichars! + private bool forward_word_start (string text, ref int offset, bool immediate = false) { + if (offset >= text.length - MINIMUM_WORD_LENGTH) { +// warning ("offset too large"); return false; } - pos--; - while (text.get_next_char (ref pos, out uc) && uc != null && is_delimiter (uc)) { - // warning ("forward word start while is delimiter - pos %i", pos); + unichar? uc = null; + bool found = false; + int delimiters = -1; + + // Skip delimiters before word + do { +// warning ("forward word end while IS delimiter - pos %i", offset); + found = text.get_next_char (ref offset, out uc); + delimiters++; + } while (found && is_delimiter (uc)); + + if (immediate && delimiters > 0) { + // warning ("Found preceding delimiters - not immediate"); + return false; } - if (uc == null) { + if (!found) { + // Unable to find next non-delimiter in text - must be end of text +// warning (" no more word starts"); return false; } - pos--; - return pos < text.length - MINIMUM_WORD_LENGTH; + // Skip back + text.get_prev_char (ref offset, out uc); +// warning ("word start after skip back offset %i", offset); + return true; } - // Returns pointing to delimiter (or end of text) after last char of word - public bool forward_word_end (string text, ref int pos) { - unichar? uc; - while (text.get_next_char (ref pos, out uc) && uc != null && is_delimiter (uc)) { - // warning ("forward word end while delimiter - pos %i", pos); + // Pos could point to middle or end of text and point + // at a delimiter or non-delimiter. Moves pointer forward to next word end + // Returns pointing after last char of word + // Offset is in bytes NOT unichars! + private bool forward_word_end (string text, ref int offset, bool immediate = false) { + if (offset >= text.length) { +// warning ("offset too large"); + return false; } - if (uc == null) { + unichar? uc = null; + bool found = false; + int delimiters = -1; + + // Skip delimiters before word + do { +// warning ("forward word end while IS delimiter - pos %i", offset); + found = text.get_next_char (ref offset, out uc); + delimiters++; + } while (found && is_delimiter (uc)); + + if (immediate && delimiters > 0) { + warning ("found following delimiters - not immediate"); return false; } - pos--; - while (text.get_next_char (ref pos, out uc) && uc != null && !is_delimiter (uc)) { - // warning ("forward word end while not delimiter - pos %i", pos); + if (!found) { // Reached end of text without finding target +// warning ("No more word ends"); + return false; } - if (uc == null) { - return false; + // Skip chars in word + do { +// warning ("forward word end while IS char - pos %i", offset); + found = text.get_next_char (ref offset, out uc); + } while (found && !is_delimiter (uc)); + + if (!found) { // Reached end of text without finding target +// warning ("End of text is word end - pos %i", offset); + return true; } - pos--; - return pos < text.length; + // warning ("pos now %i", offset); + + // Pointing after first delimiter after word end - back up if not text end + text.get_prev_char (ref offset, out uc); +// warning ("word end after skip back offset %i", offset); + return true; } // Returns pointing to first char of word - public bool backward_word_start (string text, ref int pos) { - unichar? uc; - while (text.get_prev_char (ref pos, out uc) && uc != null && is_delimiter (uc)) { - // warning ("backward word start while is delimiter pos %i", pos); - } - if (uc == null) { + // Offset is in bytes NOT unichars! + private bool backward_word_start (string text, ref int offset, bool immediate = false) requires (offset > 0) { + unichar? uc = null; + bool found = false; + int delimiters = -1; + // Skip delimiters before start point + do { +// warning ("backward word start while IS delimiter - pos %i", offset); + found = text.get_prev_char (ref offset, out uc); + delimiters++; + } while (found && is_delimiter (uc)); + + if (immediate && delimiters > 0) { + // warning ("Found preceding delimiters - not immediate"); return false; } - pos++; - while (text.get_prev_char (ref pos, out uc) && uc != null && !is_delimiter (uc)) { - // warning ("backward word start while is not delimiter pos %i", pos); + if (!found) { + // Unable to find next non-delimiter before +// warning (" no more word starts"); + return false; } - if (uc == null) { - return false; + // Skip chars before in word + do { +// warning ("forward word end while IS char - pos %i", offset); + found = text.get_prev_char (ref offset, out uc); + } while (found && !is_delimiter (uc)); + + if (!found) { // Reached start of text without finding target - must be word start +// warning ("Start of text is word start - pos %i", offset); + return true; } - pos++; + // Pointing before delimiter before word - skip forward to word start + text.get_next_char (ref offset, out uc); +// warning ("after skip forward offset %i", offset); return true; } @@ -211,51 +313,18 @@ public class Euclide.Completion.Parser : GLib.Object { return completions.first () != null; } - public void get_words_before_and_after_pos ( - string text, - int offset, - out string word_before, - out string word_after - ) { - var pos = offset; - unichar? prev_char = null; - unichar? following_char = null; - word_before = ""; - word_after = ""; - text.get_next_char (ref pos, out following_char); - pos = offset; - text.get_prev_char (ref pos, out prev_char); - pos = offset; - var is_word_before = !is_delimiter (prev_char); - var is_word_after = !is_delimiter (following_char); - - if (is_word_before) { - pos = offset; - if (backward_word_start (text, ref pos)) { - word_before = text.slice (pos, offset); - } - } - - if (is_word_after) { - pos = offset; - if (forward_word_end (text, ref pos)) { - word_after = text.slice (offset, pos); - } - } - - debug ("word before %s, after %s", word_before, word_after); - } - - private void add_word (string word) requires (current_tree != null) { + // Only call if known that @word is a single word + public void add_word (string word) requires (current_tree != null) { if (is_valid_word (word)) { lock (current_tree) { - warning ("add word %s", word); + // warning ("add word %s", word); current_tree.insert (word); } } } - private void remove_word (string word) requires (current_tree != null) { + // only call if known that @word is a single word + public void remove_word (string word) requires (current_tree != null) { if (is_valid_word (word)) { lock (current_tree) { current_tree.remove (word); diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 49aec9db7f..4cfaee20b6 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -44,7 +44,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { private bool completion_in_progress = false; - Gtk.TextMark start_del_mark = new Gtk.TextMark ("StartDelete", true); + // Gtk.TextMark start_del_mark = new Gtk.TextMark ("StartDelete", true); private uint timeout_id = 0; @@ -67,7 +67,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } public void on_new_source_view (Scratch.Services.Document doc) { - debug ("new source_view %s", doc.title); + // warning ("new source_view %s", doc.title); if (current_view != null) { if (current_view == doc.source_view) { return; @@ -105,7 +105,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_view.completion.show_headers = true; current_view.completion.show_icons = true; } catch (Error e) { - warning ( + critical ( "Could not add completion provider to %s. %s\n", current_document.title, e.message @@ -138,23 +138,31 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // 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) { + // warning ("on insert text %s", new_text); + if (!parser.get_initial_parsing_completed ()) { + // warning ("ignore 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_parse = word_before + new_text + word_after; - parser.parse_text_and_add (text_to_parse); - - if (word_before != "" || word_after != "") { - // Word has been altered and potentially requires removal and readding - var to_remove = word_before + word_after; - var to_add = word_before + new_text + word_after; - debug ("remove %s", to_remove); - parser.parse_text_and_remove (to_remove); - debug ("add %s", to_add); - parser.parse_text_and_add (to_add); + var text_to_add = (word_before + new_text + word_after).strip (); + // warning ("add text to parse %s", text_to_parse); + // Inserted text could contain delimiters so parse before adding + + var text_to_remove = (word_before + word_after).strip (); + // Only update if words have changed + if (text_to_add != text_to_remove) { + // warning ("after insert remove %s", to_remove); + // We know this does not contain delimiters + // warning ("adding %s, removing %s", text_to_add, text_to_remove); + // Text to add may contain delimiters so parse + parser.parse_text_and_add (text_to_add); + // We know text to remove does not contain delimiters + parser.remove_word (text_to_remove); } } @@ -167,50 +175,53 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { var delete_start_pos = del_start_iter.get_offset (); var delete_end_pos = del_end_iter.get_offset (); - string before, after; - parser.get_words_before_and_after_pos (text, delete_start_pos, out before, out after); - var word_before = before; + // string before, after; + // parser.get_words_before_and_after_pos (text, delete_start_pos, out before, out after); + var word_before = parser.get_word_immediately_before (text, delete_start_pos); - // We do not want word in deleted text so get word after delete end separately - parser.get_words_before_and_after_pos (text, delete_end_pos, out before, out after); - var word_after = after; + // // We do not want word in deleted text so get word after delete end separately + // parser.get_words_before_and_after_pos (text, delete_end_pos, out before, out after); + var word_after = parser.get_word_immediately_after (text, delete_end_pos); var to_remove = word_before + del_text + word_after; - warning ("parse and remove %s", to_remove); parser.parse_text_and_remove (to_remove); - // Mark for after_delete handler where deletion occurred - current_view.buffer.add_mark (start_del_mark, del_start_iter); + // A new word could have been created + var to_add = word_before + word_after; + parser.add_word (to_add); + + // // Mark for after_delete handler where deletion occurred + // current_view.buffer.add_mark (start_del_mark, del_start_iter); } private void after_delete_range () { - Gtk.TextIter? iter = null; - if (start_del_mark.get_deleted ()) { - critical ("No DeleteMark after deletion"); - return; - } - - // The deleted text has already been parsed and removed from prefix tree - // Need to check whether a new word has been created by deletion - current_view.buffer.get_iter_at_mark (out iter, start_del_mark); - if (iter == null) { - critical ("Unable to get iter from deletion mark"); - return; - } - - var delete_pos = iter.get_offset (); - string word_before, word_after; - parser. get_words_before_and_after_pos ( - current_view.buffer.text, - delete_pos, - out word_before, - out word_after - ); - - // A new word could have been created - var to_add = word_before + word_after; - parser.parse_text_and_add (to_add); - current_view.buffer.delete_mark (start_del_mark); + // Gtk.TextIter? iter = null; + // if (start_del_mark.get_deleted ()) { + // critical ("No DeleteMark after deletion"); + // return; + // } + + // // The deleted text has already been parsed and removed from prefix tree + // // Need to check whether a new word has been created by deletion + // current_view.buffer.get_iter_at_mark (out iter, start_del_mark); + // if (iter == null) { + // critical ("Unable to get iter from deletion mark"); + // return; + // } + + // var delete_pos = iter.get_offset (); + // string word_before, word_after; + // parser. get_words_before_and_after_pos ( + // current_view.buffer.text, + // delete_pos, + // out word_before, + // out word_after + // ); + + // // A new word could have been created + // var to_add = word_before + word_after; + // parser.parse_text_and_add (to_add); + // current_view.buffer.delete_mark (start_del_mark); } private int current_insertion_line = -1; diff --git a/plugins/word-completion/prefix-tree-node.vala b/plugins/word-completion/prefix-tree-node.vala index 5d188c6793..bd7a165331 100644 --- a/plugins/word-completion/prefix-tree-node.vala +++ b/plugins/word-completion/prefix-tree-node.vala @@ -32,17 +32,17 @@ public class Scratch.Plugins.PrefixNode : Object { public uint occurrences { get; set construct; default = 0; } public PrefixNode? parent { get; construct; default = null; } - public bool is_word_end { - get { - return type == WORD_END; - } - } - - public bool is_root { - get { - return type == ROOT; - } - } + // public bool is_word_end { + // get { + // return type == WORD_END; + // } + // } + + // public bool is_root { + // get { + // return type == ROOT; + // } + // } public uint length { get { @@ -104,32 +104,41 @@ public class Scratch.Plugins.PrefixNode : Object { } private void increment () requires (type == WORD_END) { - occurrences++; + lock (occurrences) { + occurrences++; + } } private void decrement () requires (type == WORD_END && occurrences > 0) { - occurrences--; + lock (occurrences) { + occurrences--; + } + if (occurrences == 0) { - warning ("removing child no longer used"); + // critical ("removing child no longer used"); parent.remove_child (this); } } private void append_child (owned PrefixNode child) requires (type != WORD_END) { - children.add (child); + lock (children) { + children.add (child); + } } - private void remove_child (PrefixNode child) requires (type == CHAR) { - children.remove (child); - if (children.is_empty) { - parent.remove_child (this); + private void remove_child (PrefixNode child) requires (type != WORD_END) { + lock (children) { + children.remove (child); + if (children.is_empty && type != ROOT) { + parent.remove_child (this); + } } } private bool remove_or_decrement_word_end () requires (this.has_children) { foreach (var child in children) { - if (child.is_word_end) { - debug ("found word end - occurrences %u - decrementing", child.occurrences); + if (child.type == WORD_END) { +// warning ("found word end - occurrences %u - decrementing", child.occurrences); child.decrement (); return true; @@ -141,9 +150,10 @@ public class Scratch.Plugins.PrefixNode : Object { return false; } - private void append_or_increment_word_end () requires (!this.is_word_end && !this.is_root) { + private void append_or_increment_word_end () requires (type != WORD_END && type != ROOT) { foreach (var child in children) { if (child.type == WORD_END) { +// warning ("incrementing child occurrence"); child.increment (); return; } @@ -156,7 +166,7 @@ public class Scratch.Plugins.PrefixNode : Object { private PrefixNode? find_or_append_char_child ( unichar c, bool append_if_not_found = false - ) requires (!this.is_word_end) { + ) requires (type != WORD_END) { foreach (var child in children) { if (child.has_char (c)) { @@ -194,7 +204,7 @@ public class Scratch.Plugins.PrefixNode : Object { return res; } - protected PrefixNode? find_last_node_for_internal (string text, ref int index) requires (!this.is_word_end) { + protected PrefixNode? find_last_node_for_internal (string text, ref int index) requires (type != WORD_END) { unichar? uc = null; if (text.get_next_char (ref index, out uc)) { var child = find_or_append_char_child (uc, false); @@ -211,11 +221,11 @@ public class Scratch.Plugins.PrefixNode : Object { public bool remove_word (string text) { var node = find_last_node_for (text); var res = node.remove_or_decrement_word_end (); - warning ("remove %s result %s", text, res.to_string ()); + // warning ("remove %s result %s", text, res.to_string ()); return res; } - public PrefixNode? has_char_child (unichar c) requires (!this.is_word_end) { + public PrefixNode? has_char_child (unichar c) requires (type != WORD_END) { foreach (var child in children) { if (child.has_char (c)) { return child; @@ -227,9 +237,13 @@ public class Scratch.Plugins.PrefixNode : Object { // First could with node at the last char of the prefix public void get_all_completions (ref List completions, ref StringBuilder sb) { + if (type == WORD_END) { + return; + } + var initial_sb_str = sb.str; foreach (var child in children) { - if (child.is_word_end) { + if (child.type == WORD_END) { completions.prepend (sb.str); } else { sb.append (child.char_s); diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index 4cbd60b20d..abfb94c6c2 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -34,15 +34,13 @@ initial_parse_complete = false; } - public void insert (string word) { - if (word.length == 0) { - return; - } - + public void insert (string word) requires (word.length > 0) { + warning ("add '%s' to root", word); root.insert_word (word); } public void remove (string word) requires (word.length > 0) { + warning ("remove '%s' from root", word); var res = root.remove_word (word); } @@ -52,9 +50,8 @@ public List get_all_completions (string prefix) { var list = new List (); - // var node = find_prefix_at (prefix, root, 0); var node = root.find_last_node_for (prefix); - if (node != null && !node.is_word_end) { + if (node != null) { sb.erase (); node.get_all_completions (ref list, ref sb); } From 307774b559b858fa1fdb166587d71e427089e9d9 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 3 Dec 2024 11:43:05 +0000 Subject: [PATCH 33/46] Incorporate changes from experiments --- .../word-completion/completion-provider.vala | 34 +- plugins/word-completion/engine.vala | 333 ++++++------------ plugins/word-completion/plugin.vala | 88 +---- plugins/word-completion/prefix-tree-node.vala | 66 ++-- plugins/word-completion/prefix-tree.vala | 93 ++++- 5 files changed, 243 insertions(+), 371 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index 24d217bd6c..8f9625ecd0 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 @@ -58,39 +59,30 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, 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) { + 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; - // parser.backward_word_start (text, ref start_pos); var preceding_word = parser.get_word_immediately_before (text, start_pos); if (preceding_word != "") { - // warning ("preceding word found %s", preceding_word); - // current_text_to_find = text.slice (start_pos, end_pos); found = parser.match (preceding_word); - // warning ("parser match returned %s", found.to_string ()); current_text_to_find = found ? preceding_word : ""; } return found; } - private bool is_delimiter (unichar uc) { - return Euclide.Completion.Parser.is_delimiter (uc); - } - - public void populate (Gtk.SourceCompletionContext context) { -// warning ("populate"); + 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); @@ -98,7 +90,7 @@ 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_iter; Gtk.TextMark mark; @@ -109,7 +101,6 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, // If inserting in middle of word then completion overwrites end of word var end_pos = end_iter.get_offset (); var text = buffer.text; - // If word immediately follows then find offset of end and reset end_iter there 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); @@ -123,16 +114,16 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, 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) { @@ -162,8 +153,8 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, /* 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_PREFIX_LENGTH) { /* Get proposals, if any */ - List completions; - if (parser.get_completions_for_prefix (to_find, out completions)) { + 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 (); @@ -177,6 +168,7 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, return true; } } + return false; } } diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 7fc82950f4..bafcdc9c3a 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -23,8 +23,9 @@ public class Euclide.Completion.Parser : GLib.Object { // DELIMITERS used for word completion are not necessarily the same Pango word breaks // Therefore, we reimplement some iter functions to move between words here below public const string DELIMITERS = " .,;:?{}[]()+=&|<>*\\/\r\n\t`\"\'"; - public const uint MINIMUM_WORD_LENGTH = 3; - public const uint MINIMUM_PREFIX_LENGTH = 1; + public const int MINIMUM_WORD_LENGTH = 3; + public const int MAXIMUM_WORD_LENGTH = 50; + public const int MINIMUM_PREFIX_LENGTH = 1; public const int MAX_TOKENS = 100000; public static bool is_delimiter (unichar? uc) { return uc == null || DELIMITERS.index_of_char (uc) > -1; @@ -38,49 +39,72 @@ public class Euclide.Completion.Parser : GLib.Object { text_view_words = new Gee.HashMap (); } + public bool select_current_tree (Gtk.TextView view) { + bool pre_existing = true; + if (!text_view_words.has_key (view)) { + var new_treemap = new Scratch.Plugins.PrefixTree (); + 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 void set_initial_parsing_completed (bool completed) requires (current_tree != null) { + lock (current_tree) { + current_tree.completed = completed; + } + } + + public bool get_initial_parsing_completed () requires (current_tree != null) { + return current_tree.completed; + } + public void initial_parse_buffer_text (string buffer_text) { - // warning ("initial parse buffer text %s", buffer_text); parsing_cancelled = false; clear (); if (buffer_text.length > 0) { - set_initial_parsing_completed (parse_text_and_add (buffer_text)); + var parsed = parse_text_and_add (buffer_text); + set_initial_parsing_completed (parsed); } else { - set_initial_parsing_completed (false); + // Assume any buffer text would have been loaded when this is called + // so definitely no initial parse needed + set_initial_parsing_completed (true); } - warning ("initial parsing %s", get_initial_parsing_completed () ? "completed" : "INCOMPLETE"); + 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) { - if (text.length < MINIMUM_WORD_LENGTH) { - return false; - } - int index = 0; - string word = ""; - while (!parsing_cancelled && get_next_word (text, ref index, out word)) { - add_word (word); + 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 !parsing_cancelled; + return index == n_words; } - // Returns whether text was completely parsed - public bool parse_text_and_remove (string text) { + public void parse_text_and_remove (string text) { if (text.length < MINIMUM_WORD_LENGTH) { - return false; + return; } - // Ensure text starts and ends with delimiter - easier to parse; - string to_parse = " " + text + " "; - int start_pos = 0; - string word = ""; - while (!parsing_cancelled && get_next_word (to_parse, ref start_pos, out word)) { - remove_word (word); + int index = 0; + string[] words = text.split_set (DELIMITERS); + uint n_words = words.length; + while (index < n_words) { + remove_word (words[index++]); } - return parsing_cancelled; + return; } public void get_words_before_and_after_pos ( @@ -89,246 +113,97 @@ public class Euclide.Completion.Parser : GLib.Object { out string word_before, out string word_after ) { - -// warning ("get words before and after pos %i", offset); - var pos = offset; - word_before = ""; - word_after = ""; - - // Words must be contiguous with start point - if (backward_word_start (text, ref pos, true) && offset >= pos) { - word_before = text.slice (pos, offset); - } - - if (forward_word_end (text, ref pos, true) && pos >= offset) { - word_after = text.slice (offset, pos); - } - -// warning ("got word before %s, word after %s", word_before, 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) { - int start_pos = end_pos; - if (backward_word_start (text, ref start_pos, true) && end_pos > start_pos) { - return text.slice (start_pos, end_pos); - } - - return ""; - } - - public string get_word_immediately_after (string text, int start_pos) { - int end_pos = start_pos; - if (forward_word_start (text, ref end_pos, true) && end_pos > start_pos) { - return text.slice (start_pos, end_pos); - } - - return ""; - } - - private bool get_next_word (string text, ref int pos, out string word) { - word = ""; - // May be delimiters after start point - if (forward_word_start (text, ref pos)) { - var start = pos; - forward_word_end (text, ref pos); - word = text.slice (start, pos).strip (); -// warning ("found %s", word); - return true; - } - - // warning ("Next word not found"); - return false; - } - - // Pos could point to beginning, middle or end of text and point - // at a delimiter or non-delimiter. Moves pointer forward to start of next - // Returns pointing BEFORE first char of next word - // Offset is in bytes NOT unichars! - private bool forward_word_start (string text, ref int offset, bool immediate = false) { - if (offset >= text.length - MINIMUM_WORD_LENGTH) { -// warning ("offset too large"); - return false; - } - - unichar? uc = null; - bool found = false; - int delimiters = -1; - - // Skip delimiters before word - do { -// warning ("forward word end while IS delimiter - pos %i", offset); - found = text.get_next_char (ref offset, out uc); - delimiters++; - } while (found && is_delimiter (uc)); - - if (immediate && delimiters > 0) { - // warning ("Found preceding delimiters - not immediate"); - return false; - } - - if (!found) { - // Unable to find next non-delimiter in text - must be end of text -// warning (" no more word starts"); - return false; - } - - // Skip back - text.get_prev_char (ref offset, out uc); -// warning ("word start after skip back offset %i", offset); - return true; - } - - // Pos could point to middle or end of text and point - // at a delimiter or non-delimiter. Moves pointer forward to next word end - // Returns pointing after last char of word - // Offset is in bytes NOT unichars! - private bool forward_word_end (string text, ref int offset, bool immediate = false) { - if (offset >= text.length) { -// warning ("offset too large"); - return false; + if (end_pos < 1) { + return ""; } - unichar? uc = null; - bool found = false; - int delimiters = -1; - - // Skip delimiters before word - do { -// warning ("forward word end while IS delimiter - pos %i", offset); - found = text.get_next_char (ref offset, out uc); - delimiters++; - } while (found && is_delimiter (uc)); - - if (immediate && delimiters > 0) { - warning ("found following delimiters - not immediate"); - return false; + int pos = end_pos; + unichar uc; + text.get_prev_char (ref pos, out uc); + if (is_delimiter (uc)) { + return ""; } - if (!found) { // Reached end of text without finding target -// warning ("No more word ends"); - return false; + pos = (end_pos - MAXIMUM_WORD_LENGTH - 1).clamp (0, end_pos); + if (pos >= end_pos) { + critical ("pos after end_pos"); + return ""; } - // Skip chars in word - do { -// warning ("forward word end while IS char - pos %i", offset); - found = text.get_next_char (ref offset, out uc); - } while (found && !is_delimiter (uc)); - - if (!found) { // Reached end of text without finding target -// warning ("End of text is word end - pos %i", offset); - return true; - } - - // warning ("pos now %i", offset); - - // Pointing after first delimiter after word end - back up if not text end - text.get_prev_char (ref offset, out uc); -// warning ("word end after skip back offset %i", offset); - return true; + 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 (); } - // Returns pointing to first char of word - // Offset is in bytes NOT unichars! - private bool backward_word_start (string text, ref int offset, bool immediate = false) requires (offset > 0) { - unichar? uc = null; - bool found = false; - int delimiters = -1; - // Skip delimiters before start point - do { -// warning ("backward word start while IS delimiter - pos %i", offset); - found = text.get_prev_char (ref offset, out uc); - delimiters++; - } while (found && is_delimiter (uc)); - - if (immediate && delimiters > 0) { - // warning ("Found preceding delimiters - not immediate"); - return false; - } - - if (!found) { - // Unable to find next non-delimiter before -// warning (" no more word starts"); - return false; - } - - // Skip chars before in word - do { -// warning ("forward word end while IS char - pos %i", offset); - found = text.get_prev_char (ref offset, out uc); - } while (found && !is_delimiter (uc)); - - if (!found) { // Reached start of text without finding target - must be word start -// warning ("Start of text is word start - pos %i", offset); - return true; + public string get_word_immediately_after (string text, int start_pos) { + if (start_pos < 0 || start_pos > text.length - 1) { + return ""; } - // Pointing before delimiter before word - skip forward to word start - text.get_next_char (ref offset, out uc); -// warning ("after skip forward offset %i", offset); - return true; - } - - public bool match (string to_find) requires (current_tree != null) { - return current_tree.has_prefix (to_find); - } - - public bool select_current_tree (Gtk.TextView view) { - bool pre_existing = true; - - if (!text_view_words.has_key (view)) { - text_view_words.@set (view, new Scratch.Plugins.PrefixTree ()); - pre_existing = false; + int pos = start_pos; + unichar uc; + text.get_next_char (ref pos, out uc); + if (is_delimiter (uc)) { + return ""; } - lock (current_tree) { - current_tree = text_view_words.@get (view); + pos = (start_pos + MAXIMUM_WORD_LENGTH + 1).clamp (start_pos, text.length); + if (start_pos >= pos) { + critical ("start pos after pos"); + return ""; } - return pre_existing && get_initial_parsing_completed (); + 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_parsing (); lock (current_tree) { current_tree.clear (); // Sets completed false + set_initial_parsing_completed (false); + } parsing_cancelled = false; } - public void set_initial_parsing_completed (bool completed) requires (current_tree != null) { - lock (current_tree) { - debug ("setting current tree completed %s", completed.to_string ()); - current_tree.initial_parse_complete = completed; - } + public void cancel_parsing () { + // Do not need to cancel reaping - this continues when prefix_tree is not current + parsing_cancelled = true; } - public bool get_initial_parsing_completed () requires (current_tree != null) { - return current_tree.initial_parse_complete; + private List current_completions; + public bool match (string prefix) requires (current_tree != null) { + current_completions = current_tree.get_all_completions (prefix); + return current_completions != null && current_completions.first ().data != null; } - // Fills list with complete words having prefix - public bool get_completions_for_prefix (string prefix, out List completions) requires (current_tree != null) { - completions = current_tree.get_all_completions (prefix); - return completions.first () != null; + public List get_completions_for_prefix (string prefix) requires (current_tree != null) { + // Assume always preceded by match and current_completions up to date + return (owned)current_completions; } - - // Only call if known that @word is a single word - public void add_word (string word) requires (current_tree != null) { - if (is_valid_word (word)) { + + public void add_word (string word_to_add) requires (current_tree != null) { + if (is_valid_word (word_to_add)) { lock (current_tree) { - // warning ("add word %s", word); - current_tree.insert (word); + current_tree.add_word (word_to_add); } } } - // only call if known that @word is a single word - public void remove_word (string word) requires (current_tree != null) { - if (is_valid_word (word)) { - lock (current_tree) { - current_tree.remove (word); - } + public void remove_word (string word_to_remove) requires (current_tree != null) { + lock (current_tree) { + current_tree.remove_word (word_to_remove); } } @@ -344,8 +219,4 @@ public class Euclide.Completion.Parser : GLib.Object { return true; } - - public void cancel_parsing () { - parsing_cancelled = true; - } } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 4cfaee20b6..0f1e570300 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -43,9 +43,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { private Scratch.Services.Interface plugins; private bool completion_in_progress = false; - - // Gtk.TextMark start_del_mark = new Gtk.TextMark ("StartDelete", true); - private uint timeout_id = 0; public void activate () { @@ -67,7 +64,7 @@ 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); + debug ("new source_view %s", doc !=null ? doc.title : "null"); if (current_view != null) { if (current_view == doc.source_view) { return; @@ -81,8 +78,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_view = doc.source_view; current_view.buffer.insert_text.connect (on_insert_text); current_view.buffer.delete_range.connect (on_delete_range); - current_view.buffer.delete_range.connect_after (after_delete_range); - // current_view.buffer.notify["cursor-position"].connect (on_cursor_moved); current_view.completion.show.connect (() => { completion_in_progress = true; @@ -138,9 +133,8 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // 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) { - // warning ("on insert text %s", new_text); if (!parser.get_initial_parsing_completed ()) { - // warning ("ignore spurious insertions when doc loading"); + // Ignore spurious insertions when doc loading return; } // Determine whether insertion point ends and/or starts a word @@ -148,101 +142,36 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { 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 (); - // warning ("add text to parse %s", text_to_parse); - // Inserted text could contain delimiters so parse before adding - var text_to_remove = (word_before + word_after).strip (); + // Only update if words have changed if (text_to_add != text_to_remove) { - // warning ("after insert remove %s", to_remove); - // We know this does not contain delimiters - // warning ("adding %s, removing %s", text_to_add, text_to_remove); + debug ("adding %s, removing %s", text_to_add, text_to_remove); // Text to add may contain delimiters so parse parser.parse_text_and_add (text_to_add); + debug ("after insert remove %s", text_to_remove); // We know text to remove does not contain delimiters parser.remove_word (text_to_remove); } } - // Used by both insertions and deletion handlers - - 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 (); - - // string before, after; - // parser.get_words_before_and_after_pos (text, delete_start_pos, out before, out after); var word_before = parser.get_word_immediately_before (text, delete_start_pos); - - // // We do not want word in deleted text so get word after delete end separately - // parser.get_words_before_and_after_pos (text, delete_end_pos, out before, out after); var word_after = parser.get_word_immediately_after (text, delete_end_pos); var to_remove = word_before + del_text + word_after; + debug ("delete range: remove %s", to_remove); parser.parse_text_and_remove (to_remove); // A new word could have been created var to_add = word_before + word_after; + debug ("delete range: new word created %s", to_add); parser.add_word (to_add); - - // // Mark for after_delete handler where deletion occurred - // current_view.buffer.add_mark (start_del_mark, del_start_iter); - } - - private void after_delete_range () { - // Gtk.TextIter? iter = null; - // if (start_del_mark.get_deleted ()) { - // critical ("No DeleteMark after deletion"); - // return; - // } - - // // The deleted text has already been parsed and removed from prefix tree - // // Need to check whether a new word has been created by deletion - // current_view.buffer.get_iter_at_mark (out iter, start_del_mark); - // if (iter == null) { - // critical ("Unable to get iter from deletion mark"); - // return; - // } - - // var delete_pos = iter.get_offset (); - // string word_before, word_after; - // parser. get_words_before_and_after_pos ( - // current_view.buffer.text, - // delete_pos, - // out word_before, - // out word_after - // ); - - // // A new word could have been created - // var to_add = word_before + word_after; - // parser.parse_text_and_add (to_add); - // current_view.buffer.delete_mark (start_del_mark); - } - - private int current_insertion_line = -1; - private string original_text = ""; - private void record_original_line_at (Gtk.TextIter iter) requires (current_insertion_line < 0) { - current_insertion_line = iter.get_line (); - var start_iter = iter; - var end_iter = iter; - while (!start_iter.starts_line ()) { - start_iter.backward_char (); - } - - end_iter.forward_to_line_end (); - original_text = start_iter.get_text (end_iter); - } - - private string retrieve_original_text () { - var return_s = original_text; - original_text = ""; - current_insertion_line = -1; - return return_s; } private string provider_name_from_document (Scratch.Services.Document doc) { @@ -256,14 +185,13 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_view.buffer.insert_text.disconnect (on_insert_text); current_view.buffer.delete_range.disconnect (on_delete_range); - current_view.buffer.delete_range.disconnect (after_delete_range); // Disconnect show completion?? current_view.completion.get_providers ().foreach ((p) => { try { /* Only remove provider added by this plug in */ if (p.get_name () == provider_name_from_document (current_document)) { - debug ("removing provider %s", p.get_name ()); + debug ("removing provider %s", p != null ? p.get_name () : "null"); current_view.completion.remove_provider (p); } } catch (Error e) { diff --git a/plugins/word-completion/prefix-tree-node.vala b/plugins/word-completion/prefix-tree-node.vala index bd7a165331..0d1497ee36 100644 --- a/plugins/word-completion/prefix-tree-node.vala +++ b/plugins/word-completion/prefix-tree-node.vala @@ -32,11 +32,11 @@ public class Scratch.Plugins.PrefixNode : Object { public uint occurrences { get; set construct; default = 0; } public PrefixNode? parent { get; construct; default = null; } - // public bool is_word_end { - // get { - // return type == WORD_END; - // } - // } + public bool is_word_end { + get { + return type == WORD_END; + } + } // public bool is_root { // get { @@ -109,16 +109,20 @@ public class Scratch.Plugins.PrefixNode : Object { } } - private void decrement () requires (type == WORD_END && occurrences > 0) { - lock (occurrences) { - occurrences--; + public void decrement () requires (type == WORD_END) { + if (occurrences == 0) { + warning ("decrementing non-occurring node"); + return; } - if (occurrences == 0) { - // critical ("removing child no longer used"); - parent.remove_child (this); + lock (occurrences) { + occurrences--; } } + + public bool occurs () requires (type == WORD_END) { + return occurrences > 0; + } private void append_child (owned PrefixNode child) requires (type != WORD_END) { lock (children) { @@ -126,7 +130,7 @@ public class Scratch.Plugins.PrefixNode : Object { } } - private void remove_child (PrefixNode child) requires (type != WORD_END) { + public void remove_child (PrefixNode child) requires (type != WORD_END) { lock (children) { children.remove (child); if (children.is_empty && type != ROOT) { @@ -135,25 +139,25 @@ public class Scratch.Plugins.PrefixNode : Object { } } - private bool remove_or_decrement_word_end () requires (this.has_children) { - foreach (var child in children) { - if (child.type == WORD_END) { -// warning ("found word end - occurrences %u - decrementing", child.occurrences); - child.decrement (); +// private bool remove_or_decrement_word_end () requires (this.has_children) { +// foreach (var child in children) { +// if (child.type == WORD_END) { +// // warning ("found word end - occurrences %u - decrementing", child.occurrences); +// child.decrement (); - return true; - } - } +// return true; +// } +// } - critical ("No word end node found when removing"); +// critical ("No word end node found when removing"); - return false; - } +// return false; +// } private void append_or_increment_word_end () requires (type != WORD_END && type != ROOT) { foreach (var child in children) { if (child.type == WORD_END) { -// warning ("incrementing child occurrence"); + debug ("incrementing child occurrence"); child.increment (); return; } @@ -183,7 +187,7 @@ public class Scratch.Plugins.PrefixNode : Object { } } - public void insert_word (string text) { + public void insert_word (string text) requires (type == ROOT) { int index = 0; insert_word_internal (text, ref index); } @@ -218,12 +222,12 @@ public class Scratch.Plugins.PrefixNode : Object { } } - public bool remove_word (string text) { - var node = find_last_node_for (text); - var res = node.remove_or_decrement_word_end (); - // warning ("remove %s result %s", text, res.to_string ()); - return res; - } + // public bool remove_word (string text) { + // var node = find_last_node_for (text); + // var res = node.remove_or_decrement_word_end (); + // // warning ("remove %s result %s", text, res.to_string ()); + // return res; + // } public PrefixNode? has_char_child (unichar c) requires (type != WORD_END) { foreach (var child in children) { diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index abfb94c6c2..d5c18856c5 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -22,11 +22,19 @@ public class Scratch.Plugins.PrefixTree : Object { private PrefixNode? root = null; private GLib.StringBuilder sb; + private uint reaper_timeout_id = 0; + private bool delay_reaping = false; + private const uint REAPING_THROTTLE_MS = 500; + private bool reaping_cancelled = false; + public bool initial_parse_complete = false; + public bool completed { get; set; default = false; } + public Gee.LinkedList words_to_remove { get; construct; } construct { clear (); sb = new GLib.StringBuilder (""); + words_to_remove = new Gee.LinkedList (); } public void clear () { @@ -34,16 +42,11 @@ initial_parse_complete = false; } - public void insert (string word) requires (word.length > 0) { - warning ("add '%s' to root", word); + public void add_word (string word) requires (word.length > 0) { + debug ("add '%s' to root", word); root.insert_word (word); } - public void remove (string word) requires (word.length > 0) { - warning ("remove '%s' from root", word); - var res = root.remove_word (word); - } - public bool has_prefix (string prefix) { return root.find_last_node_for (prefix) != null ? true : false; } @@ -56,6 +59,80 @@ node.get_all_completions (ref list, ref sb); } - return list; + return (owned)list; + } + + + + // 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 ()); + // } + // } else { + // debug ("Not valid to add %s", word_to_add); + // } + // } + + public void remove_word (string word_to_remove) { + debug ("remove word %s", word_to_remove); + var end_node = root.find_last_node_for (word_to_remove); + if (end_node != null && end_node.is_word_end) { + end_node.decrement (); + if (!end_node.occurs ()) { + debug ("schedule remove %s", word_to_remove); + words_to_remove.add (end_node); + schedule_reaping (); + } else { + debug ("not removing %s", word_to_remove); + } + } else { + debug ("%s not found in tree", word_to_remove); + } + } + + // 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 ((end_node) => { + if (reaping_cancelled) { + debug ("reaping was cancelled"); + return false; + } + + // var wo = current_tree.@get (word); + if (!end_node.occurs ()) { + end_node.parent.remove_child (end_node); + } + + return true; + }); + + // Cannot remove inside @foreach loop so do it now + words_to_remove.clear (); + return Source.REMOVE; + } + }); + } } } From f0d0dd6b70be0167497b750c9cb8e390e3dd0c77 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 3 Dec 2024 16:44:10 +0000 Subject: [PATCH 34/46] Cleanup, comment, fix find end node --- plugins/word-completion/engine.vala | 2 +- plugins/word-completion/plugin.vala | 2 +- plugins/word-completion/prefix-tree-node.vala | 152 ++++++++---------- plugins/word-completion/prefix-tree.vala | 56 ++----- 4 files changed, 85 insertions(+), 127 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index bafcdc9c3a..e23a145b9b 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -192,7 +192,7 @@ public class Euclide.Completion.Parser : GLib.Object { // Assume always preceded by match and current_completions up to date return (owned)current_completions; } - + public void add_word (string word_to_add) requires (current_tree != null) { if (is_valid_word (word_to_add)) { lock (current_tree) { diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 0f1e570300..7ed54dd4bf 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -64,7 +64,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } public void on_new_source_view (Scratch.Services.Document doc) { - debug ("new source_view %s", doc !=null ? doc.title : "null"); + debug ("new source_view %s", doc != null ? doc.title : "null"); if (current_view != null) { if (current_view == doc.source_view) { return; diff --git a/plugins/word-completion/prefix-tree-node.vala b/plugins/word-completion/prefix-tree-node.vala index 0d1497ee36..ec67701b38 100644 --- a/plugins/word-completion/prefix-tree-node.vala +++ b/plugins/word-completion/prefix-tree-node.vala @@ -20,6 +20,7 @@ */ public class Scratch.Plugins.PrefixNode : Object { + private const unichar WORD_END_CHAR = '\0'; private enum NodeType { ROOT, CHAR, @@ -32,25 +33,13 @@ public class Scratch.Plugins.PrefixNode : Object { public uint occurrences { get; set construct; default = 0; } public PrefixNode? parent { get; construct; default = null; } - public bool is_word_end { + protected bool is_word_end { get { return type == WORD_END; } } - // public bool is_root { - // get { - // return type == ROOT; - // } - // } - - public uint length { - get { - return char_s.length; - } - } - - public string char_s { + protected string char_s { owned get { if (uc != null) { return uc.to_string (); @@ -60,13 +49,13 @@ public class Scratch.Plugins.PrefixNode : Object { } } - public bool has_children { + protected bool has_children { get { return type != WORD_END && children.size > 0; } } - public PrefixNode.from_unichar (unichar c, PrefixNode? _parent) requires (c != '\0') { + public PrefixNode.from_unichar (unichar c, PrefixNode? _parent) requires (c != WORD_END_CHAR) { Object ( parent: _parent, occurrences: 1 @@ -91,7 +80,7 @@ public class Scratch.Plugins.PrefixNode : Object { occurrences: 1 ); - uc = '\0'; + uc = WORD_END_CHAR; type = WORD_END; } @@ -109,89 +98,63 @@ public class Scratch.Plugins.PrefixNode : Object { } } - public void decrement () requires (type == WORD_END) { + // Returns true if word still occurs + public bool decrement () requires (type == WORD_END) { if (occurrences == 0) { warning ("decrementing non-occurring node"); - return; + return false; } lock (occurrences) { occurrences--; } + + return occurrences > 0; } - + public bool occurs () requires (type == WORD_END) { return occurrences > 0; } - private void append_child (owned PrefixNode child) requires (type != WORD_END) { - lock (children) { - children.add (child); - } + // Only called to add a complete word to the tree + public void insert_word (string text) requires (type == ROOT) { + debug ("rootnode: insert word %s", text); + int index = 0; + insert_word_internal (text, ref index); } public void remove_child (PrefixNode child) requires (type != WORD_END) { lock (children) { children.remove (child); + debug ("removed child '%s'", child.char_s); if (children.is_empty && type != ROOT) { + debug ("remove this from parent"); parent.remove_child (this); } } } -// private bool remove_or_decrement_word_end () requires (this.has_children) { -// foreach (var child in children) { -// if (child.type == WORD_END) { -// // warning ("found word end - occurrences %u - decrementing", child.occurrences); -// child.decrement (); - -// return true; -// } -// } - -// critical ("No word end node found when removing"); - -// return false; -// } - - private void append_or_increment_word_end () requires (type != WORD_END && type != ROOT) { - foreach (var child in children) { - if (child.type == WORD_END) { - debug ("incrementing child occurrence"); - child.increment (); - return; - } - } - - var new_child = new PrefixNode.word_end (this); - append_child (new_child); - } - - private PrefixNode? find_or_append_char_child ( - unichar c, - bool append_if_not_found = false - ) requires (type != WORD_END) { - - foreach (var child in children) { - if (child.has_char (c)) { - return child; - } + // We return any end_node for @text even if occurences == 0 + // because we may be re-adding it before it is reaped + public PrefixNode? find_end_node_for (string text) { + debug ("find_end_node_for %s", text); + var last_node = find_last_node_for (text); + if (last_node != null) { + var end_node = last_node.find_or_append_char_child (WORD_END_CHAR, false); + return end_node; } - if (append_if_not_found) { - var new_child = new PrefixNode.from_unichar (c, this); - append_child (new_child); - return new_child; - } else { - return null; - } + return null; } - public void insert_word (string text) requires (type == ROOT) { + // Returns node corresponding to last char in @text (or null if not in tree) + public PrefixNode? find_last_node_for (string text) { int index = 0; - insert_word_internal (text, ref index); + return find_last_node_for_internal (text, ref index); } + //PROTECTED METHODS + protected void insert_word_internal (string text, ref int index) { unichar? uc = null; if (text.get_next_char (ref index, out uc)) { @@ -202,44 +165,63 @@ public class Scratch.Plugins.PrefixNode : Object { } } - public PrefixNode? find_last_node_for (string text) { - int index = 0; - var res = find_last_node_for_internal (text, ref index); - return res; - } - protected PrefixNode? find_last_node_for_internal (string text, ref int index) requires (type != WORD_END) { unichar? uc = null; if (text.get_next_char (ref index, out uc)) { + debug ("find char_child '%s'", uc.to_string ()); var child = find_or_append_char_child (uc, false); if (child == null ) { + debug ("child not found"); return null; } else { return child.find_last_node_for_internal (text, ref index); } } else { + debug ("end of text - current node type %s", this.type.to_string ()); return this; } } + //PRIVATE METHODS - // public bool remove_word (string text) { - // var node = find_last_node_for (text); - // var res = node.remove_or_decrement_word_end (); - // // warning ("remove %s result %s", text, res.to_string ()); - // return res; - // } + private void append_or_increment_word_end () requires (type != WORD_END && type != ROOT) { + foreach (var child in children) { + if (child.type == WORD_END) { + debug ("incrementing end node occurrence"); + child.increment (); + return; + } + } + + var new_child = new PrefixNode.word_end (this); + debug ("append new word end"); + append_child (new_child); + } + + private PrefixNode? find_or_append_char_child (unichar c, bool append_if_not_found = false) + requires (type != WORD_END && c != WORD_END_CHAR) { - public PrefixNode? has_char_child (unichar c) requires (type != WORD_END) { foreach (var child in children) { if (child.has_char (c)) { return child; } } - return null; + if (append_if_not_found) { + var new_child = new PrefixNode.from_unichar (c, this); + append_child (new_child); + return new_child; + } else { + return null; + } + } + + // Children are only appended if they do not already exist + private void append_child (owned PrefixNode child) requires (type != WORD_END) { + lock (children) { + children.add (child); + } } - // First could with node at the last char of the prefix public void get_all_completions (ref List completions, ref StringBuilder sb) { if (type == WORD_END) { return; diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index d5c18856c5..83acca551c 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -27,8 +27,8 @@ private const uint REAPING_THROTTLE_MS = 500; private bool reaping_cancelled = false; - public bool initial_parse_complete = false; public bool completed { get; set; default = false; } + // We just store the end_nodes corresponding to words to be removed public Gee.LinkedList words_to_remove { get; construct; } construct { @@ -39,7 +39,7 @@ public void clear () { root = new PrefixNode.root (); - initial_parse_complete = false; + completed = false; } public void add_word (string word) requires (word.length > 0) { @@ -47,59 +47,33 @@ root.insert_word (word); } - public bool has_prefix (string prefix) { - return root.find_last_node_for (prefix) != null ? true : false; - } - public List get_all_completions (string prefix) { var list = new List (); - var node = root.find_last_node_for (prefix); - if (node != null) { + var last_node = root.find_last_node_for (prefix); + if (last_node != null) { sb.erase (); - node.get_all_completions (ref list, ref sb); + last_node.get_all_completions (ref list, ref sb); } return (owned)list; } - - - // 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 ()); - // } - // } else { - // debug ("Not valid to add %s", word_to_add); - // } - // } - public void remove_word (string word_to_remove) { - debug ("remove word %s", word_to_remove); - var end_node = root.find_last_node_for (word_to_remove); - if (end_node != null && end_node.is_word_end) { - end_node.decrement (); - if (!end_node.occurs ()) { + debug ("prefix tree: remove word %s", word_to_remove); + var end_node = root.find_end_node_for (word_to_remove); + if (end_node != null) { + if (!end_node.decrement ()) { debug ("schedule remove %s", word_to_remove); words_to_remove.add (end_node); schedule_reaping (); } else { - debug ("not removing %s", word_to_remove); + debug ("not removing %s - still occurs", word_to_remove); } } else { - debug ("%s not found in tree", word_to_remove); + debug ("%s end node not found in tree", word_to_remove); } } - // 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) { @@ -111,6 +85,7 @@ delay_reaping = false; return Source.CONTINUE; } else { + debug ("reaping timeout"); reaper_timeout_id = 0; // var words_to_remove = get_words_to_remove (); debug ("reaping"); @@ -120,15 +95,16 @@ return false; } - // var wo = current_tree.@get (word); if (!end_node.occurs ()) { - end_node.parent.remove_child (end_node); + end_node.parent.remove_child (end_node); + } else { + debug ("still occurs when reaping"); } return true; }); - // Cannot remove inside @foreach loop so do it now + // Cannot clear list while inside @foreach loop so do it now words_to_remove.clear (); return Source.REMOVE; } From f89613d42f3c44a452aaba096c504483b9b832e9 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 4 Dec 2024 12:48:54 +0000 Subject: [PATCH 35/46] Lose unnecessary func --- plugins/word-completion/engine.vala | 10 ---------- plugins/word-completion/plugin.vala | 4 ++-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index e23a145b9b..74b8fd8613 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -107,16 +107,6 @@ public class Euclide.Completion.Parser : GLib.Object { return; } - 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 ""; diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 7ed54dd4bf..8ab68089e1 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -140,8 +140,8 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // 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 word_before = parser.get_word_immediately_before (text, insert_pos); + var word_after = parser.get_word_immediately_after (text, insert_pos); var text_to_add = (word_before + new_text + word_after).strip (); var text_to_remove = (word_before + word_after).strip (); From 699dbf6bfeee78663f0c32e3987dd63e726b7ce6 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 5 Dec 2024 11:58:19 +0000 Subject: [PATCH 36/46] Fix regression - allow find word end char --- plugins/word-completion/prefix-tree-node.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/word-completion/prefix-tree-node.vala b/plugins/word-completion/prefix-tree-node.vala index ec67701b38..32f9c76898 100644 --- a/plugins/word-completion/prefix-tree-node.vala +++ b/plugins/word-completion/prefix-tree-node.vala @@ -198,7 +198,7 @@ public class Scratch.Plugins.PrefixNode : Object { } private PrefixNode? find_or_append_char_child (unichar c, bool append_if_not_found = false) - requires (type != WORD_END && c != WORD_END_CHAR) { + requires (type != WORD_END) { foreach (var child in children) { if (child.has_char (c)) { From 4296f4398e48f3614b7d6d334a69a99e0e64934a Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 5 Dec 2024 13:53:40 +0000 Subject: [PATCH 37/46] Show completions after delete one letter --- plugins/word-completion/plugin.vala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 8ab68089e1..89104719ce 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -172,6 +172,14 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { var to_add = word_before + word_after; debug ("delete range: new word created %s", to_add); parser.add_word (to_add); + if (del_text.length == 1) { + // Wait until after buffer has been amended then trigger completion + Timeout.add (current_provider.interactive_delay, () => { + warning ("showing completion"); + current_view.show_completion (); + return Source.REMOVE; + }); + } } private string provider_name_from_document (Scratch.Services.Document doc) { From 2341656bdff024223e05a09f18a0b7b55c9747e0 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 5 Dec 2024 13:54:34 +0000 Subject: [PATCH 38/46] Fix regression - do not show deleted but unreaped words --- plugins/word-completion/prefix-tree-node.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/word-completion/prefix-tree-node.vala b/plugins/word-completion/prefix-tree-node.vala index 32f9c76898..34c4b7d6c6 100644 --- a/plugins/word-completion/prefix-tree-node.vala +++ b/plugins/word-completion/prefix-tree-node.vala @@ -229,7 +229,7 @@ public class Scratch.Plugins.PrefixNode : Object { var initial_sb_str = sb.str; foreach (var child in children) { - if (child.type == WORD_END) { + if (child.type == WORD_END && child.occurs ()) { completions.prepend (sb.str); } else { sb.append (child.char_s); From f704619c10037494d9fba20eb9226bd952da980b Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 5 Dec 2024 13:55:07 +0000 Subject: [PATCH 39/46] Code style, cleanup --- .../word-completion/completion-provider.vala | 35 +++++++------------ plugins/word-completion/plugin.vala | 35 +++++++++---------- plugins/word-completion/prefix-tree.vala | 6 ++-- 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index 8f9625ecd0..831222a570 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -19,9 +19,11 @@ * */ -public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, Object { - public string name; - public int priority; +public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, GLib.Object { + public string name { get; construct; } + public int priority { get; construct; } + public int interactive_delay { get; construct; } + public Gtk.SourceCompletionActivation activation { get; construct; } public const string COMPLETION_END_MARK_NAME = "ScratchWordCompletionEnd"; public const string COMPLETION_START_MARK_NAME = "ScratchWordCompletionStart"; @@ -43,30 +45,25 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, public CompletionProvider ( Euclide.Completion.Parser _parser, - Gtk.TextView _view + Scratch.Services.Document _doc ) { Object ( parser: _parser, - view: _view + view: _doc.source_view, + name: _("%s - Word Completion").printf (_doc.get_basename ()) ); } construct { + interactive_delay = (int) Completion.INTERACTIVE_DELAY; + activation = INTERACTIVE | USER_REQUESTED; Gtk.TextIter iter; 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 override string get_name () { - return this.name; - } - - public override int get_priority () { - return this.priority; - } - public override bool match (Gtk.SourceCompletionContext context) { int end_pos = buffer.cursor_position; int start_pos = end_pos; @@ -114,15 +111,6 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, return true; } - public override Gtk.SourceCompletionActivation get_activation () { - return Gtk.SourceCompletionActivation.INTERACTIVE | - Gtk.SourceCompletionActivation.USER_REQUESTED; - } - - public override int get_interactive_delay () { - return 500; - } - public override bool get_start_iter (Gtk.SourceCompletionContext context, Gtk.SourceCompletionProposal proposal, out Gtk.TextIter iter) { @@ -132,6 +120,9 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, return true; } + public override string get_name () { + return name; + } private bool get_proposals (out GLib.List? props, bool no_minimum) { string to_find = ""; diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 89104719ce..2618481038 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -21,6 +21,8 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { public const int MAX_TOKENS = 1000000; + public const uint INTERACTIVE_DELAY = 500; + private const uint [] ACTIVATE_KEYS = { Gdk.Key.Return, Gdk.Key.KP_Enter, @@ -36,8 +38,10 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { 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 Euclide.Completion.Parser parser; + private Gtk.SourceView? current_view; + private Gtk.SourceCompletion? current_completion; + private Scratch.Plugins.CompletionProvider current_provider; private Scratch.Services.Document current_document {get; private set;} private MainWindow main_window; private Scratch.Services.Interface plugins; @@ -76,14 +80,15 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_document = doc; current_view = doc.source_view; + current_completion = current_view.completion; current_view.buffer.insert_text.connect (on_insert_text); current_view.buffer.delete_range.connect (on_delete_range); - current_view.completion.show.connect (() => { + current_completion.show.connect (() => { completion_in_progress = true; }); - current_view.completion.hide.connect (() => { + current_completion.hide.connect (() => { completion_in_progress = false; }); @@ -91,14 +96,14 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { text_view_list.append (current_view); } - var comp_provider = new Scratch.Plugins.CompletionProvider (parser, current_view); - comp_provider.priority = 1; - comp_provider.name = provider_name_from_document (doc); + current_provider = new Scratch.Plugins.CompletionProvider (parser, doc); try { - current_view.completion.add_provider (comp_provider); - current_view.completion.show_headers = true; - current_view.completion.show_icons = true; + current_completion.add_provider (current_provider); + current_completion.show_headers = true; + current_completion.show_icons = true; + current_completion.accelerators = 9; + current_completion.select_on_show = true; } catch (Error e) { critical ( "Could not add completion provider to %s. %s\n", @@ -180,10 +185,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { return Source.REMOVE; }); } - } - private string provider_name_from_document (Scratch.Services.Document doc) { - return _("%s - Word Completion").printf (doc.get_basename ()); } private void cleanup () { @@ -195,13 +197,10 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_view.buffer.delete_range.disconnect (on_delete_range); // Disconnect show completion?? - current_view.completion.get_providers ().foreach ((p) => { + current_completion.get_providers ().foreach ((p) => { try { /* Only remove provider added by this plug in */ - if (p.get_name () == provider_name_from_document (current_document)) { - debug ("removing provider %s", p != null ? p.get_name () : "null"); - current_view.completion.remove_provider (p); - } + current_completion.remove_provider (current_provider); } catch (Error e) { warning (e.message); } diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala index 83acca551c..4cd34852e6 100644 --- a/plugins/word-completion/prefix-tree.vala +++ b/plugins/word-completion/prefix-tree.vala @@ -24,7 +24,9 @@ private GLib.StringBuilder sb; private uint reaper_timeout_id = 0; private bool delay_reaping = false; - private const uint REAPING_THROTTLE_MS = 500; + // Reaping need not occur before completions shown since non-occurring words are + // not shown anyway. + private const uint REAPING_THROTTLE_MS = Completion.INTERACTIVE_DELAY * 2; private bool reaping_cancelled = false; public bool completed { get; set; default = false; } @@ -85,9 +87,7 @@ delay_reaping = false; return Source.CONTINUE; } else { - debug ("reaping timeout"); reaper_timeout_id = 0; - // var words_to_remove = get_words_to_remove (); debug ("reaping"); words_to_remove.foreach ((end_node) => { if (reaping_cancelled) { From d776da6b922d1b7c6a1e6ec2dfaf6b32094f08fc Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 18 Dec 2024 19:07:05 +0000 Subject: [PATCH 40/46] Limit total number of completions --- plugins/word-completion/completion-provider.vala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index 831222a570..c29f9e45b5 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -20,6 +20,7 @@ */ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, GLib.Object { + private const int MAX_COMPLETIONS = 10; public string name { get; construct; } public int priority { get; construct; } public int interactive_delay { get; construct; } @@ -144,8 +145,9 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, /* 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_PREFIX_LENGTH) { /* Get proposals, if any */ - var completions = parser.get_completions_for_prefix (to_find); + var completions = parser.get_current_completions (to_find); if (completions.length () > 0) { + var index = 0; foreach (var completion in completions) { if (completion.length > 0) { var item = new Gtk.SourceCompletionItem (); @@ -153,6 +155,9 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, item.label = word; item.text = completion; props.append (item); + if (++index > MAX_COMPLETIONS) { + break; + } } } From 232dfcd7105dc1ec8ddef59814dda44d4df0a724 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 18 Dec 2024 19:09:15 +0000 Subject: [PATCH 41/46] Do not connect signals until initial parsing complete --- plugins/word-completion/plugin.vala | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 2618481038..77e9f5d49f 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -22,6 +22,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { public const int MAX_TOKENS = 1000000; public const uint INTERACTIVE_DELAY = 500; + public const int INITIAL_PARSE_DELAY_MSEC = 1000; private const uint [] ACTIVATE_KEYS = { Gdk.Key.Return, @@ -81,16 +82,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_document = doc; current_view = doc.source_view; current_completion = current_view.completion; - current_view.buffer.insert_text.connect (on_insert_text); - current_view.buffer.delete_range.connect (on_delete_range); - - current_completion.show.connect (() => { - completion_in_progress = true; - }); - - current_completion.hide.connect (() => { - completion_in_progress = false; - }); if (text_view_list.find (current_view) == null) { text_view_list.append (current_view); @@ -117,12 +108,23 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { /* Wait a bit to allow text to load then run parser*/ if (!parser.select_current_tree (current_view)) { // Returns false if prefix tree new or parsing not completed // Start initial parsing after timeout to ensure text loaded - timeout_id = Timeout.add (1000, () => { + timeout_id = Timeout.add (INITIAL_PARSE_DELAY_MSEC, () => { timeout_id = 0; try { new Thread.try ("word-completion-thread", () => { if (current_view != null) { parser.initial_parse_buffer_text (current_view.buffer.text); + // + current_view.buffer.insert_text.connect (on_insert_text); + current_view.buffer.delete_range.connect (on_delete_range); + + current_completion.show.connect (() => { + completion_in_progress = true; + }); + + current_completion.hide.connect (() => { + completion_in_progress = false; + }); } return null; From 51794aa474c3f5a4320003351bee5037e0717edd Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 18 Dec 2024 19:14:13 +0000 Subject: [PATCH 42/46] Rework getting immediate prev and next words --- .../word-completion/completion-provider.vala | 15 +++--- plugins/word-completion/engine.vala | 54 ++++++++++++------- plugins/word-completion/plugin.vala | 39 +++++--------- 3 files changed, 54 insertions(+), 54 deletions(-) diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index c29f9e45b5..fad3ffe6bd 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -66,12 +66,12 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, } public override bool match (Gtk.SourceCompletionContext context) { - int end_pos = buffer.cursor_position; - int start_pos = end_pos; + var iter = context.iter; + var start_iter = iter; + var end_iter = iter; bool found = false; - var text = buffer.text; - var preceding_word = parser.get_word_immediately_before (text, start_pos); + var preceding_word = parser.get_word_immediately_before (iter); if (preceding_word != "") { found = parser.match (preceding_word); current_text_to_find = found ? preceding_word : ""; @@ -95,11 +95,9 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, mark = buffer.get_mark (COMPLETION_END_MARK_NAME); 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 inserting in middle of word then completion overwrites end of word + var following_word = parser.get_word_immediately_after (end_iter); if (following_word != "") { buffer.get_iter_at_offset (out end_iter, end_pos + following_word.length); } @@ -132,7 +130,6 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, Gtk.TextIter start, end; buffer.get_selection_bounds (out start, out end); - to_find = temp_buffer.get_text (start, end, true); if (to_find.length == 0) { diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 74b8fd8613..705054d9f8 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -107,12 +107,10 @@ public class Euclide.Completion.Parser : GLib.Object { return; } - public string get_word_immediately_before (string text, int end_pos) { - if (end_pos < 1) { - return ""; - } - - int pos = end_pos; + public string get_word_immediately_before (Gtk.TextIter iter) { + int end_pos; + var text = get_sentence_at_iter (iter, out end_pos); + var pos = end_pos; unichar uc; text.get_prev_char (ref pos, out uc); if (is_delimiter (uc)) { @@ -128,32 +126,40 @@ public class Euclide.Completion.Parser : GLib.Object { 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 (); + return previous_word; } - 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; + public string get_word_immediately_after (Gtk.TextIter iter) { + int start_pos; + var text = get_sentence_at_iter (iter, out start_pos); + var pos = start_pos; unichar uc; text.get_next_char (ref pos, out uc); if (is_delimiter (uc)) { return ""; } + // Find end of search range pos = (start_pos + MAXIMUM_WORD_LENGTH + 1).clamp (start_pos, text.length); if (start_pos >= pos) { critical ("start pos after pos"); return ""; } + // Find first word in range 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 (); + return next_word; + } + + private string get_sentence_at_iter (Gtk.TextIter iter, out int iter_sentence_offset) { + var start_iter = iter; + var end_iter = iter; + start_iter.backward_sentence_start (); + end_iter.forward_sentence_end (); + var text = start_iter.get_text (end_iter); + iter_sentence_offset = iter.get_offset () - start_iter.get_offset (); + return text; } public void clear () requires (current_tree != null) { @@ -173,18 +179,29 @@ public class Euclide.Completion.Parser : GLib.Object { } private List current_completions; + private string current_prefix; public bool match (string prefix) requires (current_tree != null) { - current_completions = current_tree.get_all_completions (prefix); + lock (current_tree) { + current_completions = current_tree.get_all_completions (prefix); + current_prefix = prefix; + } + return current_completions != null && current_completions.first ().data != null; } - public List get_completions_for_prefix (string prefix) requires (current_tree != null) { + public List get_current_completions (string prefix) requires (current_tree != null) { // Assume always preceded by match and current_completions up to date + if (current_prefix != prefix) { + critical ("current prefix does not match"); + match (prefix); + } + return (owned)current_completions; } public void add_word (string word_to_add) requires (current_tree != null) { if (is_valid_word (word_to_add)) { + warning ("ADD WORD %s", word_to_add); lock (current_tree) { current_tree.add_word (word_to_add); } @@ -192,6 +209,7 @@ public class Euclide.Completion.Parser : GLib.Object { } public void remove_word (string word_to_remove) requires (current_tree != null) { + warning ("REMOVE WORD %s", word_to_remove); lock (current_tree) { current_tree.remove_word (word_to_remove); } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 77e9f5d49f..225b00c915 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -140,54 +140,39 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // 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 ()) { - // Ignore 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 (); - var word_before = parser.get_word_immediately_before (text, insert_pos); - var word_after = parser.get_word_immediately_after (text, insert_pos); - var text_to_add = (word_before + new_text + word_after).strip (); - var text_to_remove = (word_before + word_after).strip (); - + var word_before = parser.get_word_immediately_before (iter); + var word_after = parser.get_word_immediately_after (iter); + var text_to_add = (word_before + new_text + word_after); + var text_to_remove = (word_before + word_after); // Only update if words have changed - if (text_to_add != text_to_remove) { - debug ("adding %s, removing %s", text_to_add, text_to_remove); - // Text to add may contain delimiters so parse + if (text_to_add != text_to_remove && + (new_text + word_after).strip () != "" && + (new_text + word_before).strip () != "") { + parser.parse_text_and_add (text_to_add); - debug ("after insert remove %s", text_to_remove); - // We know text to remove does not contain delimiters parser.remove_word (text_to_remove); } } 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); - + var word_before = parser.get_word_immediately_before (del_end_iter); + var word_after = parser.get_word_immediately_after (del_start_iter); var to_remove = word_before + del_text + word_after; - debug ("delete range: remove %s", to_remove); parser.parse_text_and_remove (to_remove); // A new word could have been created var to_add = word_before + word_after; - debug ("delete range: new word created %s", to_add); parser.add_word (to_add); if (del_text.length == 1) { // Wait until after buffer has been amended then trigger completion - Timeout.add (current_provider.interactive_delay, () => { - warning ("showing completion"); + Timeout.add (current_provider.interactive_delay * 2, () => { + debug ("showing completion"); current_view.show_completion (); return Source.REMOVE; }); } - } private void cleanup () { From 0cc667979b395d62deb3ecf42ec47cfaec5b8604 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 18 Dec 2024 19:37:15 +0000 Subject: [PATCH 43/46] Fix delete range --- plugins/word-completion/engine.vala | 10 ++++++---- plugins/word-completion/plugin.vala | 17 ++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 705054d9f8..60129ee0b7 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -201,7 +201,7 @@ public class Euclide.Completion.Parser : GLib.Object { public void add_word (string word_to_add) requires (current_tree != null) { if (is_valid_word (word_to_add)) { - warning ("ADD WORD %s", word_to_add); + // warning ("ADD WORD %s", word_to_add); lock (current_tree) { current_tree.add_word (word_to_add); } @@ -209,9 +209,11 @@ public class Euclide.Completion.Parser : GLib.Object { } public void remove_word (string word_to_remove) requires (current_tree != null) { - warning ("REMOVE WORD %s", word_to_remove); - lock (current_tree) { - current_tree.remove_word (word_to_remove); + if (is_valid_word (word_to_remove)) { + // warning ("REMOVE WORD %s", word_to_remove); + lock (current_tree) { + current_tree.remove_word (word_to_remove); + } } } diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 225b00c915..52c2ecf101 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -146,10 +146,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { var text_to_add = (word_before + new_text + word_after); var text_to_remove = (word_before + word_after); // Only update if words have changed - if (text_to_add != text_to_remove && - (new_text + word_after).strip () != "" && - (new_text + word_before).strip () != "") { - + if (text_to_add != text_to_remove) { parser.parse_text_and_add (text_to_add); parser.remove_word (text_to_remove); } @@ -157,14 +154,16 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { 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 word_before = parser.get_word_immediately_before (del_end_iter); - var word_after = parser.get_word_immediately_after (del_start_iter); + var word_before = parser.get_word_immediately_before (del_start_iter); + var word_after = parser.get_word_immediately_after (del_end_iter); var to_remove = word_before + del_text + word_after; - parser.parse_text_and_remove (to_remove); - - // A new word could have been created var to_add = word_before + word_after; + + // More than one word could be deleted so parse. + parser.parse_text_and_remove (to_remove); + // Only one at most new words parser.add_word (to_add); + if (del_text.length == 1) { // Wait until after buffer has been amended then trigger completion Timeout.add (current_provider.interactive_delay * 2, () => { From 4c2d536a466f6d4be7a6cccd609e18b19f993abc Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 20 Dec 2024 17:08:22 +0000 Subject: [PATCH 44/46] Connect and disconnect signals correctly --- plugins/word-completion/plugin.vala | 62 ++++++++++++++++------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 52c2ecf101..9da563b5c0 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -105,47 +105,43 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { return; } - /* Wait a bit to allow text to load then run parser*/ - if (!parser.select_current_tree (current_view)) { // Returns false if prefix tree new or parsing not completed - // Start initial parsing after timeout to ensure text loaded + // Check whether there is already a parsed tree + if (!parser.select_current_tree (current_view)) { + // If not, start initial parsing after timeout to ensure text loaded + var view_to_parse = current_view; timeout_id = Timeout.add (INITIAL_PARSE_DELAY_MSEC, () => { timeout_id = 0; - try { - new Thread.try ("word-completion-thread", () => { - if (current_view != null) { - parser.initial_parse_buffer_text (current_view.buffer.text); - // - current_view.buffer.insert_text.connect (on_insert_text); - current_view.buffer.delete_range.connect (on_delete_range); - - current_completion.show.connect (() => { - completion_in_progress = true; - }); - - current_completion.hide.connect (() => { - completion_in_progress = false; - }); - } - - return null; - }); - } catch (Error e) { - warning (e.message); + // Check view has not been switched + if (view_to_parse == current_view) { + try { + new Thread.try ("word-completion-thread", () => { + parser.initial_parse_buffer_text (view_to_parse.buffer.text); + return null; + }); + } catch (Error e) { + warning (e.message); + } } return Source.REMOVE; }); + } else { + connect_signals (); } } // 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 ()) { + return; + } // Determine whether insertion point ends and/or starts a word var word_before = parser.get_word_immediately_before (iter); var word_after = parser.get_word_immediately_after (iter); var text_to_add = (word_before + new_text + word_after); var text_to_remove = (word_before + word_after); // Only update if words have changed + if (text_to_add != text_to_remove) { parser.parse_text_and_add (text_to_add); parser.remove_word (text_to_remove); @@ -153,6 +149,10 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } private void on_delete_range (Gtk.TextIter del_start_iter, Gtk.TextIter del_end_iter) { + if (!parser.get_initial_parsing_completed ()) { + return; + } + var del_text = del_start_iter.get_text (del_end_iter); var word_before = parser.get_word_immediately_before (del_start_iter); var word_after = parser.get_word_immediately_after (del_end_iter); @@ -179,9 +179,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { GLib.Source.remove (timeout_id); } - current_view.buffer.insert_text.disconnect (on_insert_text); - current_view.buffer.delete_range.disconnect (on_delete_range); - // Disconnect show completion?? + disconnect_signals (); current_completion.get_providers ().foreach ((p) => { try { @@ -192,6 +190,16 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { } }); } + + private void connect_signals () { + current_view.buffer.insert_text.connect (on_insert_text); + current_view.buffer.delete_range.connect (on_delete_range); + } + + private void disconnect_signals () { + current_view.buffer.insert_text.disconnect (on_insert_text); + current_view.buffer.delete_range.disconnect (on_delete_range); + } } [ModuleInit] From d0fb053f02cfe5e262cea6bfcb42a966788af7a5 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 20 Dec 2024 17:31:33 +0000 Subject: [PATCH 45/46] Throttle completions when deleting --- plugins/word-completion/engine.vala | 1 + plugins/word-completion/plugin.vala | 28 +++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 60129ee0b7..8efdbf5799 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -65,6 +65,7 @@ public class Euclide.Completion.Parser : GLib.Object { return current_tree.completed; } + // This gets called from a thread public void initial_parse_buffer_text (string buffer_text) { parsing_cancelled = false; clear (); diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 9da563b5c0..4bc1c5ef9f 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -115,6 +115,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { if (view_to_parse == current_view) { try { new Thread.try ("word-completion-thread", () => { + // The initial parse gets cancelled if view switched before complete parser.initial_parse_buffer_text (view_to_parse.buffer.text); return null; }); @@ -164,13 +165,30 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { // Only one at most new words parser.add_word (to_add); + // Completions not usually shown after deletions so trigger it ourselves if (del_text.length == 1) { - // Wait until after buffer has been amended then trigger completion - Timeout.add (current_provider.interactive_delay * 2, () => { - debug ("showing completion"); - current_view.show_completion (); - return Source.REMOVE; + schedule_completion (); + } + } + + uint completion_timeout_id = 0; + bool wait = true; + // Wait until after buffer has finished being amended then trigger completion + private void schedule_completion () { + if (completion_timeout_id == 0) { + completion_timeout_id = Timeout.add (current_provider.interactive_delay, () => { + if (wait) { + wait = false; + return Source.CONTINUE; + } else { + completion_timeout_id = 0; + wait = true; + current_view.show_completion (); + return Source.REMOVE; + } }); + } else { + wait = true; } } From f9749fe529a33ac61251db4b6fe25b284ca107f1 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 20 Dec 2024 17:42:01 +0000 Subject: [PATCH 46/46] Aways connect signals --- plugins/word-completion/plugin.vala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 4bc1c5ef9f..69c97b6d63 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -126,9 +126,10 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { return Source.REMOVE; }); - } else { - connect_signals (); } + + // Always connect signals - they are disconnected in cleanup + connect_signals (); } // Runs before default handler so buffer text not yet modified. @pos must not be invalidated @@ -142,7 +143,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { var text_to_add = (word_before + new_text + word_after); var text_to_remove = (word_before + word_after); // Only update if words have changed - + debug ("insert text - add '%s' + '%s' + '%s'", word_before, new_text, word_after); if (text_to_add != text_to_remove) { parser.parse_text_and_add (text_to_add); parser.remove_word (text_to_remove); @@ -161,6 +162,7 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { var to_add = word_before + word_after; // More than one word could be deleted so parse. + debug ("delete range - remove '%s' + '%s' + '%s'", word_before, del_text, word_after); parser.parse_text_and_remove (to_remove); // Only one at most new words parser.add_word (to_add);