2 * Copyright (C) 2008-2010 Abderrahim Kitouni
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 public class ValaPlugin
: Plugin
, IAnjuta
.Preferences
{
23 internal const string PREF_WIDGET_SPACE
= "preferences:completion-space-after-func";
24 internal const string PREF_WIDGET_BRACE
= "preferences:completion-brace-after-func";
25 internal const string PREF_WIDGET_AUTO
= "preferences:completion-enable";
26 internal const string ICON_FILE
= "anjuta-vala.png";
27 internal static string PREFS_BUILDER
= Config
.PACKAGE_DATA_DIR
+ "/glade/anjuta-vala.ui";
29 internal weak IAnjuta
.Editor current_editor
;
30 internal GLib
.Settings settings
= new GLib
.Settings ("org.gnome.anjuta.plugins.vala");
32 ulong project_loaded_id
;
34 Vala
.CodeContext context
;
36 BlockLocator locator
= new
BlockLocator ();
39 ValaProvider provider
;
42 Vala
.Genie
.Parser genie_parser
;
44 public static Gtk
.Builder bxml
;
46 Vala
.Set
<string> current_sources
= new Vala
.HashSet
<string> (str_hash
, str_equal
);
50 public override bool activate () {
51 debug("Activating ValaPlugin");
52 report
= new
AnjutaReport();
53 report
.docman
= (IAnjuta
.DocumentManager
) shell
.get_object("IAnjutaDocumentManager");
54 parser
= new Vala
.Parser ();
55 genie_parser
= new Vala
.Genie
.Parser ();
59 provider
= new
ValaProvider(this
);
60 editor_watch_id
= add_watch("document_manager_current_document",
62 editor_value_removed
);
67 public override bool deactivate () {
68 debug("Deactivating ValaPlugin");
69 remove_watch(editor_watch_id
, true);
79 void init_context () {
80 context
= new Vala
.CodeContext();
81 context
.profile
= Vala
.Profile
.GOBJECT
;
82 context
.report
= report
;
83 report
.clear_error_indicators ();
85 cancel
= new
Cancellable ();
87 /* This doesn't actually parse anything as there are no files yet,
88 it's just to set the context in the parsers */
89 parser
.parse (context
);
90 genie_parser
.parse (context
);
92 current_sources
= new Vala
.HashSet
<string> (str_hash
, str_equal
);
98 Thread
.create
<void>(() => {
100 Vala
.CodeContext
.push(context
);
101 var report
= context
.report as AnjutaReport
;
103 foreach (var src
in context
.get_source_files ()) {
104 if (src
.get_nodes ().size
== 0) {
105 debug ("parsing file %s", src
.filename
);
106 genie_parser
.visit_source_file (src
);
107 parser
.visit_source_file (src
);
110 if (cancel
.is_cancelled ()) {
111 Vala
.CodeContext
.pop();
116 if (report
.get_errors () > 0 || cancel
.is_cancelled ()) {
117 Vala
.CodeContext
.pop();
122 Vala
.CodeContext
.pop();
125 } catch (ThreadError err
) {
126 warning ("cannot create thread : %s", err
.message
);
130 void add_project_files () {
131 var pm
= (IAnjuta
.ProjectManager
) shell
.get_object("IAnjutaProjectManager");
132 var project
= pm
.get_current_project ();
133 var current_file
= (current_editor as IAnjuta
.File
).get_file ();
137 Vala
.CodeContext
.push (context
);
139 var current_src
= project
.get_root ().get_source_from_file (current_file
);
140 if (current_src
== null)
143 var current_target
= current_src
.parent_type (Anjuta
.ProjectNodeType
.TARGET
);
144 if (current_target
== null)
147 current_target
.foreach (TraverseType
.PRE_ORDER
, (node
) => {
148 if (!(Anjuta
.ProjectNodeType
.SOURCE
in node
.get_node_type ()))
151 if (node
.get_file () == null)
154 var path
= node
.get_file ().get_path ();
158 if (path
.has_suffix (".vala") || path
.has_suffix (".vapi") || path
.has_suffix (".gs")) {
159 if (path
in current_sources
) {
160 debug ("file %s already added", path
);
162 context
.add_source_filename (path
);
163 current_sources
.add (path
);
164 debug ("file %s added", path
);
167 debug ("file %s skipped", path
);
171 if (!context
.has_package ("gobject-2.0")) {
172 context
.add_external_package("glib-2.0");
173 context
.add_external_package("gobject-2.0");
174 debug ("standard packages added");
176 debug ("standard packages already added");
180 unowned Anjuta
.ProjectProperty prop
= current_target
.get_property ("VALAFLAGS");
181 if (prop
!= null && prop
!= prop
.info
.default_value
) {
182 GLib
.Shell
.parse_argv (prop
.value
, out flags
);
184 /* Fall back to AM_VALAFLAGS */
185 var current_group
= current_target
.parent_type (Anjuta
.ProjectNodeType
.GROUP
);
186 prop
= current_group
.get_property ("VALAFLAGS");
187 if (prop
!= null && prop
!= prop
.info
.default_value
)
188 GLib
.Shell
.parse_argv (prop
.value
, out flags
);
191 string[] packages
= {};
192 string[] vapidirs
= {};
194 for (int i
= 0; i
< flags
.length
; i
++) {
195 if (flags
[i
] == "--vapidir")
196 vapidirs
+= flags
[++i
];
197 else if (flags
[i
].has_prefix ("--vapidir="))
198 vapidirs
+= flags
[i
].substring ("--vapidir=".length
);
199 else if (flags
[i
] == "--pkg")
200 packages
+= flags
[++i
];
201 else if (flags
[i
].has_prefix ("--pkg="))
202 packages
+= flags
[i
].substring ("--pkg=".length
);
204 debug ("Unknown valac flag %s", flags
[i
]);
207 var srcdir
= current_target
.parent_type (Anjuta
.ProjectNodeType
.GROUP
).get_file ().get_path ();
208 var top_srcdir
= project
.get_root ().get_file ().get_path ();
209 for (int i
= 0; i
< vapidirs
.length
; i
++) {
210 vapidirs
[i
] = vapidirs
[i
].replace ("$(srcdir)", srcdir
)
211 .replace ("$(top_srcdir)", top_srcdir
);
214 context
.vapi_directories
= vapidirs
;
215 foreach (var pkg
in packages
) {
216 if (context
.has_package (pkg
)) {
217 debug ("package %s skipped", pkg
);
218 } else if (context
.add_external_package(pkg
)) {
219 debug ("package %s added", pkg
);
221 debug ("package %s not found", pkg
);
224 Vala
.CodeContext
.pop();
227 public void on_project_loaded (IAnjuta
.ProjectManager pm
, Error? e
) {
230 add_project_files ();
232 pm
.disconnect (project_loaded_id
);
233 project_loaded_id
= 0;
236 /* "document_manager_current_document" watch */
237 public void editor_value_added (Anjuta
.Plugin plugin
, string name
, Value value
) {
238 debug("editor value added");
239 assert (current_editor
== null);
240 if (!(value
.get_object() is IAnjuta
.Editor
)) {
241 /* a glade document, for example, isn't an editor */
245 current_editor
= value
.get_object() as IAnjuta
.Editor
;
246 var current_file
= value
.get_object() as IAnjuta
.File
;
248 var pm
= (IAnjuta
.ProjectManager
) shell
.get_object("IAnjutaProjectManager");
249 var project
= pm
.get_current_project ();
251 if (!project
.is_loaded()) {
252 if (project_loaded_id
== 0)
253 project_loaded_id
= pm
.project_loaded
.connect (on_project_loaded
);
255 var cur_gfile
= current_file
.get_file ();
256 if (cur_gfile
== null) {
257 // File hasn't been saved yet
261 if (!(cur_gfile
.get_path () in current_sources
)) {
265 add_project_files ();
271 if (current_editor
!= null) {
272 if (current_editor is IAnjuta
.EditorAssist
)
273 (current_editor as IAnjuta
.EditorAssist
).add(provider
);
274 if (current_editor is IAnjuta
.EditorTip
)
275 current_editor
.char_added
.connect (on_char_added
);
276 if (current_editor is IAnjuta
.FileSavable
) {
277 var file_savable
= (IAnjuta
.FileSavable
) current_editor
;
278 file_savable
.saved
.connect (on_file_saved
);
280 if (current_editor is IAnjuta
.EditorGladeSignal
) {
281 var gladesig
= current_editor as IAnjuta
.EditorGladeSignal
;
282 gladesig
.drop_possible
.connect (on_drop_possible
);
283 gladesig
.drop
.connect (on_drop
);
285 current_editor
.glade_member_add
.connect (insert_member_decl_and_init
);
287 report
.update_errors (current_editor
);
289 public void editor_value_removed (Anjuta
.Plugin plugin
, string name
) {
290 debug("editor value removed");
291 if (current_editor is IAnjuta
.EditorAssist
)
292 (current_editor as IAnjuta
.EditorAssist
).remove(provider
);
293 if (current_editor is IAnjuta
.EditorTip
)
294 current_editor
.char_added
.disconnect (on_char_added
);
295 if (current_editor is IAnjuta
.FileSavable
) {
296 var file_savable
= (IAnjuta
.FileSavable
) current_editor
;
297 file_savable
.saved
.disconnect (on_file_saved
);
299 if (current_editor is IAnjuta
.EditorGladeSignal
) {
300 var gladesig
= current_editor as IAnjuta
.EditorGladeSignal
;
301 gladesig
.drop_possible
.disconnect (on_drop_possible
);
302 gladesig
.drop
.disconnect (on_drop
);
304 current_editor
.glade_member_add
.disconnect (insert_member_decl_and_init
);
305 current_editor
= null;
308 public void on_file_saved (IAnjuta
.FileSavable savable
, File file
) {
309 foreach (var source_file
in context
.get_source_files ()) {
310 if (source_file
.filename
!= file
.get_path())
315 file
.load_contents (null, out contents
, null);
316 source_file
.content
= (string) contents
;
317 update_file (source_file
);
325 public void on_char_added (IAnjuta
.Editor editor
, IAnjuta
.Iterable position
, char ch
) {
326 if (!settings
.get_boolean (ValaProvider
.PREF_CALLTIP_ENABLE
))
329 var editortip
= editor as IAnjuta
.EditorTip
;
331 provider
.show_call_tip (editortip
);
332 } else if (ch
== ')') {
337 /* tries to find the opening brace of the scope the current position before calling
338 * get_current_context since the source_reference of a class or namespace only
339 * contain the declaration not the entire "content" */
340 Vala
.Symbol?
get_scope (IAnjuta
.Editor editor
, IAnjuta
.Iterable position
) {
343 var current_char
= (position as IAnjuta
.EditorCell
).get_character ();
344 if (current_char
== "}") {
346 } else if (current_char
== "{") {
350 // a scope which contains the current position
352 position
.previous ();
353 current_char
= (position as IAnjuta
.EditorCell
).get_character ();
354 } while (! current_char
.get_char ().isalnum ());
355 return get_current_context (editor
, position
);
358 } while (position
.previous ());
362 public bool on_drop_possible (IAnjuta
.EditorGladeSignal editor
, IAnjuta
.Iterable position
) {
363 var line
= editor
.get_line_from_position (position
);
364 var column
= editor
.get_line_begin_position (line
).diff (position
);
365 debug ("line %d, column %d", line
, column
);
367 var scope
= get_scope (editor
, position
.clone ());
369 debug ("drag is inside %s", scope
.get_full_name ());
370 if (scope
== null || scope is Vala
.Namespace
|| scope is Vala
.Class
)
376 public void on_drop (IAnjuta
.EditorGladeSignal editor
, IAnjuta
.Iterable position
, string signal_data
) {
377 var data
= signal_data
.split (":");
378 var widget_name
= data
[0];
379 var signal_name
= data
[1].replace ("-", "_");
380 var handler_name
= data
[2];
381 var swapped
= (data
[4] == "1");
382 var scope
= get_scope (editor
, position
.clone ());
383 var builder
= new
StringBuilder ();
386 var handler_cname
= "";
388 var scope_prefix
= "";
390 scope_prefix
= Vala
.CCodeBaseModule
.get_ccode_lower_case_prefix (scope
);
391 if (handler_name
.has_prefix (scope_prefix
))
392 handler_name
= handler_name
.substring (scope_prefix
.length
);
394 var handler_cname
= scope_prefix
+ handler_name
;
397 if (data
[2] != handler_cname
&& !swapped
) {
398 builder
.append_printf ("[CCode (cname=\"%s\", instance_pos=-1)]\n", data
[2]);
399 } else if (data
[2] != handler_cname
) {
400 builder
.append_printf ("[CCode (cname=\"%s\")]\n", data
[2]);
401 } else if (!swapped
) {
402 builder
.append ("[CCode (instance_pos=-1)]\n");
405 var widget
= lookup_symbol_by_cname (widget_name
);
406 var sigs
= symbol_lookup_inherited (widget
, signal_name
, false);
407 if (sigs
== null || !(sigs
.data is Vala
.Signal
))
409 Vala
.Signal sig
= (Vala
.Signal
) sigs
.data
;
411 builder
.append_printf ("public void %s (", handler_name
);
414 builder
.append_printf ("%s sender", widget
.get_full_name ());
416 foreach (var param
in sig
.get_parameters ()) {
417 builder
.append_printf (", %s %s", param
.variable_type
.data_type
.get_full_name (), param
.name
);
420 foreach (var param
in sig
.get_parameters ()) {
421 builder
.append_printf ("%s %s, ", param
.variable_type
.data_type
.get_full_name (), param
.name
);
424 builder
.append_printf ("%s sender", widget
.get_full_name ());
427 builder
.append_printf (") {\n\n}\n");
429 editor
.insert (position
, builder
.str
, -1);
431 var indenter
= shell
.get_object ("IAnjutaIndenter") as IAnjuta
.Indenter
;
432 if (indenter
!= null) {
433 var end
= position
.clone ();
434 /* -1 so we don't count the last newline (as that would indent the line after) */
435 end
.set_position (end
.get_position () + builder
.str
.char_count () - 1);
436 indenter
.indent (position
, end
);
439 var inside
= editor
.get_line_end_position (editor
.get_line_from_position (position
) + 2);
440 editor
.goto_position (inside
);
441 if (indenter
!= null)
442 indenter
.indent (inside
, inside
);
445 const string DECL_MARK
= "/* ANJUTA: Widgets declaration for %s - DO NOT REMOVE */\n";
446 const string INIT_MARK
= "/* ANJUTA: Widgets initialization for %s - DO NOT REMOVE */\n";
448 void insert_member_decl_and_init (IAnjuta
.Editor editor
, string widget_ctype
, string widget_name
, string filename
) {
449 var widget_type
= lookup_symbol_by_cname (widget_ctype
).get_full_name ();
450 var basename
= Path
.get_basename (filename
);
452 string member_decl
= "%s %s;\n".printf (widget_type
, widget_name
);
453 string member_init
= "%s = builder.get_object(\"%s\") as %s;\n".printf (widget_name
, widget_name
, widget_type
);
455 insert_after_mark (editor
, DECL_MARK
.printf (basename
), member_decl
)
456 && insert_after_mark (editor
, INIT_MARK
.printf (basename
), member_init
);
459 bool insert_after_mark (IAnjuta
.Editor editor
, string mark
, string code_to_add
) {
460 var search_start
= editor
.get_start_position () as IAnjuta
.EditorCell
;
461 var search_end
= editor
.get_end_position () as IAnjuta
.EditorCell
;
463 IAnjuta
.EditorCell result_end
;
464 (editor as IAnjuta
.EditorSearch
).forward (mark
, false, search_start
, search_end
, null, out result_end
);
466 var mark_position
= result_end as IAnjuta
.Iterable
;
467 if (mark_position
== null)
470 editor
.insert (mark_position
, code_to_add
, -1);
472 var indenter
= shell
.get_object ("IAnjutaIndenter") as IAnjuta
.Indenter
;
473 if (indenter
!= null) {
474 var end
= mark_position
.clone ();
475 /* -1 so we don't count the last newline (as that would indent the line after) */
476 end
.set_position (end
.get_position () + code_to_add
.char_count () - 1);
477 indenter
.indent (mark_position
, end
);
480 /* Emit code-added signal, so symbols will be updated */
481 editor
.code_added (mark_position
, code_to_add
);
486 Vala
.Symbol?
lookup_symbol_by_cname (string cname
, Vala
.Symbol parent
=context
.root
) {
487 var sym
= parent
.scope
.lookup (cname
);
491 var symtab
= parent
.scope
.get_symbol_table ();
492 foreach (var name
in symtab
.get_keys ()) {
493 if (cname
.has_prefix (name
)) {
494 return lookup_symbol_by_cname (cname
.substring (name
.length
), parent
.scope
.lookup (name
));
500 internal Vala
.Symbol
get_current_context (IAnjuta
.Editor editor
, IAnjuta
.Iterable? position
=null) requires (editor is IAnjuta
.File
) {
501 var file
= editor as IAnjuta
.File
;
503 var path
= file
.get_file().get_path();
505 Vala
.SourceFile source
= null;
506 foreach (var src
in context
.get_source_files()) {
507 if (src
.filename
== path
) {
512 if (source
== null) {
513 source
= new Vala
.SourceFile (context
,
514 path
.has_suffix("vapi") ? Vala
.SourceFileType
.PACKAGE
:
515 Vala
.SourceFileType
.SOURCE
,
517 context
.add_source_file(source
);
520 int line
; int column
;
521 if (position
== null) {
522 line
= editor
.get_lineno ();
523 column
= editor
.get_column ();
525 line
= editor
.get_line_from_position (position
);
526 column
= editor
.get_line_begin_position (line
).diff (position
);
528 return locator
.locate(source
, line
, column
);
532 internal List
<Vala
.Symbol
> lookup_symbol (Vala
.Expression? inner
, string name
, bool prefix_match
,
534 var matching_symbols
= new List
<Vala
.Symbol
> ();
536 if (block
== null) return matching_symbols
;
540 for (var sym
= (Vala
.Symbol
) block
; sym
!= null; sym
= sym
.parent_symbol
) {
541 matching_symbols
.concat (symbol_lookup_inherited (sym
, name
, prefix_match
));
544 foreach (var ns
in block
.source_reference
.file
.current_using_directives
) {
545 matching_symbols
.concat (symbol_lookup_inherited (ns
.namespace_symbol
, name
, prefix_match
));
547 } else if (inner
.symbol_reference
!= null) {
548 matching_symbols
.concat (symbol_lookup_inherited (inner
.symbol_reference
, name
, prefix_match
));
549 } else if (inner is Vala
.MemberAccess
) {
550 var inner_ma
= (Vala
.MemberAccess
) inner
;
551 var matching
= lookup_symbol (inner_ma
.inner
, inner_ma
.member_name
, false, block
);
552 if (matching
!= null)
553 matching_symbols
.concat (symbol_lookup_inherited (matching
.data
, name
, prefix_match
));
554 } else if (inner is Vala
.MethodCall
) {
555 var inner_inv
= (Vala
.MethodCall
) inner
;
556 var inner_ma
= inner_inv
.call as Vala
.MemberAccess
;
557 if (inner_ma
!= null) {
558 var matching
= lookup_symbol (inner_ma
.inner
, inner_ma
.member_name
, false, block
);
559 if (matching
!= null)
560 matching_symbols
.concat (symbol_lookup_inherited (matching
.data
, name
, prefix_match
, true));
564 return matching_symbols
;
566 List
<Vala
.Symbol
> symbol_lookup_inherited (Vala
.Symbol? sym
, string name
, bool prefix_match
, bool invocation
= false) {
567 List
<Vala
.Symbol
> result
= null;
569 // This may happen if we cannot find all the needed packages
573 var symbol_table
= sym
.scope
.get_symbol_table ();
574 if (symbol_table
!= null) {
575 foreach (string key
in symbol_table
.get_keys()) {
576 if (((prefix_match
&& key
.has_prefix (name
)) || key
== name
)) {
577 result
.append (symbol_table
[key
]);
581 if (invocation
&& sym is Vala
.Method
) {
582 var func
= (Vala
.Method
) sym
;
583 result
.concat (symbol_lookup_inherited (func
.return_type
.data_type
, name
, prefix_match
));
584 } else if (sym is Vala
.Class
) {
585 var cl
= (Vala
.Class
) sym
;
586 foreach (var base_type
in cl
.get_base_types ()) {
587 result
.concat (symbol_lookup_inherited (base_type
.data_type
, name
, prefix_match
));
589 } else if (sym is Vala
.Struct
) {
590 var st
= (Vala
.Struct
) sym
;
591 result
.concat (symbol_lookup_inherited (st
.base_type
.data_type
, name
, prefix_match
));
592 } else if (sym is Vala
.Interface
) {
593 var iface
= (Vala
.Interface
) sym
;
594 foreach (var prerequisite
in iface
.get_prerequisites ()) {
595 result
.concat (symbol_lookup_inherited (prerequisite
.data_type
, name
, prefix_match
));
597 } else if (sym is Vala
.LocalVariable
) {
598 var variable
= (Vala
.LocalVariable
) sym
;
599 result
.concat (symbol_lookup_inherited (variable
.variable_type
.data_type
, name
, prefix_match
));
600 } else if (sym is Vala
.Field
) {
601 var field
= (Vala
.Field
) sym
;
602 result
.concat (symbol_lookup_inherited (field
.variable_type
.data_type
, name
, prefix_match
));
603 } else if (sym is Vala
.Property
) {
604 var prop
= (Vala
.Property
) sym
;
605 result
.concat (symbol_lookup_inherited (prop
.property_type
.data_type
, name
, prefix_match
));
606 } else if (sym is Vala
.Parameter
) {
607 var fp
= (Vala
.Parameter
) sym
;
608 result
.concat (symbol_lookup_inherited (fp
.variable_type
.data_type
, name
, prefix_match
));
613 void update_file (Vala
.SourceFile file
) {
615 /* Removing nodes in the same loop causes problems (probably due to ReadOnlyList)*/
616 var nodes
= new Vala
.ArrayList
<Vala
.CodeNode
> ();
617 foreach (var node
in file
.get_nodes()) {
620 foreach (var node
in nodes
) {
621 file
.remove_node (node
);
622 if (node is Vala
.Symbol
) {
623 var sym
= (Vala
.Symbol
) node
;
624 if (sym
.owner
!= null)
625 /* we need to remove it from the scope*/
626 sym
.owner
.remove(sym
.name
);
627 if (context
.entry_point
== sym
)
628 context
.entry_point
= null;
631 file
.current_using_directives
= new Vala
.ArrayList
<Vala
.UsingDirective
>();
632 var ns_ref
= new Vala
.UsingDirective (new Vala
.UnresolvedSymbol (null, "GLib"));
633 file
.add_using_directive (ns_ref
);
634 context
.root
.add_using_directive (ns_ref
);
636 report
.clear_error_indicators (file
);
640 report
.update_errors(current_editor
);
644 private void on_autocompletion_toggled (ToggleButton button
) {
645 var sensitive
= button
.get_active();
646 Gtk
.Widget widget
= bxml
.get_object (PREF_WIDGET_SPACE
) as Widget
;
647 widget
.set_sensitive (sensitive
);
648 widget
= bxml
.get_object (PREF_WIDGET_BRACE
) as Widget
;
649 widget
.set_sensitive (sensitive
);
652 public void merge (Anjuta
.Preferences prefs
) throws GLib
.Error
{
653 bxml
= new
Builder();
655 /* Add preferences */
657 bxml
.add_from_file (PREFS_BUILDER
);
658 } catch (Error err
) {
659 warning ("Couldn't load builder file: %s", err
.message
);
661 prefs
.add_from_builder (bxml
, settings
, "preferences", _("Auto-complete"),
663 var toggle
= bxml
.get_object (PREF_WIDGET_AUTO
) as ToggleButton
;
664 toggle
.toggled
.connect (on_autocompletion_toggled
);
665 on_autocompletion_toggled (toggle
);
668 public void unmerge (Anjuta
.Preferences prefs
) throws GLib
.Error
{
669 prefs
.remove_page (_("Auto-complete"));
674 public Type
anjuta_glue_register_components (TypeModule module
) {
675 return typeof (ValaPlugin
);