1 /*******************************************************************************
2  * Copyright (c) 2010 - 2013 by Timotei Dolean <timotei21@gmail.com>
3  *
4  * This program and the accompanying materials are made available
5  * under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *******************************************************************************/
9 package org.wesnoth.ui.contentassist;
10 
11 import com.google.common.base.Function;
12 import com.google.common.base.Predicates;
13 import com.google.common.collect.Collections2;
14 
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.List;
18 import java.util.Map.Entry;
19 
20 import org.eclipse.core.resources.IFile;
21 import org.eclipse.emf.ecore.EObject;
22 import org.eclipse.jface.text.contentassist.ICompletionProposal;
23 import org.eclipse.jface.viewers.ILabelProvider;
24 import org.eclipse.jface.viewers.StyledString;
25 import org.eclipse.swt.graphics.Image;
26 import org.eclipse.xtext.Assignment;
27 import org.eclipse.xtext.RuleCall;
28 import org.eclipse.xtext.nodemodel.ILeafNode;
29 import org.eclipse.xtext.nodemodel.INode;
30 import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
31 import org.eclipse.xtext.ui.editor.contentassist.ContentAssistContext;
32 import org.eclipse.xtext.ui.editor.contentassist.ICompletionProposalAcceptor;
33 
34 import org.wesnoth.installs.WesnothInstallsUtils;
35 import org.wesnoth.preprocessor.Define;
36 import org.wesnoth.projects.ProjectCache;
37 import org.wesnoth.projects.ProjectUtils;
38 import org.wesnoth.schema.SchemaParser;
39 import org.wesnoth.templates.TemplateProvider;
40 import org.wesnoth.ui.editor.WMLEditor;
41 import org.wesnoth.utils.ResourceUtils;
42 import org.wesnoth.utils.StringUtils;
43 import org.wesnoth.utils.WMLUtils;
44 import org.wesnoth.wml.WMLConfig;
45 import org.wesnoth.wml.WMLKey;
46 import org.wesnoth.wml.WMLKeyValue;
47 import org.wesnoth.wml.WMLTag;
48 import org.wesnoth.wml.WMLVariable;
49 import org.wesnoth.wml.WMLVariable.Scope;
50 
51 /**
52  * Proposal provider for WML content assist
53  */
54 public class WMLProposalProvider extends AbstractWMLProposalProvider
55 {
56     protected SchemaParser     schemaParser_;
57     protected ProjectCache     projectCache_;
58     protected int              dependencyIndex_;
59 
60     protected static final int KEY_VALUE_PRIORITY   = 1700;
61     protected static final int KEY_NAME_PRIORITY    = 1500;
62     protected static final int TAG_PRIORITY         = 1000;
63     protected static final int MACRO_CALL_PRIORITY  = 100;
64 
65     private static boolean     IMAGES_INITED        = false;
66     private static Image       MACRO_CALL_IMAGE     = null;
67     private static Image       SCENARIO_VALUE_IMAGE = null;
68     private static Image       WML_KEY_IMAGE        = null;
69     private static Image       WML_TAG_IMAGE        = null;
70 
71     /**
72      * For priorities, see: {@link #KEY_NAME_PRIORITY}
73      * {@link #KEY_VALUE_PRIORITY} {@link #TAG_PRIORITY}
74      * {@link #MACRO_CALL_PRIORITY}
75      */
WMLProposalProvider( )76     public WMLProposalProvider( )
77     {
78         super( );
79     }
80 
initImages( )81     private void initImages( )
82     {
83         if( IMAGES_INITED ) {
84             return;
85         }
86 
87         ILabelProvider labelProvider = getLabelProvider( );
88 
89         MACRO_CALL_IMAGE = labelProvider.getImage( "macrocall.png" );
90         SCENARIO_VALUE_IMAGE = labelProvider.getImage( "scenario.png" );
91         WML_KEY_IMAGE = labelProvider.getImage( "wmlkey.png" );
92         WML_TAG_IMAGE = labelProvider.getImage( "wmltag.png" );
93     }
94 
95     /**
96      * Initializes the proposal provider with all needed values
97      */
initProposalProvider( )98     private void initProposalProvider( )
99     {
100         if( projectCache_ != null ) {
101             return;
102         }
103 
104         IFile file = WMLEditor.getActiveEditorFile( );
105         projectCache_ = ProjectUtils.getCacheForProject( file.getProject( ) );
106 
107         // load the schema so we know what to suggest for autocomplete
108         schemaParser_ = SchemaParser.getInstance( WesnothInstallsUtils
109             .getInstallNameForResource( file ) );
110 
111         dependencyIndex_ = ResourceUtils.getDependencyIndex( file );
112 
113         initImages( );
114     }
115 
116     @Override
createProposals( ContentAssistContext context, ICompletionProposalAcceptor acceptor )117     public void createProposals( ContentAssistContext context,
118         ICompletionProposalAcceptor acceptor )
119     {
120         initProposalProvider( );
121 
122         super.createProposals( context, acceptor );
123     }
124 
125     @Override
completeWMLKey_Name( EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor )126     public void completeWMLKey_Name( EObject model, Assignment assignment,
127         ContentAssistContext context, ICompletionProposalAcceptor acceptor )
128     {
129         super.completeWMLKey_Name( model, assignment, context, acceptor );
130 
131         if( model != null ) {
132             addKeyNameProposals( model, context, acceptor );
133         }
134     }
135 
136     @Override
complete_WMLKeyValue( EObject model, RuleCall ruleCall, ContentAssistContext context, ICompletionProposalAcceptor acceptor )137     public void complete_WMLKeyValue( EObject model, RuleCall ruleCall,
138         ContentAssistContext context, ICompletionProposalAcceptor acceptor )
139     {
140         super.complete_WMLKeyValue( model, ruleCall, context, acceptor );
141 
142         if( model != null ) {
143             addKeyValueProposals( model, context, acceptor );
144         }
145     }
146 
147     @Override
complete_WMLTag( EObject model, RuleCall ruleCall, ContentAssistContext context, ICompletionProposalAcceptor acceptor )148     public void complete_WMLTag( EObject model, RuleCall ruleCall,
149         ContentAssistContext context, ICompletionProposalAcceptor acceptor )
150     {
151         super.complete_WMLTag( model, ruleCall, context, acceptor );
152 
153         if( model != null ) {
154             addTagProposals( model, true, context, acceptor );
155         }
156     }
157 
158     @Override
completeWMLTag_Name( EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor )159     public void completeWMLTag_Name( EObject model, Assignment assignment,
160         ContentAssistContext context, ICompletionProposalAcceptor acceptor )
161     {
162         super.completeWMLTag_Name( model, assignment, context, acceptor );
163 
164         if( model != null ) {
165             addTagProposals( model, false, context, acceptor );
166         }
167     }
168 
169     @Override
completeWMLMacroCall_Name( EObject model, Assignment assignment, ContentAssistContext context, ICompletionProposalAcceptor acceptor )170     public void completeWMLMacroCall_Name( EObject model,
171         Assignment assignment, ContentAssistContext context,
172         ICompletionProposalAcceptor acceptor )
173     {
174         super.completeWMLMacroCall_Name( model, assignment, context, acceptor );
175 
176         if( model != null ) {
177             addMacroCallProposals( model, false, context, acceptor );
178         }
179     }
180 
181     @Override
complete_WMLMacroCall( EObject model, RuleCall ruleCall, ContentAssistContext context, ICompletionProposalAcceptor acceptor )182     public void complete_WMLMacroCall( EObject model, RuleCall ruleCall,
183         ContentAssistContext context, ICompletionProposalAcceptor acceptor )
184     {
185         super.complete_WMLMacroCall( model, ruleCall, context, acceptor );
186 
187         if( model != null ) {
188             addMacroCallProposals( model, true, context, acceptor );
189         }
190     }
191 
addMacroCallProposals( EObject model, boolean ruleProposal, ContentAssistContext context, ICompletionProposalAcceptor acceptor )192     private void addMacroCallProposals( EObject model, boolean ruleProposal,
193         ContentAssistContext context, ICompletionProposalAcceptor acceptor )
194     {
195         INode currentNode = context.getCurrentNode( );
196 
197         boolean appendEndBrace = currentNode != null && ! currentNode.getText( ).equals( "}" );
198 
199 
200         for( Entry< String, Define > define: projectCache_.getDefines( )
201             .entrySet( ) ) {
202             StringBuilder proposal = new StringBuilder( 10 );
203             if( ruleProposal == true ) {
204                 proposal.append( "{" ); //$NON-NLS-1$
205             }
206             proposal.append( define.getKey( ) );
207 
208             for( String arg: define.getValue( ).getArguments( ) ) {
209                 proposal.append( " " + arg ); //$NON-NLS-1$
210             }
211 
212             if( appendEndBrace ) {
213                 proposal.append( "}" ); //$NON-NLS-1$
214             }
215 
216             acceptor.accept( createCompletionProposal( proposal.toString( ),
217                 define.getKey( ), MACRO_CALL_IMAGE, context,
218                 MACRO_CALL_PRIORITY ) );
219         }
220     }
221 
addKeyValueProposals( EObject model, ContentAssistContext context, ICompletionProposalAcceptor acceptor )222     private void addKeyValueProposals( EObject model,
223         ContentAssistContext context, ICompletionProposalAcceptor acceptor )
224     {
225         if( model == null || ! ( model instanceof WMLKey ) ) {
226             return;
227         }
228         WMLKey key = ( WMLKey ) model;
229         String keyName = key.getName( );
230 
231         // handle the next_scenario and first_scenario
232         if( keyName.equals( "next_scenario" ) || //$NON-NLS-1$
233             keyName.equals( "first_scenario" ) ) //$NON-NLS-1$
234         {
235             for( WMLConfig config: projectCache_.getWMLConfigs( ).values( ) ) {
236                 if( StringUtils.isNullOrEmpty( config.ScenarioId ) ) {
237                     continue;
238                 }
239                 acceptor.accept( createCompletionProposal( config.ScenarioId,
240                     config.ScenarioId, SCENARIO_VALUE_IMAGE, context,
241                     KEY_VALUE_PRIORITY ) );
242             }
243         }
244         else if( model.eContainer( ) != null
245             && model.eContainer( ) instanceof WMLTag ) {
246             WMLTag parent = ( WMLTag ) model.eContainer( );
247             String tagName = parent.getName( );
248             WMLTag tag = schemaParser_.getTags( ).get( tagName );
249             if( tag != null ) {
250                 WMLKey tagKey = WMLUtils.getKeyByName( tag, keyName );
251                 if( tagKey != null && tagKey.is_Enum( ) ) {
252                     for( WMLKeyValue val: tagKey.getValues( ) ) {
253                         acceptor.accept( createCompletionProposal(
254                             val.toString( ), context, KEY_VALUE_PRIORITY ) );
255                     }
256                 }
257             }
258 
259             if( ( tagName.equals( "event" ) || tagName.equals( "fire_event" ) )
260                 && keyName.equals( "name" ) ) {
261                 // add events
262                 List< String > events = new ArrayList< String >( );
263                 events.addAll( TemplateProvider.getInstance( )
264                     .getCAC( "events" ) );
265                 events.addAll( projectCache_.getEvents( ) );
266 
267                 for( String event: events ) {
268                     acceptor
269                         .accept( createCompletionProposal( event, context ) );
270                 }
271             }
272             else {
273                 final int nodeOffset = NodeModelUtils.getNode( model )
274                     .getTotalOffset( );
275                 List< String > variables = new ArrayList< String >( );
276 
277                 // add CAC variables
278                 variables.addAll( TemplateProvider.getInstance( ).getCAC(
279                     "variables" ) );
280 
281                 // filter variables by index
282                 Collection< String > projectVariables = Collections2.transform(
283                     projectCache_.getVariables( ).values( ),
284                     new Function< WMLVariable, String >( ) {
285 
286                         @Override
287                         public String apply( WMLVariable from )
288                         {
289                             for( Scope scope: from.getScopes( ) ) {
290                                 if( scope.contains( dependencyIndex_,
291                                     nodeOffset ) ) {
292                                     return from.getName( );
293                                 }
294                             }
295 
296                             return null;
297                         }
298                     } );
299 
300                 variables.addAll( Collections2.filter( projectVariables,
301                     Predicates.notNull( ) ) );
302 
303                 for( String variable: variables ) {
304                     acceptor.accept( createCompletionProposal( "$" + variable,
305                         context ) );
306                 }
307             }
308         }
309     }
310 
addKeyNameProposals( EObject model, ContentAssistContext context, ICompletionProposalAcceptor acceptor )311     private void addKeyNameProposals( EObject model,
312         ContentAssistContext context, ICompletionProposalAcceptor acceptor )
313     {
314         WMLTag tag = null;
315         if( model instanceof WMLTag ) {
316             tag = ( WMLTag ) model;
317         }
318         else if( model.eContainer( ) instanceof WMLTag ) {
319             tag = ( WMLTag ) model.eContainer( );
320         }
321 
322         if( tag != null ) {
323             WMLTag schemaTag = schemaParser_.getTags( ).get( tag.getName( ) );
324             // try getting the custom ones
325             if( schemaTag == null ) {
326                 schemaTag = projectCache_.getWMLTags( ).get( tag.getName( ) );
327             }
328 
329             if( schemaTag != null ) {
330                 for( WMLKey key: schemaTag.getWMLKeys( ) ) {
331                     // skip forbidden keys
332                     if( key.is_Forbidden( ) ) {
333                         continue;
334                     }
335 
336                     boolean toAdd = true;
337                     // check only non-repeatable keys
338                     if( ! key.is_Repeatable( ) ) {
339                         // don't suggest already completed keys
340                         toAdd = ( WMLUtils.getKeyByName( tag, key.getName( ) ) != null );
341                     }
342 
343                     if( toAdd ) {
344                         acceptor.accept( createCompletionProposal(
345                             key.getName( ) + "=", //$NON-NLS-1$
346                             key.getName( ), WML_KEY_IMAGE, context,
347                             KEY_NAME_PRIORITY ) );
348                     }
349                 }
350             }
351         }
352     }
353 
addTagProposals( EObject model, boolean ruleProposal, ContentAssistContext context, ICompletionProposalAcceptor acceptor )354     private void addTagProposals( EObject model, boolean ruleProposal,
355         ContentAssistContext context, ICompletionProposalAcceptor acceptor )
356     {
357         WMLTag parentTag = null;
358         if( model instanceof WMLTag ) {
359             parentTag = ( WMLTag ) model;
360         }
361         else if( model.eContainer( ) instanceof WMLTag ) {
362             parentTag = ( WMLTag ) model.eContainer( );
363         }
364         INode currentNode = context.getCurrentNode( );
365 
366         if( currentNode == null ) {
367             return;
368         }
369 
370         boolean appendEndBracket = ! currentNode.getText( ).equals( "]" );
371 
372         WMLTag sourceTag = null;
373         String indent = "";
374         // find the just previous leaf node (which contains the indent)
375         ILeafNode leafNode = NodeModelUtils.findLeafNodeAtOffset(
376             NodeModelUtils.getNode( model ),
377             currentNode.getTotalOffset( ) - 1 - context.getPrefix( ).length( ) );
378 
379         if( leafNode != null ) {
380             indent = leafNode.getText( );
381         }
382 
383         if( parentTag != null ) {
384             sourceTag = schemaParser_.getTags( ).get( parentTag.getName( ) );
385         }
386         else // we are at the root
387         {
388             sourceTag = schemaParser_.getTags( ).get( "root" ); //$NON-NLS-1$
389         }
390 
391         // remove new lines from the indent
392         indent = indent.replaceAll( "\\r|\\n", "" ); //$NON-NLS-1$ //$NON-NLS-2$
393 
394         if( sourceTag != null ) {
395             for( WMLTag tag: sourceTag.getWMLTags( ) ) {
396                 // skip forbidden tags
397                 if( tag.is_Forbidden( ) ) {
398                     continue;
399                 }
400 
401                 // check that non-repeatable tags aren't suggested again
402                 if( ! tag.is_Repeatable( ) && parentTag != null ) {
403                     if( WMLUtils.getTagByName( parentTag, tag.getName( ) ) != null ) {
404                         return;
405                     }
406                 }
407 
408                 acceptor.accept( createTagProposal( tag, indent, ruleProposal,
409                     context, appendEndBracket ) );
410             }
411         }
412 
413         // parsed custom tags
414         for( WMLTag tag: projectCache_.getWMLTags( ).values( ) ) {
415             acceptor.accept( createTagProposal( tag, "", ruleProposal, context, //$NON-NLS-1$
416                 appendEndBracket ) );
417         }
418     }
419 
420     /**
421      * Returns the proposal for the specified tag, usign the specified indent
422      *
423      * @param tag
424      *        The tag from which to construct the proposal
425      * @param indent
426      *        The indent used to indent the tag and subsequent keys
427      * @param ruleProposal
428      *        Whether this is a proposal for an entire rule or not
429      * @param context
430      * @param appendEndBracked
431      * @return
432      */
createTagProposal( WMLTag tag, String indent, boolean ruleProposal, ContentAssistContext context, boolean appendEndBracked )433     private ICompletionProposal createTagProposal( WMLTag tag, String indent,
434         boolean ruleProposal, ContentAssistContext context,
435         boolean appendEndBracked )
436     {
437         StringBuilder proposal = new StringBuilder( );
438         if( ruleProposal ) {
439             proposal.append( "[" ); //$NON-NLS-1$
440         }
441         proposal.append( tag.getName( ) );
442         proposal.append( "]\n" ); //$NON-NLS-1$
443         for( WMLKey key: tag.getWMLKeys( ) ) {
444             if( key.is_Required( ) ) {
445                 proposal.append( String.format( "\t%s%s=\n", //$NON-NLS-1$
446                     indent, key.getName( ) ) );
447             }
448         }
449         proposal.append( String.format( "%s[/%s%s",//$NON-NLS-1$
450             indent,
451             tag.getName( ),
452             appendEndBracked ? "]": "" ) );
453 
454         return createCompletionProposal( proposal.toString( ), tag.getName( ),
455             WML_TAG_IMAGE, context, TAG_PRIORITY );
456     }
457 
createCompletionProposal( String proposal, ContentAssistContext context, int priority )458     private ICompletionProposal createCompletionProposal( String proposal,
459         ContentAssistContext context, int priority )
460     {
461         return createCompletionProposal( proposal, null, null, priority,
462             context.getPrefix( ), context );
463     }
464 
createCompletionProposal( String proposal, String displayString, Image image, ContentAssistContext contentAssistContext, int priority )465     private ICompletionProposal createCompletionProposal( String proposal,
466         String displayString, Image image,
467         ContentAssistContext contentAssistContext, int priority )
468     {
469         return createCompletionProposal( proposal, new StyledString(
470             displayString ), image, priority,
471             contentAssistContext.getPrefix( ), contentAssistContext );
472     }
473 }
474