diff --git a/plugins/meson.build b/plugins/meson.build index 2e8855ee1d..ca19626854 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -9,7 +9,9 @@ subdir('outline') subdir('pastebin') subdir('preserve-indent') subdir('spell') +subdir('snippets') subdir('strip-trailing-save') subdir('terminal') subdir('vim-emulation') subdir('word-completion') + diff --git a/plugins/preserve-indent/preserve-indent.vala b/plugins/preserve-indent/preserve-indent.vala index 6f07e0c3d3..5e83214f70 100644 --- a/plugins/preserve-indent/preserve-indent.vala +++ b/plugins/preserve-indent/preserve-indent.vala @@ -55,30 +55,6 @@ public class Scratch.Plugins.PreserveIndent : Peas.ExtensionBase, Peas.Activatab public void update_state () { } - // determine how many characters precede a given iterator position - private int measure_indent_at_iter (Widgets.SourceView view, Gtk.TextIter iter) { - Gtk.TextIter line_begin, pos; - - view.buffer.get_iter_at_line (out line_begin, iter.get_line ()); - - pos = line_begin; - int indent = 0; - int tabwidth = Scratch.settings.get_int ("indent-width"); - - unichar ch = pos.get_char (); - while (pos.get_offset () < iter.get_offset () && ch != '\n' && ch.isspace ()) { - if (ch == '\t') { - indent += tabwidth; - } else { - ++indent; - } - - pos.forward_char (); - ch = pos.get_char (); - } - return indent; - } - private void on_cut_or_copy_clipboard () { Widgets.SourceView view = this.active_document.source_view; if (!view.auto_indent) { @@ -90,7 +66,7 @@ public class Scratch.Plugins.PreserveIndent : Peas.ExtensionBase, Peas.Activatab var buffer = view.buffer; if (buffer.get_selection_bounds (out select_begin, out select_end)) { - int indent = this.measure_indent_at_iter (view, select_begin); + int indent = Scratch.Utils.measure_indent_at_iter (view, select_begin); this.last_clipboard_indent_level = indent; } else { this.last_clipboard_indent_level = 0; @@ -135,110 +111,21 @@ public class Scratch.Plugins.PreserveIndent : Peas.ExtensionBase, Peas.Activatab view.buffer.get_iter_at_mark (out paste_begin, view.buffer.get_mark ("paste_start")); view.buffer.get_iter_at_mark (out paste_end, view.buffer.get_insert ()); - int indent_level = this.measure_indent_at_iter (view, paste_begin); + int indent_level = Scratch.Utils.measure_indent_at_iter (view, paste_begin); int indent_diff = indent_level - this.last_clipboard_indent_level; paste_begin.forward_line (); if (indent_diff > 0) { - this.increase_indent_in_region (view, paste_begin, paste_end, indent_diff); + Scratch.Utils.increase_indent_in_region (view, paste_begin, paste_end, indent_diff); } else if (indent_diff < 0) { - this.decrease_indent_in_region (view, paste_begin, paste_end, indent_diff.abs ()); + Scratch.Utils.decrease_indent_in_region (view, paste_begin, paste_end, indent_diff.abs ()); } view.buffer.delete_mark_by_name ("paste_start"); view.buffer.end_user_action (); this.waiting_for_clipboard_text = false; } - - private void increase_indent_in_region ( - Widgets.SourceView view, - Gtk.TextIter region_begin, - Gtk.TextIter region_end, - int nchars - ) { - int first_line = region_begin.get_line (); - int last_line = region_end.get_line (); - int buf_last_line = view.buffer.get_line_count () - 1; - - int nlines = (first_line - last_line).abs () + 1; - if (nlines < 1 || nchars < 1 || last_line < first_line || !view.editable - || first_line == buf_last_line - ) { - return; - } - - // add a string of whitespace to each line after the first pasted line - string indent_str; - - if (view.insert_spaces_instead_of_tabs) { - indent_str = string.nfill (nchars, ' '); - } else { - int tabwidth = Scratch.settings.get_int ("indent-width"); - int tabs = nchars / tabwidth; - int spaces = nchars % tabwidth; - - indent_str = string.nfill (tabs, '\t'); - if (spaces > 0) { - indent_str += string.nfill (spaces, ' '); - } - } - - Gtk.TextIter itr; - for (var i = first_line; i <= last_line; ++i) { - view.buffer.get_iter_at_line (out itr, i); - view.buffer.insert (ref itr, indent_str, indent_str.length); - } - } - - private void decrease_indent_in_region ( - Widgets.SourceView view, - Gtk.TextIter region_begin, - Gtk.TextIter region_end, - int nchars - ) { - int first_line = region_begin.get_line (); - int last_line = region_end.get_line (); - - int nlines = (first_line - last_line).abs () + 1; - if (nlines < 1 || nchars < 1 || last_line < first_line || !view.editable) { - return; - } - - Gtk.TextBuffer buffer = view.buffer; - int tabwidth = Scratch.settings.get_int ("indent-width"); - Gtk.TextIter del_begin, del_end, itr; - - for (var line = first_line; line <= last_line; ++line) { - buffer.get_iter_at_line (out itr, line); - // crawl along the line and tally indentation as we go, - // when requested number of chars is hit, or if we run out of whitespace (eg. find glyphs or newline), - // delete the segment from line start to where we are now - int chars_to_delete = 0; - int indent_chars_found = 0; - unichar ch = itr.get_char (); - while (ch != '\n' && !ch.isgraph () && indent_chars_found < nchars) { - if (ch == ' ') { - ++chars_to_delete; - ++indent_chars_found; - } else if (ch == '\t') { - ++chars_to_delete; - indent_chars_found += tabwidth; - } - itr.forward_char (); - ch = itr.get_char (); - } - - if (ch == '\n' || chars_to_delete < 1) { - continue; - } - - buffer.get_iter_at_line (out del_begin, line); - buffer.get_iter_at_line_offset (out del_end, line, chars_to_delete); - buffer.delete (ref del_begin, ref del_end); - } - - } } [ModuleInit] diff --git a/plugins/snippets/meson.build b/plugins/snippets/meson.build new file mode 100644 index 0000000000..04d076b645 --- /dev/null +++ b/plugins/snippets/meson.build @@ -0,0 +1,40 @@ +module_name = 'snippets' + +module_files = [ + 'plugin.vala' +] + +json_dep = dependency('json-glib-1.0') + +module_deps = [ + codecore_dep, + json_dep +] + +shared_module( + module_name, + module_files, + dependencies: module_deps, + install: true, + install_dir: join_paths(pluginsdir, module_name), +) + +install_data( + 'snippets.json', + install_dir: join_paths(pluginsdir, module_name), +) + +custom_target(module_name + '.plugin_merge', + input: module_name + '.plugin', + output: module_name + '.plugin', + command : [msgfmt, + '--desktop', + '--keyword=Description', + '--keyword=Name', + '-d' + join_paths(meson.source_root (), 'po', 'plugins'), + '--template=@INPUT@', + '-o@OUTPUT@', + ], + install : true, + install_dir: join_paths(pluginsdir, module_name), +) diff --git a/plugins/snippets/plugin.vala b/plugins/snippets/plugin.vala new file mode 100644 index 0000000000..c5a382ce0a --- /dev/null +++ b/plugins/snippets/plugin.vala @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2021 Igor Montagner + * + * 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. + * + */ + + + private class Code.Plugins.Snippets.Snippet : Object { + public string name {get; construct; } + public string tag {get; construct; } + public string language {get; construct; } + + public Gee.ArrayList tabstops; + public uint n_tabstops { + get { + return tabstops.size; + } + } + + private string _body; + public string body { + get { return _body; } + set { + _body = value; + + // PARSE $1 - $n + int next_tabstop = 1; + int next_tabstop_idx = 1; + while ((next_tabstop_idx = _body.index_of ("$%d".printf (next_tabstop), 0)) >= 0) { + for (int i = 0; i < next_tabstop - 1; i++) { + if (tabstops[i] > next_tabstop_idx) { + tabstops[i] -= 2; + } + } + + tabstops.add (next_tabstop_idx); + _body = _body.splice (next_tabstop_idx, next_tabstop_idx + 2); + next_tabstop++; + } + + // PARSE $0 + next_tabstop_idx = _body.index_of ("$0", 0); + if (next_tabstop_idx >= 0) { + for (int i = 0; i < next_tabstop - 1; i++) { + if (tabstops[i] > next_tabstop_idx) { + tabstops[i] -= 2; + } + } + tabstops.add (next_tabstop_idx); + _body = _body.splice (next_tabstop_idx, next_tabstop_idx + 2); + } else { + tabstops.add (_body.length); + } + } + } + + public Snippet (string name, string tag, string body, string language) { + Object ( + name: name, + tag: tag, + body: body, + language: language + ); + } + + construct { + tabstops = new Gee.ArrayList (); + } +} + +private class Code.Plugins.Snippets.Provider : Gtk.SourceCompletionProvider, Object { + public Code.Plugins.Snippets.Plugin snippets; + public Gee.HashMultiMap snippet_map; + + private int current_tabstop = 0; + private Snippet current_editing_snippet; + private uint placeholder_edit_timeout = -1; + private bool still_editing_placeholder = false; + + private const int FINISH_EDITING_TIMEOUT = 1000; + + construct { + snippet_map = new Gee.HashMultiMap (); + + var par = new Json.Parser (); + string user_snippets_filename = GLib.Path.build_filename (Constants.PLUGINDIR, "snippets", "snippets.json"); + par.load_from_file (user_snippets_filename); + var reader = new Json.Reader (par.get_root ()); + + foreach (string language in reader.list_members ()) { + reader.read_member (language); + + var n_snippets = reader.count_elements (); + for (int i = 0; i < n_snippets; i++) { + reader.read_element (i); + + reader.read_member ("name"); + var name = reader.get_string_value (); + reader.end_member (); + + reader.read_member ("tag"); + var tag = reader.get_string_value (); + reader.end_member (); + + reader.read_member ("body"); + var body = reader.get_string_value (); + reader.end_member (); + + var snippet = new Snippet (name, tag, body, language); + + snippet_map.@set (language, snippet); + reader.end_element (); + } + reader.end_member (); + } + + } + + public string get_name () { + return "Snippets"; + } + + private string word_prefix (Gtk.SourceCompletionContext context) { + var ins_mark = snippets.current_view.buffer.get_insert (); + Gtk.TextIter? word; + snippets.current_view.buffer.get_iter_at_mark (out word, ins_mark); + var word_start = word.copy (); + word_start.backward_word_start (); + + return word.get_buffer ().get_text (word_start, word, false); + } + + public bool match (Gtk.SourceCompletionContext context) { + if (still_editing_placeholder) { + return false; + } + + var word = word_prefix (context); + var snips = snippet_map.@get (snippets.current_document.get_language_name ()); + var found = false; + snips.foreach ((sni) => { + if (sni.tag.has_prefix (word)) found = true; + return !found; + }); + return found; + } + + public void populate (Gtk.SourceCompletionContext context) { + var word = word_prefix (context); + var snips = snippet_map.@get (snippets.current_document.get_language_name ()); + var props = new List (); + + snips.foreach ((sni) => { + if (sni.tag.has_prefix (word)) { + var item = new Gtk.SourceCompletionItem () { + label = sni.name, + text = sni.body, + info = sni.body + }; + item.set_data ("snippet", sni); + props.append (item); + } + return true; + }); + context.add_proposals (this, props, true); + } + + public void start_editing_placeholders () { + var current_view = snippets.current_view; + var buffer = current_view.buffer; + Gtk.TextIter snippet_start, snippet_end; + buffer.get_iter_at_mark (out snippet_start, buffer.get_mark ("SNIPPET_START")); + buffer.get_iter_at_mark (out snippet_end, buffer.get_mark ("SNIPPET_END")); + + for (int i = 0; i < current_editing_snippet.n_tabstops; i++) { + Gtk.TextIter tab_i = snippet_start.copy (); + tab_i.forward_chars (current_editing_snippet.tabstops[i]); + current_view.buffer.create_mark ("SNIPPET_TAB_%d".printf (i), tab_i, true); + } + + var indent_start = Scratch.Utils.measure_indent_at_iter (current_view, snippet_start); + var snippet_line2 = snippet_start.copy (); + snippet_line2.forward_line (); + Scratch.Utils.increase_indent_in_region (current_view, snippet_line2, snippet_end, indent_start); + + current_tabstop = 0; + place_cursor_at_tabstop (buffer, current_tabstop); + if (current_editing_snippet.n_tabstops > 1) { + current_view.key_press_event.connect (next_placeholder); + + still_editing_placeholder = true; + placeholder_edit_timeout = Timeout.add (FINISH_EDITING_TIMEOUT, () => { + if (!still_editing_placeholder) { + end_editing_placeholders (); + return false; + } + still_editing_placeholder = false; + return true; + }); + } else { + end_editing_placeholders (); + } + } + + public bool next_placeholder (Gdk.EventKey evt) { + still_editing_placeholder = true; + if (evt.keyval == Gdk.Key.Tab) { + var current_view = snippets.current_view; + var buffer = current_view.buffer; + + current_tabstop++; + place_cursor_at_tabstop (buffer, current_tabstop); + if (current_tabstop == current_editing_snippet.n_tabstops - 1) { + end_editing_placeholders (); + } + + return true; + } + + return false; + } + + public void place_cursor_at_tabstop (Gtk.TextBuffer buffer, int tabstop) { + Gtk.TextIter iter_start; + var mark = buffer.get_mark ("SNIPPET_TAB_%d".printf (tabstop)); + buffer.get_iter_at_mark (out iter_start, mark); + buffer.place_cursor (iter_start); + } + + public void end_editing_placeholders () { + still_editing_placeholder = false; + if (current_editing_snippet.n_tabstops > 0) { + snippets.current_view.key_press_event.disconnect (next_placeholder); + } + + for (int i = 0; i < current_editing_snippet.n_tabstops; i++) { + snippets.current_view.buffer.delete_mark_by_name ("SNIPPET_TAB_%d".printf (i)); + } + + snippets.current_view.buffer.delete_mark_by_name ("SNIPPET_START"); + snippets.current_view.buffer.delete_mark_by_name ("SNIPPET_END"); + Source.remove (placeholder_edit_timeout); + placeholder_edit_timeout = -1; + } + + public bool activate_proposal (Gtk.SourceCompletionProposal proposal, Gtk.TextIter iter) { + current_editing_snippet = proposal.get_data ("snippet"); + var iter_start = iter.copy (); + iter_start.backward_word_start (); + + var current_buffer = iter.get_buffer (); + current_buffer.delete (ref iter_start, ref iter); + current_buffer.create_mark ("SNIPPET_START", iter_start, true); + current_buffer.create_mark ("SNIPPET_END", iter_start, false); + iter.get_buffer ().insert (ref iter, current_editing_snippet.body, current_editing_snippet.body.length); + start_editing_placeholders (); + return true; + } +} + + +public class Code.Plugins.Snippets.Plugin : Peas.ExtensionBase, Peas.Activatable { + public Object object { owned get; construct; } + + private List text_view_list = new List (); + public Scratch.Widgets.SourceView? current_view {get; private set;} + public Scratch.Services.Document current_document {get; private set;} + + private Scratch.MainWindow main_window; + private Scratch.Services.Interface plugins; + + public void activate () { + plugins = (Scratch.Services.Interface) object; + plugins.hook_window.connect ((w) => { + this.main_window = w; + }); + + plugins.hook_document.connect (on_new_source_view); + } + + public void deactivate () { + text_view_list.@foreach (cleanup); + } + + public void update_state () { + + } + + public void on_new_source_view (Scratch.Services.Document doc) { + if (current_view != null) { + if (current_view == doc.source_view) + return; + + cleanup (current_view); + } + + current_document = doc; + current_view = doc.source_view; + + if (text_view_list.find (current_view) == null) + text_view_list.append (current_view); + + var comp_provider = new Code.Plugins.Snippets.Provider () { + snippets = this + }; + + try { + current_view.completion.add_provider (comp_provider); + current_view.completion.show_headers = true; + current_view.completion.show_icons = true; + } catch (Error e) { + warning (e.message); + } + } + + private void cleanup (Gtk.SourceView view) { + current_view.completion.get_providers ().foreach ((p) => { + try { + /* Only remove provider added by this plug in */ + if (p.get_name () == "Snippets") { + debug ("removing provider %s", p.get_name ()); + current_view.completion.remove_provider (p); + } + } catch (Error e) { + warning (e.message); + } + }); + } +} + +[ModuleInit] +public void peas_register_types (GLib.TypeModule module) { + var objmodule = module as Peas.ObjectModule; + objmodule.register_extension_type (typeof (Peas.Activatable), + typeof (Code.Plugins.Snippets.Plugin)); +} diff --git a/plugins/snippets/snippets.json b/plugins/snippets/snippets.json new file mode 100644 index 0000000000..ffcb2db956 --- /dev/null +++ b/plugins/snippets/snippets.json @@ -0,0 +1,18 @@ +{ + "Vala": [ + {"name": "Namespace", "tag": "name", "body": "namespace $1 {\n $0\n}"}, + {"name": "Class", "tag": "class", "body": "class $1 {\n $0\n}"}, + {"name": "Construct", "tag": "constr", "body": "construct {\n $0\n}"}, + + {"name": "While", "tag": "while", "body": "while ($1) {\n $0\n}"}, + {"name": "ForEach", "tag": "foreach", "body": "foreach ($1 in $2) {\n $0\n}"}, + {"name": "For", "tag": "for", "body": "for ($1; $2; $3) {\n $0\n}"}, + + {"name": "If", "tag": "if", "body": "if ($1) {\n $0\n}"}, + {"name": "IfElse", "tag": "ifelse", "body": "if ($1) {\n $0\n} else {\n \n}"}, + + {"name": "Public Method", "tag": "pubfunc", "body": "public $1 $2 ($3) {\n $0\n}"}, + {"name": "Private Method", "tag": "privfunc", "body": "public $1 $2 ($3) {\n $0\n}"} + + ] +} \ No newline at end of file diff --git a/plugins/snippets/snippets.plugin b/plugins/snippets/snippets.plugin new file mode 100644 index 0000000000..edb4385959 --- /dev/null +++ b/plugins/snippets/snippets.plugin @@ -0,0 +1,9 @@ +[Plugin] +Name=Snippets +Module=snippets +Loader=C +IAge=2 +Description=Turbo charge productivity using snippets +Authors=Igor Montagner +Copyright=Copyright © Igor Montagner +Website=https://github.com/elementary/code diff --git a/plugins/word-completion/completion-provider.vala b/plugins/word-completion/completion-provider.vala index 7a9071a793..af3e4cba70 100644 --- a/plugins/word-completion/completion-provider.vala +++ b/plugins/word-completion/completion-provider.vala @@ -28,7 +28,6 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, private Gtk.TextView? view; private Gtk.TextBuffer? buffer; private Euclide.Completion.Parser parser; - private bool proposals_found = false; private Gtk.TextMark completion_end_mark; private Gtk.TextMark completion_start_mark; @@ -53,40 +52,36 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, } public bool match (Gtk.SourceCompletionContext context) { - return true; + Gtk.TextIter start, end; + buffer.get_iter_at_offset (out end, buffer.cursor_position); + start = end.copy (); + start.backward_word_start (); + string text = buffer.get_text (start, end, true); + + return parser.match (text); } public void populate (Gtk.SourceCompletionContext context) { /*Store current insertion point for use in activate_proposal */ GLib.List? file_props; bool no_minimum = (context.get_activation () == Gtk.SourceCompletionActivation.USER_REQUESTED); - proposals_found = get_proposals (out file_props, no_minimum); - - if (proposals_found) - context.add_proposals (this, file_props, true); - - /* Signal to plugin whether proposals are available - * If none, the completion will be active but not visible */ - can_propose (proposals_found); + get_proposals (out file_props, no_minimum); + context.add_proposals (this, file_props, true); } public bool activate_proposal (Gtk.SourceCompletionProposal proposal, Gtk.TextIter iter) { - if (proposals_found) { - /* Count backward from completion_mark instead of iter - * (avoids wrong insertion if the user is typing fast) */ - Gtk.TextIter start; - Gtk.TextIter end; - Gtk.TextMark mark; + Gtk.TextIter start; + Gtk.TextIter end; + Gtk.TextMark mark; - mark = buffer.get_mark (COMPLETION_END_MARK_NAME); - buffer.get_iter_at_mark (out end, mark); + mark = buffer.get_mark (COMPLETION_END_MARK_NAME); + buffer.get_iter_at_mark (out end, mark); - mark = buffer.get_mark (COMPLETION_START_MARK_NAME); - buffer.get_iter_at_mark (out start, mark); + mark = buffer.get_mark (COMPLETION_START_MARK_NAME); + buffer.get_iter_at_mark (out start, mark); - buffer.@delete (ref start, ref end); - buffer.insert (ref start, proposal.get_text (), proposal.get_text ().length); - } + buffer.@delete (ref start, ref end); + buffer.insert (ref start, proposal.get_text (), proposal.get_text ().length); return true; } @@ -95,11 +90,6 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, Gtk.SourceCompletionActivation.USER_REQUESTED; } - public unowned Gtk.Widget? get_info_widget (Gtk.SourceCompletionProposal proposal) { - /* As no additional info is provided no widget is needed */ - return null; - } - public int get_interactive_delay () { return 0; } @@ -116,11 +106,6 @@ public class Scratch.Plugins.CompletionProvider : Gtk.SourceCompletionProvider, return true; } - public void update_info (Gtk.SourceCompletionProposal proposal, Gtk.SourceCompletionInfo info) { - /* No additional info provided on proposals */ - return; - } - private bool get_proposals (out GLib.List? props, bool no_minimum) { string to_find = ""; Gtk.TextIter iter; @@ -158,13 +143,13 @@ 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 */ - Gee.TreeSet prop_word_list; + List prop_word_list; if (parser.get_for_word (to_find, out prop_word_list)) { foreach (var word in prop_word_list) { var item = new Gtk.SourceCompletionItem (); item.label = word; item.text = word; - props.prepend (item); + props.append (item); } return true; diff --git a/plugins/word-completion/engine.vala b/plugins/word-completion/engine.vala index 55273ffa29..6fa58a1fc2 100644 --- a/plugins/word-completion/engine.vala +++ b/plugins/word-completion/engine.vala @@ -22,74 +22,58 @@ public class Euclide.Completion.Parser : GLib.Object { public const int MINIMUM_WORD_LENGTH = 1; public const int MAX_TOKENS = 1000000; + private Scratch.Plugins.PrefixTree prefix_tree; + public const string DELIMITERS = " .,;:?{}[]()0123456789+-=&|-<>*\\/\n\t\'\""; public bool is_delimiter (unichar c) { return DELIMITERS.index_of_char (c) >= 0; } - public Gee.HashMap> text_view_words; + public Gee.HashMap text_view_words; public bool parsing_cancelled = false; - private Gee.ArrayList words; - private string last_word = ""; - public Parser () { - text_view_words = new Gee.HashMap> (); + text_view_words = new Gee.HashMap (); + prefix_tree = new Scratch.Plugins.PrefixTree (); } - public void add_last_word () { - add_word (last_word); + public bool match (string to_find) { + return prefix_tree.find_prefix (to_find); } - public bool get_for_word (string to_find, out Gee.TreeSet list) { - uint length = to_find.length; - list = new Gee.TreeSet (); - last_word = to_find; - if (words != null) { - lock (words) { - foreach (var word in words) { - if (word.length > length && word.slice (0, length) == to_find) { - list.add (word); - } - } - } - } - - return !list.is_empty; + public bool get_for_word (string to_find, out List list) { + list = prefix_tree.get_all_matches (to_find); + return list.length () > 0; } public void rebuild_word_list (Gtk.TextView view) { - lock (words) { - words.clear (); - } + prefix_tree.clear (); parse_text_view (view); } public void parse_text_view (Gtk.TextView view) { /* If this view has already been parsed, restore the word list */ - lock (words) { + lock (prefix_tree) { if (text_view_words.has_key (view)) { - words = text_view_words.@get (view); + prefix_tree = text_view_words.@get (view); } else { - /* Else create a new word list and parse the buffer text */ - words = new Gee.ArrayList (); + /* Else create a new word list and parse the buffer text */ + prefix_tree = new Scratch.Plugins.PrefixTree (); } } if (view.buffer.text.length > 0) { parse_string (view.buffer.text); - text_view_words.@set (view, words); + text_view_words.@set (view, prefix_tree); } } - private void add_word (string word) { + public void add_word (string word) { if (word.length < MINIMUM_WORD_LENGTH) return; - if (!(word in words)) { - lock (words) { - words.add (word); - } + lock (prefix_tree) { + prefix_tree.insert (word); } } diff --git a/plugins/word-completion/meson.build b/plugins/word-completion/meson.build index 6aa1ed0225..2887aef312 100644 --- a/plugins/word-completion/meson.build +++ b/plugins/word-completion/meson.build @@ -1,6 +1,7 @@ module_name = 'word-completion' module_files = [ + 'prefix-tree.vala', 'completion-provider.vala', 'engine.vala', 'plugin.vala' diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index 0debc5c6b1..29fe371915 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -41,7 +41,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { private const uint USER_REQUESTED_KEY = Gdk.Key.backslash; private uint timeout_id = 0; - private bool completion_visible = false; public void activate () { plugins = (Scratch.Services.Interface) object; @@ -77,8 +76,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { current_document = doc; current_view = doc.source_view; current_view.key_press_event.connect (on_key_press); - current_view.completion.show.connect (on_completion_shown); - current_view.completion.hide.connect (on_completion_hidden); if (text_view_list.find (current_view) == null) text_view_list.append (current_view); @@ -86,11 +83,10 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { var comp_provider = new Scratch.Plugins.CompletionProvider (this); comp_provider.priority = 1; comp_provider.name = provider_name_from_document (doc); - comp_provider.can_propose.connect (on_propose); try { current_view.completion.add_provider (comp_provider); - current_view.completion.show_headers = false; + current_view.completion.show_headers = true; current_view.completion.show_icons = true; /* Wait a bit to allow text to load then run parser*/ timeout_id = Timeout.add (1000, on_timeout_update); @@ -134,49 +130,32 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { parser.rebuild_word_list (current_view); current_view.show_completion (); return true; - } else - return false; + } } - bool activating = kv in ACTIVATE_KEYS; + if (kv in ACTIVATE_KEYS || + (parser.is_delimiter (uc) && uc.isprint ()) ) { + var buffer = current_view.buffer; + var mark = buffer.get_insert (); + Gtk.TextIter cursor_iter; + buffer.get_iter_at_mark (out cursor_iter, mark); - if (completion_visible && activating) { - current_view.completion.activate_proposal (); - parser.add_last_word (); - return true; - } + var word_start = cursor_iter; + word_start.backward_word_start (); - if (activating || (uc.isprint () && parser.is_delimiter (uc) )) { - parser.add_last_word (); - current_view.completion.hide (); + string word = buffer.get_text (word_start, cursor_iter, false); + parser.add_word (word); } return false; } - private void on_completion_shown () { - completion_visible = true; - } - - private void on_completion_hidden () { - completion_visible = false; - } - - private void on_propose (bool can_propose) { - if (!can_propose) - current_view.completion.hide (); - - completion_visible = can_propose; - } - private string provider_name_from_document (Scratch.Services.Document doc) { return _("%s - Word Completion").printf (doc.get_basename ()); } private void cleanup (Gtk.SourceView view) { current_view.key_press_event.disconnect (on_key_press); - current_view.completion.show.disconnect (on_completion_shown); - current_view.completion.hide.disconnect (on_completion_hidden); current_view.completion.get_providers ().foreach ((p) => { try { @@ -189,8 +168,6 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { warning (e.message); } }); - - completion_visible = false; } } diff --git a/plugins/word-completion/prefix-tree.vala b/plugins/word-completion/prefix-tree.vala new file mode 100644 index 0000000000..1b437efc4f --- /dev/null +++ b/plugins/word-completion/prefix-tree.vala @@ -0,0 +1,116 @@ + +namespace Scratch.Plugins { + private class PrefixNode : Object { + public GLib.List children; + public unichar value { get; set; } + + construct { + children = new List (); + } + } + + public class PrefixTree : Object { + private PrefixNode root; + + construct { + clear (); + } + + public void clear () { + root = new PrefixNode () { + value = '\0' + }; + } + + public void insert (string word) { + if (word.length == 0) { + return; + } + + this.insert_at (word, this.root); + } + + private void insert_at (string word, PrefixNode node, int i = 0) { + unichar curr; + + word.get_next_char (ref i, out curr); + + foreach (var child in node.children) { + if (child.value == curr) { + if (curr != '\0') { + insert_at (word, child, i); + } + return; + } + } + + if (!curr.isprint () && curr != '\0') { + return; + } + + var new_child = new PrefixNode () { + value = curr + }; + node.children.insert_sorted (new_child, (c1, c2) => { + if (c1.value > c2.value) { + return 1; + } else if (c1.value == c2.value) { + return 0; + } + return -1; + }); + if (curr != '\0') { + insert_at (word, new_child, i); + } + } + + public bool find_prefix (string prefix) { + return find_prefix_at (prefix, root) != null? true : false; + } + + private PrefixNode? find_prefix_at (string prefix, PrefixNode node, int i = 0) { + unichar curr; + + prefix.get_next_char (ref i, out curr); + if (curr == '\0') { + return node; + } + + foreach (var child in node.children) { + if (child.value == curr) { + return find_prefix_at (prefix, child, i); + } + } + + return null; + } + + public List get_all_matches (string prefix) { + var list = new List (); + var node = find_prefix_at (prefix, root, 0); + if (node != null) { + var sb = new StringBuilder (prefix); + get_all_matches_rec (node, ref sb, ref list); + } + + return list; + } + + private void get_all_matches_rec ( + PrefixNode node, + ref StringBuilder sbuilder, + ref List matches) { + + foreach (var child in node.children) { + if (child.value == '\0') { + matches.append (sbuilder.str); + } else { + sbuilder.append_unichar (child.value); + get_all_matches_rec (child, ref sbuilder, ref matches); + var length = child.value.to_string ().length; + sbuilder.erase (sbuilder.len - length, -1); + } + } + } + } +} diff --git a/src/Utils.vala b/src/Utils.vala index f96356af5f..3d39d6cef3 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -143,6 +143,118 @@ namespace Scratch.Utils { } } + // determine how many characters precede a given iterator position + public int measure_indent_at_iter (Widgets.SourceView view, Gtk.TextIter iter) { + Gtk.TextIter line_begin, pos; + + view.buffer.get_iter_at_line (out line_begin, iter.get_line ()); + + pos = line_begin; + int indent = 0; + int tabwidth = Scratch.settings.get_int ("indent-width"); + + unichar ch = pos.get_char (); + while (pos.get_offset () < iter.get_offset () && ch != '\n' && ch.isspace ()) { + if (ch == '\t') { + indent += tabwidth; + } else { + ++indent; + } + + pos.forward_char (); + ch = pos.get_char (); + } + return indent; + } + + public void increase_indent_in_region ( + Widgets.SourceView view, + Gtk.TextIter region_begin, + Gtk.TextIter region_end, + int nchars + ) { + int first_line = region_begin.get_line (); + int last_line = region_end.get_line (); + int buf_last_line = view.buffer.get_line_count () - 1; + + int nlines = (first_line - last_line).abs () + 1; + if (nlines < 1 || nchars < 1 || last_line < first_line || !view.editable + || first_line == buf_last_line + ) { + return; + } + + // add a string of whitespace to each line after the first pasted line + string indent_str; + + if (view.insert_spaces_instead_of_tabs) { + indent_str = string.nfill (nchars, ' '); + } else { + int tabwidth = Scratch.settings.get_int ("indent-width"); + int tabs = nchars / tabwidth; + int spaces = nchars % tabwidth; + + indent_str = string.nfill (tabs, '\t'); + if (spaces > 0) { + indent_str += string.nfill (spaces, ' '); + } + } + + Gtk.TextIter itr; + for (var i = first_line; i <= last_line; ++i) { + view.buffer.get_iter_at_line (out itr, i); + view.buffer.insert (ref itr, indent_str, indent_str.length); + } + } + + public void decrease_indent_in_region ( + Widgets.SourceView view, + Gtk.TextIter region_begin, + Gtk.TextIter region_end, + int nchars + ) { + int first_line = region_begin.get_line (); + int last_line = region_end.get_line (); + + int nlines = (first_line - last_line).abs () + 1; + if (nlines < 1 || nchars < 1 || last_line < first_line || !view.editable) { + return; + } + + Gtk.TextBuffer buffer = view.buffer; + int tabwidth = Scratch.settings.get_int ("indent-width"); + Gtk.TextIter del_begin, del_end, itr; + + for (var line = first_line; line <= last_line; ++line) { + buffer.get_iter_at_line (out itr, line); + // crawl along the line and tally indentation as we go, + // when requested number of chars is hit, or if we run out of whitespace (eg. find glyphs or newline), + // delete the segment from line start to where we are now + int chars_to_delete = 0; + int indent_chars_found = 0; + unichar ch = itr.get_char (); + while (ch != '\n' && !ch.isgraph () && indent_chars_found < nchars) { + if (ch == ' ') { + ++chars_to_delete; + ++indent_chars_found; + } else if (ch == '\t') { + ++chars_to_delete; + indent_chars_found += tabwidth; + } + itr.forward_char (); + ch = itr.get_char (); + } + + if (ch == '\n' || chars_to_delete < 1) { + continue; + } + + buffer.get_iter_at_line (out del_begin, line); + buffer.get_iter_at_line_offset (out del_end, line, chars_to_delete); + buffer.delete (ref del_begin, ref del_end); + } + } + public bool find_unique_path (File f1, File f2, out string? path1, out string? path2) { if (f1.equal (f2)) { path1 = null;