fix #774 infinite loop in net.sourceforge.phpeclipse.builder.IdentifierIndexManager...
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / ui / text / template / contentassist / TemplateProposal.java
1 /*******************************************************************************
2  * Copyright (c) 2000, 2004 IBM Corporation and others.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Common Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/cpl-v10.html
7  *
8  * Contributors:
9  *     IBM Corporation - initial API and implementation
10  *******************************************************************************/
11 package net.sourceforge.phpdt.internal.ui.text.template.contentassist;
12
13 import net.sourceforge.phpdt.internal.corext.template.php.CompilationUnitContext;
14 import net.sourceforge.phpdt.internal.corext.template.php.JavaContext;
15 import net.sourceforge.phpdt.internal.ui.text.java.IPHPCompletionProposal;
16 import net.sourceforge.phpdt.internal.ui.util.ExceptionHandler;
17 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
18 import net.sourceforge.phpeclipse.phpeditor.EditorHighlightingSynchronizer;
19 import net.sourceforge.phpeclipse.phpeditor.PHPEditor;
20
21 import org.eclipse.core.runtime.CoreException;
22 import org.eclipse.core.runtime.IStatus;
23 import org.eclipse.core.runtime.Status;
24 import org.eclipse.jface.dialogs.MessageDialog;
25 import org.eclipse.jface.text.Assert;
26 import org.eclipse.jface.text.BadLocationException;
27 import org.eclipse.jface.text.BadPositionCategoryException;
28 import org.eclipse.jface.text.DocumentEvent;
29 import org.eclipse.jface.text.IDocument;
30 import org.eclipse.jface.text.IInformationControlCreator;
31 import org.eclipse.jface.text.IRegion;
32 import org.eclipse.jface.text.ITextViewer;
33 import org.eclipse.jface.text.Position;
34 import org.eclipse.jface.text.Region;
35 import org.eclipse.jface.text.contentassist.ICompletionProposal;
36 import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2;
37 import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3;
38 import org.eclipse.jface.text.contentassist.IContextInformation;
39 import org.eclipse.jface.text.link.ILinkedModeListener;
40 import org.eclipse.jface.text.link.LinkedModeModel;
41 import org.eclipse.jface.text.link.LinkedModeUI;
42 import org.eclipse.jface.text.link.LinkedPosition;
43 import org.eclipse.jface.text.link.LinkedPositionGroup;
44 import org.eclipse.jface.text.link.ProposalPosition;
45 import org.eclipse.jface.text.templates.DocumentTemplateContext;
46 import org.eclipse.jface.text.templates.GlobalTemplateVariables;
47 import org.eclipse.jface.text.templates.Template;
48 import org.eclipse.jface.text.templates.TemplateBuffer;
49 import org.eclipse.jface.text.templates.TemplateContext;
50 import org.eclipse.jface.text.templates.TemplateException;
51 import org.eclipse.jface.text.templates.TemplateVariable;
52 import org.eclipse.swt.graphics.Image;
53 import org.eclipse.swt.graphics.Point;
54 import org.eclipse.swt.widgets.Shell;
55 import org.eclipse.ui.IEditorPart;
56 import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
57
58 /**
59  * A template proposal.
60  */
61 public class TemplateProposal implements IPHPCompletionProposal,
62                 ICompletionProposalExtension2, ICompletionProposalExtension3 {
63
64         private final Template fTemplate;
65
66         private final TemplateContext fContext;
67
68         private final Image fImage;
69
70         private IRegion fRegion;
71
72         private int fRelevance;
73
74         private IRegion fSelectedRegion; // initialized by apply()
75
76         private String fDisplayString;
77
78         /**
79          * Creates a template proposal with a template and its context.
80          * 
81          * @param template
82          *            the template
83          * @param context
84          *            the context in which the template was requested
85          * @param region
86          *            the region this proposal applies to
87          * @param image
88          *            the icon of the proposal
89          */
90         public TemplateProposal(Template template, TemplateContext context,
91                         IRegion region, Image image) {
92                 Assert.isNotNull(template);
93                 Assert.isNotNull(context);
94                 Assert.isNotNull(region);
95
96                 fTemplate = template;
97                 fContext = context;
98                 fImage = image;
99                 fRegion = region;
100
101                 fDisplayString = null;
102
103                 if (context instanceof JavaContext) {
104                         switch (((JavaContext) context).getCharacterBeforeStart()) {
105                         // high relevance after whitespace
106                         case ' ':
107                         case '\r':
108                         case '\n':
109                         case '\t':
110                                 fRelevance = 90;
111                                 break;
112                         default:
113                                 fRelevance = 0;
114                         }
115                 } else {
116                         fRelevance = 90;
117                 }
118         }
119
120         /*
121          * @see ICompletionProposal#apply(IDocument)
122          */
123         public final void apply(IDocument document) {
124                 // not called anymore
125         }
126
127         /*
128          * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#apply(org.eclipse.jface.text.ITextViewer,
129          *      char, int, int)
130          */
131         public void apply(ITextViewer viewer, char trigger, int stateMask,
132                         int offset) {
133
134                 try {
135
136                         fContext.setReadOnly(false);
137                         TemplateBuffer templateBuffer;
138                         try {
139                                 templateBuffer = fContext.evaluate(fTemplate);
140                         } catch (TemplateException e1) {
141                                 fSelectedRegion = fRegion;
142                                 return;
143                         }
144
145                         int start = getReplaceOffset();
146                         int end = getReplaceEndOffset();
147                         end = Math.max(end, offset);
148
149                         // insert template string
150                         IDocument document = viewer.getDocument();
151                         String templateString = templateBuffer.getString();
152                         document.replace(start, end - start, templateString);
153
154                         // translate positions
155                         LinkedModeModel model = new LinkedModeModel();
156                         TemplateVariable[] variables = templateBuffer.getVariables();
157
158                         MultiVariableGuess guess = fContext instanceof CompilationUnitContext ? ((CompilationUnitContext) fContext)
159                                         .getMultiVariableGuess()
160                                         : null;
161
162                         boolean hasPositions = false;
163                         for (int i = 0; i != variables.length; i++) {
164                                 TemplateVariable variable = variables[i];
165
166                                 if (variable.isUnambiguous())
167                                         continue;
168
169                                 LinkedPositionGroup group = new LinkedPositionGroup();
170
171                                 int[] offsets = variable.getOffsets();
172                                 int length = variable.getLength();
173
174                                 LinkedPosition first;
175                                 if (guess != null && variable instanceof MultiVariable) {
176                                         first = new VariablePosition(document, offsets[0] + start,
177                                                         length, guess, (MultiVariable) variable);
178                                         guess.addSlave((VariablePosition) first);
179                                 } else {
180                                         String[] values = variable.getValues();
181                                         ICompletionProposal[] proposals = new ICompletionProposal[values.length];
182                                         for (int j = 0; j < values.length; j++) {
183                                                 ensurePositionCategoryInstalled(document, model);
184                                                 Position pos = new Position(offsets[0] + start, length);
185                                                 document.addPosition(getCategory(), pos);
186                                                 proposals[j] = new PositionBasedCompletionProposal(
187                                                                 values[j], pos, length);
188                                         }
189
190                                         if (proposals.length > 1)
191                                                 first = new ProposalPosition(document, offsets[0]
192                                                                 + start, length, proposals);
193                                         else
194                                                 first = new LinkedPosition(document,
195                                                                 offsets[0] + start, length);
196                                 }
197
198                                 for (int j = 0; j != offsets.length; j++)
199                                         if (j == 0)
200                                                 group.addPosition(first);
201                                         else
202                                                 group.addPosition(new LinkedPosition(document,
203                                                                 offsets[j] + start, length));
204
205                                 model.addGroup(group);
206                                 hasPositions = true;
207                         }
208
209                         if (hasPositions) {
210                                 model.forceInstall();
211                                 PHPEditor editor = getJavaEditor();
212                                 if (editor != null) {
213                                         model
214                                                         .addLinkingListener(new EditorHighlightingSynchronizer(
215                                                                         editor));
216                                 }
217
218                                 LinkedModeUI ui = new EditorLinkedModeUI(model, viewer);
219                                 ui.setExitPosition(viewer, getCaretOffset(templateBuffer)
220                                                 + start, 0, Integer.MAX_VALUE);
221                                 ui.enter();
222
223                                 fSelectedRegion = ui.getSelectedRegion();
224                         } else
225                                 fSelectedRegion = new Region(getCaretOffset(templateBuffer)
226                                                 + start, 0);
227
228                 } catch (BadLocationException e) {
229                         PHPeclipsePlugin.log(e);
230                         openErrorDialog(viewer.getTextWidget().getShell(), e);
231                         fSelectedRegion = fRegion;
232                 } catch (BadPositionCategoryException e) {
233                         PHPeclipsePlugin.log(e);
234                         openErrorDialog(viewer.getTextWidget().getShell(), e);
235                         fSelectedRegion = fRegion;
236                 }
237
238         }
239
240         /**
241          * Returns the currently active java editor, or <code>null</code> if it
242          * cannot be determined.
243          * 
244          * @return the currently active java editor, or <code>null</code>
245          */
246         private PHPEditor getJavaEditor() {
247                 IEditorPart part = PHPeclipsePlugin.getActivePage().getActiveEditor();
248                 if (part instanceof PHPEditor)
249                         return (PHPEditor) part;
250                 else
251                         return null;
252         }
253
254         /**
255          * Returns the offset of the range in the document that will be replaced by
256          * applying this template.
257          * 
258          * @return the offset of the range in the document that will be replaced by
259          *         applying this template
260          */
261         private int getReplaceOffset() {
262                 int start;
263                 if (fContext instanceof DocumentTemplateContext) {
264                         DocumentTemplateContext docContext = (DocumentTemplateContext) fContext;
265                         start = docContext.getStart();
266                 } else {
267                         start = fRegion.getOffset();
268                 }
269                 return start;
270         }
271
272         /**
273          * Returns the end offset of the range in the document that will be replaced
274          * by applying this template.
275          * 
276          * @return the end offset of the range in the document that will be replaced
277          *         by applying this template
278          */
279         private int getReplaceEndOffset() {
280                 int end;
281                 if (fContext instanceof DocumentTemplateContext) {
282                         DocumentTemplateContext docContext = (DocumentTemplateContext) fContext;
283                         end = docContext.getEnd();
284                 } else {
285                         end = fRegion.getOffset() + fRegion.getLength();
286                 }
287                 return end;
288         }
289
290         private void ensurePositionCategoryInstalled(final IDocument document,
291                         LinkedModeModel model) {
292                 if (!document.containsPositionCategory(getCategory())) {
293                         document.addPositionCategory(getCategory());
294                         final InclusivePositionUpdater updater = new InclusivePositionUpdater(
295                                         getCategory());
296                         document.addPositionUpdater(updater);
297
298                         model.addLinkingListener(new ILinkedModeListener() {
299
300                                 /*
301                                  * @see org.eclipse.jface.text.link.ILinkedModeListener#left(org.eclipse.jface.text.link.LinkedModeModel,
302                                  *      int)
303                                  */
304                                 public void left(LinkedModeModel environment, int flags) {
305                                         try {
306                                                 document.removePositionCategory(getCategory());
307                                         } catch (BadPositionCategoryException e) {
308                                                 // ignore
309                                         }
310                                         document.removePositionUpdater(updater);
311                                 }
312
313                                 public void suspend(LinkedModeModel environment) {
314                                 }
315
316                                 public void resume(LinkedModeModel environment, int flags) {
317                                 }
318                         });
319                 }
320         }
321
322         private String getCategory() {
323                 return "TemplateProposalCategory_" + toString(); //$NON-NLS-1$
324         }
325
326         private int getCaretOffset(TemplateBuffer buffer) {
327
328                 TemplateVariable[] variables = buffer.getVariables();
329                 for (int i = 0; i != variables.length; i++) {
330                         TemplateVariable variable = variables[i];
331                         if (variable.getType().equals(GlobalTemplateVariables.Cursor.NAME))
332                                 return variable.getOffsets()[0];
333                 }
334
335                 return buffer.getString().length();
336         }
337
338         /*
339          * @see ICompletionProposal#getSelection(IDocument)
340          */
341         public Point getSelection(IDocument document) {
342                 return new Point(fSelectedRegion.getOffset(), fSelectedRegion
343                                 .getLength());
344         }
345
346         /*
347          * @see ICompletionProposal#getAdditionalProposalInfo()
348          */
349         public String getAdditionalProposalInfo() {
350                 try {
351                         fContext.setReadOnly(true);
352                         TemplateBuffer templateBuffer;
353                         try {
354                                 templateBuffer = fContext.evaluate(fTemplate);
355                         } catch (TemplateException e1) {
356                                 return null;
357                         }
358
359                         return templateBuffer.getString();
360
361                 } catch (BadLocationException e) {
362                         handleException(PHPeclipsePlugin.getActiveWorkbenchShell(),
363                                         new CoreException(new Status(IStatus.ERROR,
364                                                         PHPeclipsePlugin.getPluginId(), IStatus.OK, "", e))); //$NON-NLS-1$
365                         return null;
366                 }
367         }
368
369         /*
370          * @see ICompletionProposal#getDisplayString()
371          */
372         public String getDisplayString() {
373                 if (fDisplayString == null) {
374                         fDisplayString = fTemplate.getName()
375                                         + TemplateContentAssistMessages
376                                                         .getString("TemplateProposal.delimiter") + fTemplate.getDescription(); //$NON-NLS-1$
377                 }
378                 return fDisplayString;
379         }
380
381         public void setDisplayString(String displayString) {
382                 fDisplayString = displayString;
383         }
384
385         /*
386          * @see ICompletionProposal#getImage()
387          */
388         public Image getImage() {
389                 return fImage;
390         }
391
392         /*
393          * @see ICompletionProposal#getContextInformation()
394          */
395         public IContextInformation getContextInformation() {
396                 return null;
397         }
398
399         private void openErrorDialog(Shell shell, Exception e) {
400                 MessageDialog.openError(shell, TemplateContentAssistMessages
401                                 .getString("TemplateEvaluator.error.title"), e.getMessage()); //$NON-NLS-1$
402         }
403
404         private void handleException(Shell shell, CoreException e) {
405                 ExceptionHandler.handle(e, shell, TemplateContentAssistMessages
406                                 .getString("TemplateEvaluator.error.title"), null); //$NON-NLS-1$
407         }
408
409         /*
410          * @see IJavaCompletionProposal#getRelevance()
411          */
412         public int getRelevance() {
413                 return fRelevance;
414         }
415
416         public void setRelevance(int relevance) {
417                 fRelevance = relevance;
418         }
419
420         public Template getTemplate() {
421                 return fTemplate;
422         }
423
424         /*
425          * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getInformationControlCreator()
426          */
427         public IInformationControlCreator getInformationControlCreator() {
428                 return new TemplateInformationControlCreator();
429         }
430
431         /*
432          * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#selected(org.eclipse.jface.text.ITextViewer,
433          *      boolean)
434          */
435         public void selected(ITextViewer viewer, boolean smartToggle) {
436         }
437
438         /*
439          * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#unselected(org.eclipse.jface.text.ITextViewer)
440          */
441         public void unselected(ITextViewer viewer) {
442         }
443
444         /*
445          * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#validate(org.eclipse.jface.text.IDocument,
446          *      int, org.eclipse.jface.text.DocumentEvent)
447          */
448         public boolean validate(IDocument document, int offset, DocumentEvent event) {
449                 try {
450                         int replaceOffset = getReplaceOffset();
451                         if (offset >= replaceOffset) {
452                                 String content = document.get(replaceOffset, offset
453                                                 - replaceOffset);
454                                 return fTemplate.getName().startsWith(content);
455                         }
456                 } catch (BadLocationException e) {
457                         // concurrent modification - ignore
458                 }
459                 return false;
460         }
461
462         /*
463          * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getReplacementString()
464          */
465         public CharSequence getPrefixCompletionText(IDocument document,
466                         int completionOffset) {
467                 return fTemplate.getName();
468         }
469
470         /*
471          * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getReplacementOffset()
472          */
473         public int getPrefixCompletionStart(IDocument document, int completionOffset) {
474                 return getReplaceOffset();
475         }
476 }