diff --git a/plugins/meson.build b/plugins/meson.build index aa8891eb76..794a5b616b 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -8,5 +8,7 @@ subdir('markdown-actions') subdir('pastebin') subdir('preserve-indent') subdir('spell') +subdir('snippets') 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/src/Utils.vala b/src/Utils.vala index bf3b8f8247..7a68056784 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;