replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / xpath / xpath-view / src / org / intellij / plugins / xpathView / XPathEvalAction.java
1 /*
2  * Copyright 2002-2005 Sascha Weinreuter
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.intellij.plugins.xpathView;
17
18 import com.intellij.find.FindProgressIndicator;
19 import com.intellij.find.FindSettings;
20 import com.intellij.ide.projectView.PresentationData;
21 import com.intellij.lang.Language;
22 import com.intellij.navigation.ItemPresentation;
23 import com.intellij.openapi.actionSystem.AnActionEvent;
24 import com.intellij.openapi.actionSystem.CommonDataKeys;
25 import com.intellij.openapi.application.ApplicationManager;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.editor.Editor;
28 import com.intellij.openapi.editor.ScrollType;
29 import com.intellij.openapi.editor.markup.RangeHighlighter;
30 import com.intellij.openapi.fileEditor.FileEditor;
31 import com.intellij.openapi.fileEditor.FileEditorManager;
32 import com.intellij.openapi.progress.ProgressIndicator;
33 import com.intellij.openapi.progress.ProgressManager;
34 import com.intellij.openapi.project.Project;
35 import com.intellij.openapi.ui.Messages;
36 import com.intellij.openapi.util.Factory;
37 import com.intellij.openapi.util.text.StringUtil;
38 import com.intellij.openapi.vfs.VirtualFile;
39 import com.intellij.openapi.wm.StatusBar;
40 import com.intellij.openapi.wm.WindowManager;
41 import com.intellij.psi.FileViewProvider;
42 import com.intellij.psi.PsiDocumentManager;
43 import com.intellij.psi.PsiElement;
44 import com.intellij.psi.PsiFile;
45 import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider;
46 import com.intellij.psi.xml.XmlElement;
47 import com.intellij.psi.xml.XmlFile;
48 import com.intellij.usageView.UsageInfo;
49 import com.intellij.usages.*;
50 import com.intellij.util.Processor;
51 import icons.XpathIcons;
52 import org.intellij.plugins.xpathView.eval.EvalExpressionDialog;
53 import org.intellij.plugins.xpathView.support.XPathSupport;
54 import org.intellij.plugins.xpathView.ui.InputExpressionDialog;
55 import org.intellij.plugins.xpathView.util.CachedVariableContext;
56 import org.intellij.plugins.xpathView.util.HighlighterUtil;
57 import org.intellij.plugins.xpathView.util.MyPsiUtil;
58 import org.jaxen.JaxenException;
59 import org.jaxen.XPath;
60 import org.jaxen.XPathSyntaxException;
61 import org.jaxen.saxpath.SAXPathException;
62 import org.jetbrains.annotations.NotNull;
63 import org.jetbrains.annotations.Nullable;
64
65 import javax.swing.*;
66 import java.util.ArrayList;
67 import java.util.Collections;
68 import java.util.Comparator;
69 import java.util.List;
70
71 /**
72  * <p>This class implements the core action to enter, evaluate and display the results of an XPath expression.</p>
73  *
74  * <p>The evaluation is performed by the <a target="_blank" href="http://www.jaxen.org">Jaxen</a> XPath-engine, which allows arbitrary
75  * object models to be used. The adapter class for IDEA's object model, the PSI-tree, is located in the class
76  * {@link org.intellij.plugins.xpathView.support.jaxen.PsiDocumentNavigator}.</p>
77  *
78  * <p>The plugin can be invoked in three different ways:<ol>
79  *  <li>By pressing a keystroke (default: ctrl-alt-x, e) that can be set in the keymap configuration
80  *  <li>By selecting "Evaluate XPath" from the edtior popup menu
81  *  <li>By clicking the icon in the toolbar (it's the icon that is associated with xml-files on windows)
82  * </ol>
83  *
84  * <p>The result of an expression is displayed according to its type: Primitive XPath values (Strings, numbers, booleans)
85  * are displayed by a message box. If the result is a node/nodelist, the corresponding nodes are highlighted in IDEA's
86  * editor.</p>
87  * <p>The highlighting is cleared upon each new evaluation. Additionally, the plugin registers an own handler for the
88  * &lt;esc&gt; key, which also clears the highlighting.</p>
89  *
90  * <p>The evalutation can be performed relatively to a context node: When the option "Use node at cursor as context node"
91  * is turned on, all XPath expressions are evaluted relatively to this node. This node (which can actually only be a tag
92  * element), is then highlighted to give a visual indication when entering the expression. This does not affect
93  * expressions that start with {@code /} or {@code //}.</p>
94  *
95  * <p><b>Limitations:</b></p>
96  * <ul>
97  *  <li>Namespaces: Although queries containing namespace-prefixes are supported, the XPath namespace-axis
98  *      ({@code namespace::}) is currently unsupported.<br>
99  * <li>Matching for text(): Such queries will currently also highlight whitespace <em>inside</em> a start/end tag.<br>
100  *      This is due the tree-structure of the PSI. Further investigation is needed here.
101  * <li>String values with string(): Whitespace handling for the string() function is far from being correctly
102  *      implemented. To produce somewhat acceptable results, all whitespace inside a string is normalized.<br>
103  *      <em>DON'T EXPECT THESE RESULTS TO BE THE SAME AS WITH OTHER TOOLS</em>.
104  * <li>Entites references: This is a limitation for matching text() as well as for the result produced by string().
105  *      The only recognized entity refences are the predefined ones for XML:<br>&nbsp;&nbsp;
106  *          &amp;amp; &amp;lt; &amp;gt; &amp;quot;<br>
107  *      In all other cases, the text that is returned is the text shown in the editor and does not include resolved
108  *      entities. Therefore you will get no/false results when entites are involved.<br>
109  *      It is currently undecided whether it makes sense to recurse into resolved entities, because there seems no
110  *      reasonable way to display the result.
111  * <li><b>This plugin is completely based on IDEA's PSI (Program Structure Interface)</b>. This API is not part of the
112  *      current Open-API and is completely unsupported by IntelliJ. Interfaces and functionality and may be changed
113  *      without any prior notice, which might break this plugin.<br>
114  *      <em>Please don't bother IntelliJ staff in such a case</em>.
115  * <li>Probably some others ;-)
116  * </ul>
117  *
118  * @author Sascha Weinreuter
119  */
120 public class XPathEvalAction extends XPathAction {
121
122     private static final Logger LOG = Logger.getInstance("org.intellij.plugins.xpathView.XPathEvalAction");
123
124   @Override
125     protected void updateToolbar(AnActionEvent event) {
126         super.updateToolbar(event);
127         if (XpathIcons.Xml != null) {
128             event.getPresentation().setIcon(XpathIcons.Xml);
129         }
130     }
131
132     @Override
133     protected boolean isEnabledAt(XmlFile xmlFile, int offset) {
134         return true;
135     }
136
137     @Override
138     public void actionPerformed(AnActionEvent event) {
139         final Project project = event.getProject();
140         if (project == null) {
141             // no active project
142             LOG.debug("No project");
143             return;
144         }
145
146         Editor editor = CommonDataKeys.EDITOR.getData(event.getDataContext());
147         if (editor == null) {
148             FileEditorManager fem = FileEditorManager.getInstance(project);
149             editor = fem.getSelectedTextEditor();
150         }
151         if (editor == null) {
152             // no editor available
153             LOG.debug("No editor");
154             return;
155         }
156
157         // do we have an xml file?
158         final PsiDocumentManager pdm = PsiDocumentManager.getInstance(project);
159         final PsiFile psiFile = pdm.getPsiFile(editor.getDocument());
160         if (!(psiFile instanceof XmlFile)) {
161             // not xml
162             LOG.debug("No XML-File: " + psiFile);
163             return;
164         }
165
166         // make sure PSI is in sync with document
167         pdm.commitDocument(editor.getDocument());
168
169         execute(editor);
170     }
171
172     private void execute(Editor editor) {
173         final Project project = editor.getProject();
174         final PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
175         if (psiFile == null) {
176             return;
177         }
178
179         InputExpressionDialog.Context input;
180         XmlElement contextNode = null;
181         final Config cfg = XPathAppComponent.getInstance().getConfig();
182         do {
183             RangeHighlighter contextHighlighter = null;
184             if (cfg.isUseContextAtCursor()) {
185                 // find out current context node
186                 contextNode = MyPsiUtil.findContextNode(psiFile, editor);
187                 if (contextNode != null) {
188                     contextHighlighter = HighlighterUtil.highlightNode(editor, contextNode, cfg.getContextAttributes(), cfg);
189                 }
190             }
191             if (contextNode == null) {
192                 // in XPath data model, / is the document itself, including comments, PIs and the root element
193                 contextNode = ((XmlFile)psiFile).getDocument();
194                 if (contextNode == null) {
195                   FileViewProvider fileViewProvider = psiFile.getViewProvider();
196                   if (fileViewProvider instanceof TemplateLanguageFileViewProvider) {
197                     Language dataLanguage = ((TemplateLanguageFileViewProvider)fileViewProvider).getTemplateDataLanguage();
198                     PsiFile templateDataFile = fileViewProvider.getPsi(dataLanguage);
199                     if (templateDataFile instanceof XmlFile) contextNode = ((XmlFile)templateDataFile).getDocument();
200                   }
201                 }
202             }
203
204             input = inputXPathExpression(project, contextNode);
205             if (contextHighlighter != null) {
206                 contextHighlighter.dispose();
207             }
208             if (input == null) {
209                 return;
210             }
211
212             HighlighterUtil.clearHighlighters(editor);
213         } while (contextNode != null && evaluateExpression(input, contextNode, editor, cfg));
214     }
215
216     private boolean evaluateExpression(EvalExpressionDialog.Context context, XmlElement contextNode, Editor editor, Config cfg) {
217         final Project project = editor.getProject();
218
219         try {
220             final XPathSupport support = XPathSupport.getInstance();
221             final XPath xpath = support.createXPath((XmlFile)contextNode.getContainingFile(), context.input.expression, context.input.namespaces);
222
223             xpath.setVariableContext(new CachedVariableContext(context.input.variables, xpath, contextNode));
224
225             // evaluate the expression on the whole document
226             final Object result = xpath.evaluate(contextNode);
227             LOG.debug("result = " + result);
228             LOG.assertTrue(result != null, "null result?");
229
230             if (result instanceof List<?>) {
231                 final List<?> list = (List<?>)result;
232                 if (!list.isEmpty()) {
233                     if (cfg.HIGHLIGHT_RESULTS) {
234                         highlightResult(contextNode, editor, list);
235                     }
236                     if (cfg.SHOW_USAGE_VIEW) {
237                         showUsageView(editor, xpath, contextNode, list);
238                     }
239                     if (!cfg.SHOW_USAGE_VIEW && !cfg.HIGHLIGHT_RESULTS) {
240                         final String s = StringUtil.pluralize("match", list.size());
241                         Messages.showInfoMessage(project, "Expression produced " + list.size() + " " + s, "XPath Result");
242                     }
243                 } else {
244                     return Messages.showOkCancelDialog(project, "Sorry, your expression did not return any result", "XPath Result",
245                                                        "OK", "Edit Expression", Messages.getInformationIcon()) != Messages.OK;
246                 }
247             } else if (result instanceof String) {
248                 Messages.showMessageDialog("'" + result.toString() + "'", "XPath result (String)", Messages.getInformationIcon());
249             } else if (result instanceof Number) {
250                 Messages.showMessageDialog(result.toString(), "XPath result (Number)", Messages.getInformationIcon());
251             } else if (result instanceof Boolean) {
252                 Messages.showMessageDialog(result.toString(), "XPath result (Boolean)", Messages.getInformationIcon());
253             } else {
254               LOG.error("Unknown XPath result: " + result);
255             }
256         } catch (XPathSyntaxException e) {
257             LOG.debug(e);
258             // TODO: Better layout of the error message with non-fixed size fonts
259             return Messages.showOkCancelDialog(project, e.getMultilineMessage(), "XPath syntax error", "Edit Expression", "Cancel", Messages.getErrorIcon()) == Messages.OK;
260         } catch (SAXPathException e) {
261             LOG.debug(e);
262             Messages.showMessageDialog(project, e.getMessage(), "XPath error", Messages.getErrorIcon());
263         }
264         return false;
265     }
266
267     private void showUsageView(final Editor editor, final XPath xPath, final XmlElement contextNode, final List<?> result) {
268         final Project project = editor.getProject();
269
270         //noinspection unchecked
271         final List<?> _result = new ArrayList(result);
272         final Factory<UsageSearcher> searcherFactory = () -> new MyUsageSearcher(_result, xPath, contextNode);
273         final MyUsageTarget usageTarget = new MyUsageTarget(xPath.toString(), contextNode);
274
275         showUsageView(project, usageTarget, searcherFactory, new EditExpressionAction() {
276             final Config config = XPathAppComponent.getInstance().getConfig();
277
278             @Override
279             protected void execute() {
280                 XPathEvalAction.this.execute(editor);
281             }
282         });
283     }
284
285     public static void showUsageView(@NotNull final Project project, MyUsageTarget usageTarget, Factory<UsageSearcher> searcherFactory, final EditExpressionAction editAction) {
286         final UsageViewPresentation presentation = new UsageViewPresentation();
287         presentation.setTargetsNodeText("XPath Expression");
288         presentation.setCodeUsages(false);
289         presentation.setCodeUsagesString("Found Matches");
290         presentation.setNonCodeUsagesString("Result");
291         presentation.setUsagesString("XPath Result");
292         presentation.setUsagesWord("match");
293         final ItemPresentation targetPresentation = usageTarget.getPresentation();
294         if (targetPresentation != null) {
295           presentation
296             .setTabText(StringUtil.shortenTextWithEllipsis("XPath '" + targetPresentation.getPresentableText() + '\'', 60, 0, true));
297         }
298         else {
299           presentation.setTabText("XPath");
300         }
301         presentation.setScopeText("XML Files");
302
303         presentation.setOpenInNewTab(FindSettings.getInstance().isShowResultsInSeparateView());
304
305         final FindUsagesProcessPresentation processPresentation = new FindUsagesProcessPresentation(presentation);
306         processPresentation.setProgressIndicatorFactory(() -> new FindProgressIndicator(project, "XML Document(s)"));
307         processPresentation.setShowPanelIfOnlyOneUsage(true);
308         processPresentation.setShowNotFoundMessage(true);
309         final UsageTarget[] usageTargets = { usageTarget };
310
311         UsageViewManager.getInstance(project).searchAndShowUsages(
312                 usageTargets,
313                 searcherFactory,
314                 processPresentation,
315                 presentation,
316                 new UsageViewManager.UsageViewStateListener() {
317                     @Override
318                     public void usageViewCreated(@NotNull UsageView usageView) {
319                         usageView.addButtonToLowerPane(editAction, "&Edit Expression");
320                     }
321
322                     @Override
323                     public void findingUsagesFinished(UsageView usageView) {
324                     }
325                 });
326     }
327
328     /**
329      * Opens an input box to input an XPath expression. The box will have a history dropdown from which
330      * previously entered expressions can be selected.
331      * @return The expression or {@code null} if the user hits the cancel button
332      * @param project The project to take the history from
333      */
334     @Nullable
335     private EvalExpressionDialog.Context inputXPathExpression(final Project project, XmlElement contextNode) {
336         final XPathProjectComponent pc = XPathProjectComponent.getInstance(project);
337         LOG.assertTrue(pc != null);
338
339         // get expression history from project component
340         final HistoryElement[] history = pc.getHistory();
341
342         final EvalExpressionDialog dialog = new EvalExpressionDialog(project, XPathAppComponent.getInstance().getConfig(), history);
343         if (!dialog.show(contextNode)) {
344             // cancel
345             LOG.debug("Input canceled");
346             return null;
347         }
348
349         final InputExpressionDialog.Context context = dialog.getContext();
350         LOG.debug("expression = " + context.input.expression);
351
352         pc.addHistory(context.input);
353
354         return context;
355     }
356
357     /**
358      * <p>Process the result of an XPath query.</p>
359      * <p>If the result is a {@code java.util.List} object, iterate over all elements and
360      * add a highlighter object in the editor if the element is of type {@code PsiElement}.
361      * <p>If the result is a primitive value (String, Number, Boolean) a message box displaying
362      * the value will be displayed. </p>
363      *
364      * @param editor The editor object to apply the highlighting to
365      */
366     private void highlightResult(XmlElement contextNode, @NotNull final Editor editor, final List<?> list) {
367
368         final Config cfg = XPathAppComponent.getInstance().getConfig();
369         int lowestOffset = Integer.MAX_VALUE;
370
371         for (final Object o : list) {
372             LOG.assertTrue(o != null, "null element?");
373
374             if (o instanceof PsiElement) {
375                 final PsiElement element = (PsiElement)o;
376
377                 if (element.getContainingFile() == contextNode.getContainingFile()) {
378                     lowestOffset = highlightElement(editor, element, cfg, lowestOffset);
379                 }
380             } else {
381                 LOG.info("Don't know what to do with " + o + " in a list context");
382             }
383             LOG.debug("o = " + o);
384         }
385
386         if (cfg.isScrollToFirst() && lowestOffset != Integer.MAX_VALUE) {
387             editor.getScrollingModel().scrollTo(editor.offsetToLogicalPosition(lowestOffset), ScrollType.MAKE_VISIBLE);
388             editor.getCaretModel().moveToOffset(lowestOffset);
389         }
390
391         SwingUtilities.invokeLater(() -> {
392             final StatusBar statusBar = WindowManager.getInstance().getStatusBar(editor.getProject());
393             final String s = StringUtil.pluralize("match", list.size());
394             statusBar.setInfo(list.size() + " XPath " + s + " found (press Escape to remove the highlighting)");
395         });
396     }
397
398     private static int highlightElement(Editor editor, PsiElement element, Config cfg, int offset) {
399         final RangeHighlighter highlighter = HighlighterUtil.highlightNode(editor, element, cfg.getAttributes(), cfg);
400         HighlighterUtil.addHighlighter(editor, highlighter);
401
402         return Math.min(highlighter.getStartOffset(), offset);
403     }
404
405     public static class MyUsageTarget implements UsageTarget {
406         private final ItemPresentation myItemPresentation;
407         private final XmlElement myContextNode;
408
409         public MyUsageTarget(String expression, XmlElement contextNode) {
410             myContextNode = contextNode;
411             myItemPresentation = new PresentationData(expression, null, null, null);
412         }
413
414         @Override
415         public void findUsages() {
416             throw new IllegalArgumentException();
417         }
418
419         @Override
420         public void findUsagesInEditor(@NotNull FileEditor editor) {
421             throw new IllegalArgumentException();
422         }
423
424       @Override
425       public void highlightUsages(@NotNull PsiFile file, @NotNull Editor editor, boolean clearHighlights) {
426         throw new UnsupportedOperationException();
427       }
428
429       @Override
430       public boolean isValid() {
431             // re-run will become unavailable if the context node is invalid
432             return myContextNode == null || myContextNode.isValid();
433         }
434
435         @Override
436         public boolean isReadOnly() {
437             return true;
438         }
439
440         @Override
441         @Nullable
442         public VirtualFile[] getFiles() {
443             return null;
444         }
445
446         @Override
447         public void update() {
448         }
449
450         @Override
451         public String getName() {
452             return "Expression";
453         }
454
455         @Override
456         public ItemPresentation getPresentation() {
457             return myItemPresentation;
458         }
459
460         @Override
461         public void navigate(boolean requestFocus) {
462         }
463
464         @Override
465         public boolean canNavigate() {
466             return false;
467         }
468
469         @Override
470         public boolean canNavigateToSource() {
471             return false;
472         }
473     }
474
475     private static class MyUsageSearcher implements UsageSearcher {
476         private final List<?> myResult;
477         private final XPath myXPath;
478         private final XmlElement myContextNode;
479
480         public MyUsageSearcher(List<?> result, XPath xPath, XmlElement contextNode) {
481             myResult = result;
482             myXPath = xPath;
483             myContextNode = contextNode;
484         }
485
486         @Override
487         public void generate(@NotNull final Processor<Usage> processor) {
488             Runnable runnable = () -> {
489                 final List<?> list;
490                 if (myResult.isEmpty()) {
491                     try {
492                         list = (List<?>)myXPath.selectNodes(myContextNode);
493                     } catch (JaxenException e) {
494                         LOG.debug(e);
495                         Messages.showMessageDialog(myContextNode.getProject(), e.getMessage(), "XPath error", Messages.getErrorIcon());
496                         return;
497                     }
498                 } else {
499                     list = myResult;
500                 }
501
502                 final int size = list.size();
503                 final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
504                 indicator.setText("Collecting matches...");
505
506                 Collections.sort(list, (Comparator)(o1, o2) -> {
507                     indicator.checkCanceled();
508                     if (o1 instanceof PsiElement && o2 instanceof PsiElement) {
509                         return ((PsiElement)o1).getTextRange().getStartOffset() - ((PsiElement)o2).getTextRange().getStartOffset();
510                     } else {
511                         return String.valueOf(o1).compareTo(String.valueOf(o2));
512                     }
513                 });
514                 for (int i = 0; i < size; i++) {
515                     indicator.checkCanceled();
516                     Object o = list.get(i);
517                     if (o instanceof PsiElement) {
518                         final PsiElement element = (PsiElement)o;
519                         processor.process(new UsageInfo2UsageAdapter(new UsageInfo(element)));
520                         indicator.setText2(element.getContainingFile().getName());
521                     }
522                     indicator.setFraction(i / (double)size);
523                 }
524                 list.clear();
525             };
526             ApplicationManager.getApplication().runReadAction(runnable);
527         }
528     }
529
530     public abstract static class EditExpressionAction implements Runnable {
531         @Override
532         public void run() {
533           execute();
534         }
535
536         protected abstract void execute();
537     }
538 }