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 ();
385 var scope_prefix
= "";
387 scope_prefix
= Vala
.CCodeBaseModule
.get_ccode_lower_case_prefix (scope
);
388 if (handler_name
.has_prefix (scope_prefix
))
389 handler_name
= handler_name
.substring (scope_prefix
.length
);
391 var handler_cname
= scope_prefix
+ handler_name
;
393 if (data
[2] != handler_cname
&& !swapped
) {
394 builder
.append_printf ("[CCode (cname=\"%s\", instance_pos=-1)]\n", data
[2]);
395 } else if (data
[2] != handler_cname
) {
396 builder
.append_printf ("[CCode (cname=\"%s\")]\n", data
[2]);
397 } else if (!swapped
) {
398 builder
.append ("[CCode (instance_pos=-1)]\n");
401 var widget
= lookup_symbol_by_cname (widget_name
);
402 var sigs
= symbol_lookup_inherited (widget
, signal_name
, false);
403 if (sigs
== null || !(sigs
.data is Vala
.Signal
))
405 Vala
.Signal sig
= (Vala
.Signal
) sigs
.data
;
407 builder
.append_printf ("public void %s (", handler_name
);
410 builder
.append_printf ("%s sender", widget
.get_full_name ());
412 foreach (var param
in sig
.get_parameters ()) {
413 builder
.append_printf (", %s %s", param
.variable_type
.data_type
.get_full_name (), param
.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 builder
.append_printf ("%s sender", widget
.get_full_name ());
423 builder
.append_printf (") {\n\n}\n");
425 editor
.insert (position
, builder
.str
, -1);
427 var indenter
= shell
.get_object ("IAnjutaIndenter") as IAnjuta
.Indenter
;
428 if (indenter
!= null) {
429 var end
= position
.clone ();
430 /* -1 so we don't count the last newline (as that would indent the line after) */
431 end
.set_position (end
.get_position () + builder
.str
.char_count () - 1);
432 indenter
.indent (position
, end
);
435 var inside
= editor
.get_line_end_position (editor
.get_line_from_position (position
) + 2);
436 editor
.goto_position (inside
);
437 if (indenter
!= null)
438 indenter
.indent (inside
, inside
);
441 const string DECL_MARK
= "/* ANJUTA: Widgets declaration for %s - DO NOT REMOVE */\n";
442 const string INIT_MARK
= "/* ANJUTA: Widgets initialization for %s - DO NOT REMOVE */\n";
444 void insert_member_decl_and_init (IAnjuta
.Editor editor
, string widget_ctype
, string widget_name
, string filename
) {
445 var widget_type
= lookup_symbol_by_cname (widget_ctype
).get_full_name ();
446 var basename
= Path
.get_basename (filename
);
448 string member_decl
= "%s %s;\n".printf (widget_type
, widget_name
);
449 string member_init
= "%s = builder.get_object(\"%s\") as %s;\n".printf (widget_name
, widget_name
, widget_type
);
451 insert_after_mark (editor
, DECL_MARK
.printf (basename
), member_decl
)
452 && insert_after_mark (editor
, INIT_MARK
.printf (basename
), member_init
);
455 bool insert_after_mark (IAnjuta
.Editor editor
, string mark
, string code_to_add
) {
456 var search_start
= editor
.get_start_position () as IAnjuta
.EditorCell
;
457 var search_end
= editor
.get_end_position () as IAnjuta
.EditorCell
;
459 IAnjuta
.EditorCell result_end
;
460 (editor as IAnjuta
.EditorSearch
).forward (mark
, false, search_start
, search_end
, null, out result_end
);
462 var mark_position
= result_end as IAnjuta
.Iterable
;
463 if (mark_position
== null)
466 editor
.insert (mark_position
, code_to_add
, -1);
468 var indenter
= shell
.get_object ("IAnjutaIndenter") as IAnjuta
.Indenter
;
469 if (indenter
!= null) {
470 var end
= mark_position
.clone ();
471 /* -1 so we don't count the last newline (as that would indent the line after) */
472 end
.set_position (end
.get_position () + code_to_add
.char_count () - 1);
473 indenter
.indent (mark_position
, end
);
476 /* Emit code-added signal, so symbols will be updated */
477 editor
.code_added (mark_position
, code_to_add
);
482 Vala
.Symbol?
lookup_symbol_by_cname (string cname
, Vala
.Symbol parent
=context
.root
) {
483 var sym
= parent
.scope
.lookup (cname
);
487 var symtab
= parent
.scope
.get_symbol_table ();
488 foreach (var name
in symtab
.get_keys ()) {
489 if (cname
.has_prefix (name
)) {
490 return lookup_symbol_by_cname (cname
.substring (name
.length
), parent
.scope
.lookup (name
));
496 internal Vala
.Symbol
get_current_context (IAnjuta
.Editor editor
, IAnjuta
.Iterable? position
=null) requires (editor is IAnjuta
.File
) {
497 var file
= editor as IAnjuta
.File
;
499 var path
= file
.get_file().get_path();
501 Vala
.SourceFile source
= null;
502 foreach (var src
in context
.get_source_files()) {
503 if (src
.filename
== path
) {
508 if (source
== null) {
509 source
= new Vala
.SourceFile (context
,
510 path
.has_suffix("vapi") ? Vala
.SourceFileType
.PACKAGE
:
511 Vala
.SourceFileType
.SOURCE
,
513 context
.add_source_file(source
);
516 int line
; int column
;
517 if (position
== null) {
518 line
= editor
.get_lineno ();
519 column
= editor
.get_column ();
521 line
= editor
.get_line_from_position (position
);
522 column
= editor
.get_line_begin_position (line
).diff (position
);
524 return locator
.locate(source
, line
, column
);
528 internal List
<Vala
.Symbol
> lookup_symbol (Vala
.Expression? inner
, string name
, bool prefix_match
,
530 List
<Vala
.Symbol
> matching_symbols
= null;
534 for (var sym
= (Vala
.Symbol
) block
; sym
!= null; sym
= sym
.parent_symbol
) {
535 matching_symbols
.concat (symbol_lookup_inherited (sym
, name
, prefix_match
));
538 foreach (var ns
in block
.source_reference
.file
.current_using_directives
) {
539 matching_symbols
.concat (symbol_lookup_inherited (ns
.namespace_symbol
, name
, prefix_match
));
541 } else if (inner
.symbol_reference
!= null) {
542 matching_symbols
.concat (symbol_lookup_inherited (inner
.symbol_reference
, name
, prefix_match
));
543 } else if (inner is Vala
.MemberAccess
) {
544 var inner_ma
= (Vala
.MemberAccess
) inner
;
545 var matching
= lookup_symbol (inner_ma
.inner
, inner_ma
.member_name
, false, block
);
546 if (matching
!= null)
547 matching_symbols
.concat (symbol_lookup_inherited (matching
.data
, name
, prefix_match
));
548 } else if (inner is Vala
.MethodCall
) {
549 var inner_inv
= (Vala
.MethodCall
) inner
;
550 var inner_ma
= inner_inv
.call as Vala
.MemberAccess
;
551 if (inner_ma
!= null) {
552 var matching
= lookup_symbol (inner_ma
.inner
, inner_ma
.member_name
, false, block
);
553 if (matching
!= null)
554 matching_symbols
.concat (symbol_lookup_inherited (matching
.data
, name
, prefix_match
, true));
558 return matching_symbols
;
560 List
<Vala
.Symbol
> symbol_lookup_inherited (Vala
.Symbol? sym
, string name
, bool prefix_match
, bool invocation
= false) {
561 List
<Vala
.Symbol
> result
= null;
563 // This may happen if we cannot find all the needed packages
567 var symbol_table
= sym
.scope
.get_symbol_table ();
568 if (symbol_table
!= null) {
569 foreach (string key
in symbol_table
.get_keys()) {
570 if (((prefix_match
&& key
.has_prefix (name
)) || key
== name
)) {
571 result
.append (symbol_table
[key
]);
575 if (invocation
&& sym is Vala
.Method
) {
576 var func
= (Vala
.Method
) sym
;
577 result
.concat (symbol_lookup_inherited (func
.return_type
.data_type
, name
, prefix_match
));
578 } else if (sym is Vala
.Class
) {
579 var cl
= (Vala
.Class
) sym
;
580 foreach (var base_type
in cl
.get_base_types ()) {
581 result
.concat (symbol_lookup_inherited (base_type
.data_type
, name
, prefix_match
));
583 } else if (sym is Vala
.Struct
) {
584 var st
= (Vala
.Struct
) sym
;
585 result
.concat (symbol_lookup_inherited (st
.base_type
.data_type
, name
, prefix_match
));
586 } else if (sym is Vala
.Interface
) {
587 var iface
= (Vala
.Interface
) sym
;
588 foreach (var prerequisite
in iface
.get_prerequisites ()) {
589 result
.concat (symbol_lookup_inherited (prerequisite
.data_type
, name
, prefix_match
));
591 } else if (sym is Vala
.LocalVariable
) {
592 var variable
= (Vala
.LocalVariable
) sym
;
593 result
.concat (symbol_lookup_inherited (variable
.variable_type
.data_type
, name
, prefix_match
));
594 } else if (sym is Vala
.Field
) {
595 var field
= (Vala
.Field
) sym
;
596 result
.concat (symbol_lookup_inherited (field
.variable_type
.data_type
, name
, prefix_match
));
597 } else if (sym is Vala
.Property
) {
598 var prop
= (Vala
.Property
) sym
;
599 result
.concat (symbol_lookup_inherited (prop
.property_type
.data_type
, name
, prefix_match
));
600 } else if (sym is Vala
.Parameter
) {
601 var fp
= (Vala
.Parameter
) sym
;
602 result
.concat (symbol_lookup_inherited (fp
.variable_type
.data_type
, name
, prefix_match
));
607 void update_file (Vala
.SourceFile file
) {
609 /* Removing nodes in the same loop causes problems (probably due to ReadOnlyList)*/
610 var nodes
= new Vala
.ArrayList
<Vala
.CodeNode
> ();
611 foreach (var node
in file
.get_nodes()) {
614 foreach (var node
in nodes
) {
615 file
.remove_node (node
);
616 if (node is Vala
.Symbol
) {
617 var sym
= (Vala
.Symbol
) node
;
618 if (sym
.owner
!= null)
619 /* we need to remove it from the scope*/
620 sym
.owner
.remove(sym
.name
);
621 if (context
.entry_point
== sym
)
622 context
.entry_point
= null;
625 file
.current_using_directives
= new Vala
.ArrayList
<Vala
.UsingDirective
>();
626 var ns_ref
= new Vala
.UsingDirective (new Vala
.UnresolvedSymbol (null, "GLib"));
627 file
.add_using_directive (ns_ref
);
628 context
.root
.add_using_directive (ns_ref
);
630 report
.clear_error_indicators (file
);
634 report
.update_errors(current_editor
);
638 private void on_autocompletion_toggled (ToggleButton button
) {
639 var sensitive
= button
.get_active();
640 Gtk
.Widget widget
= bxml
.get_object (PREF_WIDGET_SPACE
) as Widget
;
641 widget
.set_sensitive (sensitive
);
642 widget
= bxml
.get_object (PREF_WIDGET_BRACE
) as Widget
;
643 widget
.set_sensitive (sensitive
);
646 public void merge (Anjuta
.Preferences prefs
) throws GLib
.Error
{
647 bxml
= new
Builder();
649 /* Add preferences */
651 bxml
.add_from_file (PREFS_BUILDER
);
652 } catch (Error err
) {
653 warning ("Couldn't load builder file: %s", err
.message
);
655 prefs
.add_from_builder (bxml
, settings
, "preferences", _("Auto-complete"),
657 var toggle
= bxml
.get_object (PREF_WIDGET_AUTO
) as ToggleButton
;
658 toggle
.toggled
.connect (on_autocompletion_toggled
);
659 on_autocompletion_toggled (toggle
);
662 public void unmerge (Anjuta
.Preferences prefs
) throws GLib
.Error
{
663 prefs
.remove_page (_("Auto-complete"));
668 public Type
anjuta_glue_register_components (TypeModule module
) {
669 return typeof (ValaPlugin
);