/* * Copyright (C) 2008-2010 Abderrahim Kitouni * * This program is 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 program 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. If not, see . */ using GLib; using Gtk; using Anjuta; public class ValaPlugin : Plugin, IAnjuta.Preferences { internal const string PREF_WIDGET_SPACE = "preferences:completion-space-after-func"; internal const string PREF_WIDGET_BRACE = "preferences:completion-brace-after-func"; internal const string PREF_WIDGET_AUTO = "preferences:completion-enable"; internal const string ICON_FILE = "anjuta-vala.png"; internal static string PREFS_BUILDER = Config.PACKAGE_DATA_DIR + "/glade/anjuta-vala.ui"; internal weak IAnjuta.Editor current_editor; internal GLib.Settings settings = new GLib.Settings ("org.gnome.anjuta.plugins.vala"); uint editor_watch_id; ulong project_loaded_id; Vala.CodeContext context; Cancellable cancel; BlockLocator locator = new BlockLocator (); AnjutaReport report; ValaProvider provider; Vala.Parser parser; Vala.Genie.Parser genie_parser; public static Gtk.Builder bxml; Vala.Set current_sources = new Vala.HashSet (str_hash, str_equal); ValaPlugin () { Object (); } public override bool activate () { debug("Activating ValaPlugin"); report = new AnjutaReport(); report.docman = (IAnjuta.DocumentManager) shell.get_object("IAnjutaDocumentManager"); parser = new Vala.Parser (); genie_parser = new Vala.Genie.Parser (); init_context (); provider = new ValaProvider(this); editor_watch_id = add_watch("document_manager_current_document", editor_value_added, editor_value_removed); return true; } public override bool deactivate () { debug("Deactivating ValaPlugin"); remove_watch(editor_watch_id, true); cancel.cancel (); lock (context) { context = null; } return true; } void init_context () { context = new Vala.CodeContext(); context.profile = Vala.Profile.GOBJECT; context.report = report; report.clear_error_indicators (); cancel = new Cancellable (); /* This doesn't actually parse anything as there are no files yet, it's just to set the context in the parsers */ parser.parse (context); genie_parser.parse (context); current_sources = new Vala.HashSet (str_hash, str_equal); } void parse () { try { Thread.create(() => { lock (context) { Vala.CodeContext.push(context); var report = context.report as AnjutaReport; foreach (var src in context.get_source_files ()) { if (src.get_nodes ().size == 0) { debug ("parsing file %s", src.filename); genie_parser.visit_source_file (src); parser.visit_source_file (src); } if (cancel.is_cancelled ()) { Vala.CodeContext.pop(); return; } } if (report.get_errors () > 0 || cancel.is_cancelled ()) { Vala.CodeContext.pop(); return; } context.check (); Vala.CodeContext.pop(); } }, false); } catch (ThreadError err) { warning ("cannot create thread : %s", err.message); } } void add_project_files () { var pm = (IAnjuta.ProjectManager) shell.get_object("IAnjutaProjectManager"); var project = pm.get_current_project (); var current_file = (current_editor as IAnjuta.File).get_file (); if (project == null) return; Vala.CodeContext.push (context); var current_src = project.get_root ().get_source_from_file (current_file); if (current_src == null) return; var current_target = current_src.parent_type (Anjuta.ProjectNodeType.TARGET); if (current_target == null) return; current_target.foreach (TraverseType.PRE_ORDER, (node) => { if (!(Anjuta.ProjectNodeType.SOURCE in node.get_node_type ())) return; if (node.get_file () == null) return; var path = node.get_file ().get_path (); if (path == null) return; if (path.has_suffix (".vala") || path.has_suffix (".vapi") || path.has_suffix (".gs")) { if (path in current_sources) { debug ("file %s already added", path); } else { context.add_source_filename (path); current_sources.add (path); debug ("file %s added", path); } } else { debug ("file %s skipped", path); } }); if (!context.has_package ("gobject-2.0")) { context.add_external_package("glib-2.0"); context.add_external_package("gobject-2.0"); debug ("standard packages added"); } else { debug ("standard packages already added"); } string[] flags = {}; unowned Anjuta.ProjectProperty prop = current_target.get_property ("VALAFLAGS"); if (prop != null && prop != prop.info.default_value) { GLib.Shell.parse_argv (prop.value, out flags); } else { /* Fall back to AM_VALAFLAGS */ var current_group = current_target.parent_type (Anjuta.ProjectNodeType.GROUP); prop = current_group.get_property ("VALAFLAGS"); if (prop != null && prop != prop.info.default_value) GLib.Shell.parse_argv (prop.value, out flags); } string[] packages = {}; string[] vapidirs = {}; for (int i = 0; i < flags.length; i++) { if (flags[i] == "--vapidir") vapidirs += flags[++i]; else if (flags[i].has_prefix ("--vapidir=")) vapidirs += flags[i].substring ("--vapidir=".length); else if (flags[i] == "--pkg") packages += flags[++i]; else if (flags[i].has_prefix ("--pkg=")) packages += flags[i].substring ("--pkg=".length); else debug ("Unknown valac flag %s", flags[i]); } var srcdir = current_target.parent_type (Anjuta.ProjectNodeType.GROUP).get_file ().get_path (); var top_srcdir = project.get_root ().get_file ().get_path (); for (int i = 0; i < vapidirs.length; i++) { vapidirs[i] = vapidirs[i].replace ("$(srcdir)", srcdir) .replace ("$(top_srcdir)", top_srcdir); } context.vapi_directories = vapidirs; foreach (var pkg in packages) { if (context.has_package (pkg)) { debug ("package %s skipped", pkg); } else if (context.add_external_package(pkg)) { debug ("package %s added", pkg); } else { debug ("package %s not found", pkg); } } Vala.CodeContext.pop(); } public void on_project_loaded (IAnjuta.ProjectManager pm, Error? e) { if (context == null) return; add_project_files (); parse (); pm.disconnect (project_loaded_id); project_loaded_id = 0; } /* "document_manager_current_document" watch */ public void editor_value_added (Anjuta.Plugin plugin, string name, Value value) { debug("editor value added"); assert (current_editor == null); if (!(value.get_object() is IAnjuta.Editor)) { /* a glade document, for example, isn't an editor */ return; } current_editor = value.get_object() as IAnjuta.Editor; var current_file = value.get_object() as IAnjuta.File; var pm = (IAnjuta.ProjectManager) shell.get_object("IAnjutaProjectManager"); var project = pm.get_current_project (); if (!project.is_loaded()) { if (project_loaded_id == 0) project_loaded_id = pm.project_loaded.connect (on_project_loaded); } else { var cur_gfile = current_file.get_file (); if (cur_gfile == null) { // File hasn't been saved yet return; } if (!(cur_gfile.get_path () in current_sources)) { cancel.cancel (); lock (context) { init_context (); add_project_files (); } parse (); } } if (current_editor != null) { if (current_editor is IAnjuta.EditorAssist) (current_editor as IAnjuta.EditorAssist).add(provider); if (current_editor is IAnjuta.EditorTip) current_editor.char_added.connect (on_char_added); if (current_editor is IAnjuta.FileSavable) { var file_savable = (IAnjuta.FileSavable) current_editor; file_savable.saved.connect (on_file_saved); } if (current_editor is IAnjuta.EditorGladeSignal) { var gladesig = current_editor as IAnjuta.EditorGladeSignal; gladesig.drop_possible.connect (on_drop_possible); gladesig.drop.connect (on_drop); } current_editor.glade_member_add.connect (insert_member_decl_and_init); } report.update_errors (current_editor); } public void editor_value_removed (Anjuta.Plugin plugin, string name) { debug("editor value removed"); if (current_editor is IAnjuta.EditorAssist) (current_editor as IAnjuta.EditorAssist).remove(provider); if (current_editor is IAnjuta.EditorTip) current_editor.char_added.disconnect (on_char_added); if (current_editor is IAnjuta.FileSavable) { var file_savable = (IAnjuta.FileSavable) current_editor; file_savable.saved.disconnect (on_file_saved); } if (current_editor is IAnjuta.EditorGladeSignal) { var gladesig = current_editor as IAnjuta.EditorGladeSignal; gladesig.drop_possible.disconnect (on_drop_possible); gladesig.drop.disconnect (on_drop); } current_editor.glade_member_add.disconnect (insert_member_decl_and_init); current_editor = null; } public void on_file_saved (IAnjuta.FileSavable savable, File file) { foreach (var source_file in context.get_source_files ()) { if (source_file.filename != file.get_path()) continue; uint8[] contents; try { file.load_contents (null, out contents, null); source_file.content = (string) contents; update_file (source_file); } catch (Error e) { // ignore } return; } } public void on_char_added (IAnjuta.Editor editor, IAnjuta.Iterable position, char ch) { if (!settings.get_boolean (ValaProvider.PREF_CALLTIP_ENABLE)) return; var editortip = editor as IAnjuta.EditorTip; if (ch == '(') { provider.show_call_tip (editortip); } else if (ch == ')') { editortip.cancel (); } } /* tries to find the opening brace of the scope the current position before calling * get_current_context since the source_reference of a class or namespace only * contain the declaration not the entire "content" */ Vala.Symbol? get_scope (IAnjuta.Editor editor, IAnjuta.Iterable position) { var depth = 0; do { var current_char = (position as IAnjuta.EditorCell).get_character (); if (current_char == "}") { depth++; } else if (current_char == "{") { if (depth > 0) { depth--; } else { // a scope which contains the current position do { position.previous (); current_char = (position as IAnjuta.EditorCell).get_character (); } while (! current_char.get_char ().isalnum ()); return get_current_context (editor, position); } } } while (position.previous ()); return null; } public bool on_drop_possible (IAnjuta.EditorGladeSignal editor, IAnjuta.Iterable position) { var line = editor.get_line_from_position (position); var column = editor.get_line_begin_position (line).diff (position); debug ("line %d, column %d", line, column); var scope = get_scope (editor, position.clone ()); if (scope != null) debug ("drag is inside %s", scope.get_full_name ()); if (scope == null || scope is Vala.Namespace || scope is Vala.Class) return true; return false; } public void on_drop (IAnjuta.EditorGladeSignal editor, IAnjuta.Iterable position, string signal_data) { var data = signal_data.split (":"); var widget_name = data[0]; var signal_name = data[1].replace ("-", "_"); var handler_name = data[2]; var swapped = (data[4] == "1"); var scope = get_scope (editor, position.clone ()); var builder = new StringBuilder (); #if VALA_0_38 var handler_cname = ""; #else var scope_prefix = ""; if (scope != null) { scope_prefix = Vala.CCodeBaseModule.get_ccode_lower_case_prefix (scope); if (handler_name.has_prefix (scope_prefix)) handler_name = handler_name.substring (scope_prefix.length); } var handler_cname = scope_prefix + handler_name; #endif if (data[2] != handler_cname && !swapped) { builder.append_printf ("[CCode (cname=\"%s\", instance_pos=-1)]\n", data[2]); } else if (data[2] != handler_cname) { builder.append_printf ("[CCode (cname=\"%s\")]\n", data[2]); } else if (!swapped) { builder.append ("[CCode (instance_pos=-1)]\n"); } var widget = lookup_symbol_by_cname (widget_name); var sigs = symbol_lookup_inherited (widget, signal_name, false); if (sigs == null || !(sigs.data is Vala.Signal)) return; Vala.Signal sig = (Vala.Signal) sigs.data; builder.append_printf ("public void %s (", handler_name); if (swapped) { builder.append_printf ("%s sender", widget.get_full_name ()); foreach (var param in sig.get_parameters ()) { builder.append_printf (", %s %s", param.variable_type.data_type.get_full_name (), param.name); } } else { foreach (var param in sig.get_parameters ()) { builder.append_printf ("%s %s, ", param.variable_type.data_type.get_full_name (), param.name); } builder.append_printf ("%s sender", widget.get_full_name ()); } builder.append_printf (") {\n\n}\n"); editor.insert (position, builder.str, -1); var indenter = shell.get_object ("IAnjutaIndenter") as IAnjuta.Indenter; if (indenter != null) { var end = position.clone (); /* -1 so we don't count the last newline (as that would indent the line after) */ end.set_position (end.get_position () + builder.str.char_count () - 1); indenter.indent (position, end); } var inside = editor.get_line_end_position (editor.get_line_from_position (position) + 2); editor.goto_position (inside); if (indenter != null) indenter.indent (inside, inside); } const string DECL_MARK = "/* ANJUTA: Widgets declaration for %s - DO NOT REMOVE */\n"; const string INIT_MARK = "/* ANJUTA: Widgets initialization for %s - DO NOT REMOVE */\n"; void insert_member_decl_and_init (IAnjuta.Editor editor, string widget_ctype, string widget_name, string filename) { var widget_type = lookup_symbol_by_cname (widget_ctype).get_full_name (); var basename = Path.get_basename (filename); string member_decl = "%s %s;\n".printf (widget_type, widget_name); string member_init = "%s = builder.get_object(\"%s\") as %s;\n".printf (widget_name, widget_name, widget_type); insert_after_mark (editor, DECL_MARK.printf (basename), member_decl) && insert_after_mark (editor, INIT_MARK.printf (basename), member_init); } bool insert_after_mark (IAnjuta.Editor editor, string mark, string code_to_add) { var search_start = editor.get_start_position () as IAnjuta.EditorCell; var search_end = editor.get_end_position () as IAnjuta.EditorCell; IAnjuta.EditorCell result_end; (editor as IAnjuta.EditorSearch).forward (mark, false, search_start, search_end, null, out result_end); var mark_position = result_end as IAnjuta.Iterable; if (mark_position == null) return false; editor.insert (mark_position, code_to_add, -1); var indenter = shell.get_object ("IAnjutaIndenter") as IAnjuta.Indenter; if (indenter != null) { var end = mark_position.clone (); /* -1 so we don't count the last newline (as that would indent the line after) */ end.set_position (end.get_position () + code_to_add.char_count () - 1); indenter.indent (mark_position, end); } /* Emit code-added signal, so symbols will be updated */ editor.code_added (mark_position, code_to_add); return true; } Vala.Symbol? lookup_symbol_by_cname (string cname, Vala.Symbol parent=context.root) { var sym = parent.scope.lookup (cname); if (sym != null) return sym; var symtab = parent.scope.get_symbol_table (); foreach (var name in symtab.get_keys ()) { if (cname.has_prefix (name)) { return lookup_symbol_by_cname (cname.substring (name.length), parent.scope.lookup (name)); } } return null; } internal Vala.Symbol get_current_context (IAnjuta.Editor editor, IAnjuta.Iterable? position=null) requires (editor is IAnjuta.File) { var file = editor as IAnjuta.File; var path = file.get_file().get_path(); lock (context) { Vala.SourceFile source = null; foreach (var src in context.get_source_files()) { if (src.filename == path) { source = src; break; } } if (source == null) { source = new Vala.SourceFile (context, path.has_suffix("vapi") ? Vala.SourceFileType.PACKAGE: Vala.SourceFileType.SOURCE, path); context.add_source_file(source); update_file(source); } int line; int column; if (position == null) { line = editor.get_lineno (); column = editor.get_column (); } else { line = editor.get_line_from_position (position); column = editor.get_line_begin_position (line).diff (position); } return locator.locate(source, line, column); } } internal List lookup_symbol (Vala.Expression? inner, string name, bool prefix_match, Vala.Block? block) { var matching_symbols = new List (); if (block == null) return matching_symbols; lock (context) { if (inner == null) { for (var sym = (Vala.Symbol) block; sym != null; sym = sym.parent_symbol) { matching_symbols.concat (symbol_lookup_inherited (sym, name, prefix_match)); } foreach (var ns in block.source_reference.file.current_using_directives) { matching_symbols.concat (symbol_lookup_inherited (ns.namespace_symbol, name, prefix_match)); } } else if (inner.symbol_reference != null) { matching_symbols.concat (symbol_lookup_inherited (inner.symbol_reference, name, prefix_match)); } else if (inner is Vala.MemberAccess) { var inner_ma = (Vala.MemberAccess) inner; var matching = lookup_symbol (inner_ma.inner, inner_ma.member_name, false, block); if (matching != null) matching_symbols.concat (symbol_lookup_inherited (matching.data, name, prefix_match)); } else if (inner is Vala.MethodCall) { var inner_inv = (Vala.MethodCall) inner; var inner_ma = inner_inv.call as Vala.MemberAccess; if (inner_ma != null) { var matching = lookup_symbol (inner_ma.inner, inner_ma.member_name, false, block); if (matching != null) matching_symbols.concat (symbol_lookup_inherited (matching.data, name, prefix_match, true)); } } } return matching_symbols; } List symbol_lookup_inherited (Vala.Symbol? sym, string name, bool prefix_match, bool invocation = false) { List result = null; // This may happen if we cannot find all the needed packages if (sym == null) return result; var symbol_table = sym.scope.get_symbol_table (); if (symbol_table != null) { foreach (string key in symbol_table.get_keys()) { if (((prefix_match && key.has_prefix (name)) || key == name)) { result.append (symbol_table[key]); } } } if (invocation && sym is Vala.Method) { var func = (Vala.Method) sym; result.concat (symbol_lookup_inherited (func.return_type.data_type, name, prefix_match)); } else if (sym is Vala.Class) { var cl = (Vala.Class) sym; foreach (var base_type in cl.get_base_types ()) { result.concat (symbol_lookup_inherited (base_type.data_type, name, prefix_match)); } } else if (sym is Vala.Struct) { var st = (Vala.Struct) sym; result.concat (symbol_lookup_inherited (st.base_type.data_type, name, prefix_match)); } else if (sym is Vala.Interface) { var iface = (Vala.Interface) sym; foreach (var prerequisite in iface.get_prerequisites ()) { result.concat (symbol_lookup_inherited (prerequisite.data_type, name, prefix_match)); } } else if (sym is Vala.LocalVariable) { var variable = (Vala.LocalVariable) sym; result.concat (symbol_lookup_inherited (variable.variable_type.data_type, name, prefix_match)); } else if (sym is Vala.Field) { var field = (Vala.Field) sym; result.concat (symbol_lookup_inherited (field.variable_type.data_type, name, prefix_match)); } else if (sym is Vala.Property) { var prop = (Vala.Property) sym; result.concat (symbol_lookup_inherited (prop.property_type.data_type, name, prefix_match)); } else if (sym is Vala.Parameter) { var fp = (Vala.Parameter) sym; result.concat (symbol_lookup_inherited (fp.variable_type.data_type, name, prefix_match)); } return result; } void update_file (Vala.SourceFile file) { lock (context) { /* Removing nodes in the same loop causes problems (probably due to ReadOnlyList)*/ var nodes = new Vala.ArrayList (); foreach (var node in file.get_nodes()) { nodes.add(node); } foreach (var node in nodes) { file.remove_node (node); if (node is Vala.Symbol) { var sym = (Vala.Symbol) node; if (sym.owner != null) /* we need to remove it from the scope*/ sym.owner.remove(sym.name); if (context.entry_point == sym) context.entry_point = null; } } file.current_using_directives = new Vala.ArrayList(); var ns_ref = new Vala.UsingDirective (new Vala.UnresolvedSymbol (null, "GLib")); file.add_using_directive (ns_ref); context.root.add_using_directive (ns_ref); report.clear_error_indicators (file); parse (); report.update_errors(current_editor); } } private void on_autocompletion_toggled (ToggleButton button) { var sensitive = button.get_active(); Gtk.Widget widget = bxml.get_object (PREF_WIDGET_SPACE) as Widget; widget.set_sensitive (sensitive); widget = bxml.get_object (PREF_WIDGET_BRACE) as Widget; widget.set_sensitive (sensitive); } public void merge (Anjuta.Preferences prefs) throws GLib.Error { bxml = new Builder(); /* Add preferences */ try { bxml.add_from_file (PREFS_BUILDER); } catch (Error err) { warning ("Couldn't load builder file: %s", err.message); } prefs.add_from_builder (bxml, settings, "preferences", _("Auto-complete"), ICON_FILE); var toggle = bxml.get_object (PREF_WIDGET_AUTO) as ToggleButton; toggle.toggled.connect (on_autocompletion_toggled); on_autocompletion_toggled (toggle); } public void unmerge (Anjuta.Preferences prefs) throws GLib.Error { prefs.remove_page (_("Auto-complete")); } } [ModuleInit] public Type anjuta_glue_register_components (TypeModule module) { return typeof (ValaPlugin); }