| /* |
| * Copyright 2002-2005 Sascha Weinreuter |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.intellij.plugins.xpathView; |
| |
| import com.intellij.find.FindProgressIndicator; |
| import com.intellij.ide.projectView.PresentationData; |
| import com.intellij.lang.Language; |
| import com.intellij.navigation.ItemPresentation; |
| import com.intellij.openapi.actionSystem.AnActionEvent; |
| import com.intellij.openapi.actionSystem.CommonDataKeys; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.editor.ScrollType; |
| import com.intellij.openapi.editor.markup.RangeHighlighter; |
| import com.intellij.openapi.fileEditor.FileEditor; |
| import com.intellij.openapi.fileEditor.FileEditorManager; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.Messages; |
| import com.intellij.openapi.util.Factory; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.openapi.wm.StatusBar; |
| import com.intellij.openapi.wm.WindowManager; |
| import com.intellij.psi.FileViewProvider; |
| import com.intellij.psi.PsiDocumentManager; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider; |
| import com.intellij.psi.xml.XmlElement; |
| import com.intellij.psi.xml.XmlFile; |
| import com.intellij.usageView.UsageInfo; |
| import com.intellij.usages.*; |
| import com.intellij.util.Processor; |
| import icons.XpathIcons; |
| import org.intellij.plugins.xpathView.eval.EvalExpressionDialog; |
| import org.intellij.plugins.xpathView.support.XPathSupport; |
| import org.intellij.plugins.xpathView.ui.InputExpressionDialog; |
| import org.intellij.plugins.xpathView.util.CachedVariableContext; |
| import org.intellij.plugins.xpathView.util.HighlighterUtil; |
| import org.intellij.plugins.xpathView.util.MyPsiUtil; |
| import org.jaxen.JaxenException; |
| import org.jaxen.XPath; |
| import org.jaxen.XPathSyntaxException; |
| import org.jaxen.saxpath.SAXPathException; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| |
| /** |
| * <p>This class implements the core action to enter, evaluate and display the results of an XPath expression.</p> |
| * |
| * <p>The evaluation is performed by the <a target="_blank" href="http://www.jaxen.org">Jaxen</a> XPath-engine, which allows arbitrary |
| * object models to be used. The adapter class for IDEA's object model, the PSI-tree, is located in the class |
| * {@link org.intellij.plugins.xpathView.support.jaxen.PsiDocumentNavigator}.</p> |
| * |
| * <p>The plugin can be invoked in three different ways:<ol> |
| * <li>By pressing a keystroke (default: ctrl-alt-x, e) that can be set in the keymap configuration |
| * <li>By selecting "Evaluate XPath" from the edtior popup menu |
| * <li>By clicking the icon in the toolbar (it's the icon that is associated with xml-files on windows) |
| * </ol> |
| * |
| * <p>The result of an expression is displayed according to its type: Primitive XPath values (Strings, numbers, booleans) |
| * are displayed by a message box. If the result is a node/nodelist, the corresponding nodes are highlighted in IDEA's |
| * editor.</p> |
| * <p>The highlighting is cleared upon each new evaluation. Additionally, the plugin registers an own handler for the |
| * <esc> key, which also clears the highlighting.</p> |
| * |
| * <p>The evalutation can be performed relatively to a context node: When the option "Use node at cursor as context node" |
| * is turned on, all XPath expressions are evaluted relatively to this node. This node (which can actually only be a tag |
| * element), is then highlighted to give a visual indication when entering the expression. This does not affect |
| * expressions that start with <code>/</code> or <code>//</code>.</p> |
| * |
| * <p><b>Limitations:</b></p> |
| * <ul> |
| * <li>Namespaces: Although queries containing namespace-prefixes are supported, the XPath namespace-axis |
| * (<code>namespace::</code>) is currently unsupported.<br> |
| * <li>Matching for text(): Such queries will currently also highlight whitespace <em>inside</em> a start/end tag.<br> |
| * This is due the tree-structure of the PSI. Further investigation is needed here. |
| * <li>String values with string(): Whitespace handling for the string() function is far from being correctly |
| * implemented. To produce somewhat acceptable results, all whitespace inside a string is normalized.<br> |
| * <em>DON'T EXPECT THESE RESULTS TO BE THE SAME AS WITH OTHER TOOLS</em>. |
| * <li>Entites references: This is a limitation for matching text() as well as for the result produced by string(). |
| * The only recognized entity refences are the predefined ones for XML:<br> |
| * &amp; &lt; &gt; &quot;<br> |
| * In all other cases, the text that is returned is the text shown in the editor and does not include resolved |
| * entities. Therefore you will get no/false results when entites are involved.<br> |
| * It is currently undecided whether it makes sense to recurse into resolved entities, because there seems no |
| * reasonable way to display the result. |
| * <li><b>This plugin is completely based on IDEA's PSI (Program Structure Interface)</b>. This API is not part of the |
| * current Open-API and is completely unsupported by IntelliJ. Interfaces and functionality and may be changed |
| * without any prior notice, which might break this plugin.<br> |
| * <em>Please don't bother IntelliJ staff in such a case</em>. |
| * <li>Probably some others ;-) |
| * </ul> |
| * |
| * @author Sascha Weinreuter |
| */ |
| public class XPathEvalAction extends XPathAction { |
| |
| private static final Logger LOG = Logger.getInstance("org.intellij.plugins.xpathView.XPathEvalAction"); |
| |
| @Override |
| protected void updateToolbar(AnActionEvent event) { |
| super.updateToolbar(event); |
| if (XpathIcons.Xml != null) { |
| event.getPresentation().setIcon(XpathIcons.Xml); |
| } |
| } |
| |
| @Override |
| protected boolean isEnabledAt(XmlFile xmlFile, int offset) { |
| return true; |
| } |
| |
| @Override |
| public void actionPerformed(AnActionEvent event) { |
| final Project project = CommonDataKeys.PROJECT.getData(event.getDataContext()); |
| if (project == null) { |
| // no active project |
| LOG.debug("No project"); |
| return; |
| } |
| |
| Editor editor = CommonDataKeys.EDITOR.getData(event.getDataContext()); |
| if (editor == null) { |
| FileEditorManager fem = FileEditorManager.getInstance(project); |
| editor = fem.getSelectedTextEditor(); |
| } |
| if (editor == null) { |
| // no editor available |
| LOG.debug("No editor"); |
| return; |
| } |
| |
| // do we have an xml file? |
| final PsiDocumentManager pdm = PsiDocumentManager.getInstance(project); |
| final PsiFile psiFile = pdm.getPsiFile(editor.getDocument()); |
| if (!(psiFile instanceof XmlFile)) { |
| // not xml |
| LOG.debug("No XML-File: " + psiFile); |
| return; |
| } |
| |
| // make sure PSI is in sync with document |
| pdm.commitDocument(editor.getDocument()); |
| |
| execute(editor); |
| } |
| |
| private void execute(Editor editor) { |
| final Project project = editor.getProject(); |
| final PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); |
| if (psiFile == null) { |
| return; |
| } |
| |
| InputExpressionDialog.Context input; |
| XmlElement contextNode = null; |
| final Config cfg = myComponent.getConfig(); |
| do { |
| RangeHighlighter contextHighlighter = null; |
| if (cfg.isUseContextAtCursor()) { |
| // find out current context node |
| contextNode = MyPsiUtil.findContextNode(psiFile, editor); |
| if (contextNode != null) { |
| contextHighlighter = HighlighterUtil.highlightNode(editor, contextNode, cfg.getContextAttributes(), cfg); |
| } |
| } |
| if (contextNode == null) { |
| // in XPath data model, / is the document itself, including comments, PIs and the root element |
| contextNode = ((XmlFile)psiFile).getDocument(); |
| if (contextNode == null) { |
| FileViewProvider fileViewProvider = psiFile.getViewProvider(); |
| if (fileViewProvider instanceof TemplateLanguageFileViewProvider) { |
| Language dataLanguage = ((TemplateLanguageFileViewProvider)fileViewProvider).getTemplateDataLanguage(); |
| PsiFile templateDataFile = fileViewProvider.getPsi(dataLanguage); |
| if (templateDataFile instanceof XmlFile) contextNode = ((XmlFile)templateDataFile).getDocument(); |
| } |
| } |
| } |
| |
| input = inputXPathExpression(project, contextNode); |
| if (contextHighlighter != null) { |
| contextHighlighter.dispose(); |
| } |
| if (input == null) { |
| return; |
| } |
| |
| HighlighterUtil.clearHighlighters(editor); |
| } while (contextNode != null && evaluateExpression(input, contextNode, editor, cfg)); |
| } |
| |
| private boolean evaluateExpression(EvalExpressionDialog.Context context, XmlElement contextNode, Editor editor, Config cfg) { |
| final Project project = editor.getProject(); |
| |
| try { |
| final XPathSupport support = XPathSupport.getInstance(); |
| final XPath xpath = support.createXPath((XmlFile)contextNode.getContainingFile(), context.input.expression, context.input.namespaces); |
| |
| xpath.setVariableContext(new CachedVariableContext(context.input.variables, xpath, contextNode)); |
| |
| // evaluate the expression on the whole document |
| final Object result = xpath.evaluate(contextNode); |
| LOG.debug("result = " + result); |
| LOG.assertTrue(result != null, "null result?"); |
| |
| if (result instanceof List<?>) { |
| final List<?> list = (List<?>)result; |
| if (!list.isEmpty()) { |
| if (cfg.HIGHLIGHT_RESULTS) { |
| highlightResult(contextNode, editor, list); |
| } |
| if (cfg.SHOW_USAGE_VIEW) { |
| showUsageView(editor, xpath, contextNode, list); |
| } |
| if (!cfg.SHOW_USAGE_VIEW && !cfg.HIGHLIGHT_RESULTS) { |
| final String s = StringUtil.pluralize("match", list.size()); |
| Messages.showInfoMessage(project, "Expression produced " + list.size() + " " + s, "XPath Result"); |
| } |
| } else { |
| return Messages.showOkCancelDialog(project, "Sorry, your expression did not return any result", "XPath Result", |
| "OK", "Edit Expression", Messages.getInformationIcon()) != Messages.OK; |
| } |
| } else if (result instanceof String) { |
| Messages.showMessageDialog("'" + result.toString() + "'", "XPath result (String)", Messages.getInformationIcon()); |
| } else if (result instanceof Number) { |
| Messages.showMessageDialog(result.toString(), "XPath result (Number)", Messages.getInformationIcon()); |
| } else if (result instanceof Boolean) { |
| Messages.showMessageDialog(result.toString(), "XPath result (Boolean)", Messages.getInformationIcon()); |
| } else { |
| LOG.error("Unknown XPath result: " + result); |
| } |
| } catch (XPathSyntaxException e) { |
| LOG.debug(e); |
| // TODO: Better layout of the error message with non-fixed size fonts |
| return Messages.showOkCancelDialog(project, e.getMultilineMessage(), "XPath syntax error", "Edit Expression", "Cancel", Messages.getErrorIcon()) == Messages.OK; |
| } catch (SAXPathException e) { |
| LOG.debug(e); |
| Messages.showMessageDialog(project, e.getMessage(), "XPath error", Messages.getErrorIcon()); |
| } |
| return false; |
| } |
| |
| private void showUsageView(final Editor editor, final XPath xPath, final XmlElement contextNode, final List<?> result) { |
| final Project project = editor.getProject(); |
| |
| //noinspection unchecked |
| final List<?> _result = new ArrayList(result); |
| final Factory<UsageSearcher> searcherFactory = new Factory<UsageSearcher>() { |
| @Override |
| public UsageSearcher create() { |
| return new MyUsageSearcher(_result, xPath, contextNode); |
| } |
| }; |
| final MyUsageTarget usageTarget = new MyUsageTarget(xPath.toString(), contextNode); |
| |
| showUsageView(project, usageTarget, searcherFactory, new EditExpressionAction() { |
| final Config config = myComponent.getConfig(); |
| |
| @Override |
| protected void execute() { |
| config.OPEN_NEW_TAB = false; |
| XPathEvalAction.this.execute(editor); |
| } |
| |
| @Override |
| protected Object saveState() { |
| return config.OPEN_NEW_TAB; |
| } |
| |
| @Override |
| protected void restoreState(Object o) { |
| if (!config.OPEN_NEW_TAB) config.OPEN_NEW_TAB = Boolean.TRUE.equals(o); |
| } |
| }); |
| } |
| |
| public static void showUsageView(@NotNull final Project project, MyUsageTarget usageTarget, Factory<UsageSearcher> searcherFactory, final EditExpressionAction editAction) { |
| final UsageViewPresentation presentation = new UsageViewPresentation(); |
| presentation.setTargetsNodeText("Expression"); |
| presentation.setCodeUsages(false); |
| presentation.setCodeUsagesString("Result"); |
| presentation.setNonCodeUsagesString("Result"); |
| presentation.setUsagesString("XPath Result"); |
| presentation.setUsagesWord("match"); |
| presentation.setTabText("XPath"); |
| presentation.setScopeText("XML Files"); |
| |
| presentation.setOpenInNewTab(XPathAppComponent.getInstance().getConfig().OPEN_NEW_TAB); |
| |
| final FindUsagesProcessPresentation processPresentation = new FindUsagesProcessPresentation(presentation); |
| processPresentation.setProgressIndicatorFactory(new Factory<ProgressIndicator>() { |
| @Override |
| public ProgressIndicator create() { |
| return new FindProgressIndicator(project, "XML Document(s)"); |
| } |
| }); |
| processPresentation.setShowPanelIfOnlyOneUsage(true); |
| processPresentation.setShowNotFoundMessage(true); |
| final UsageTarget[] usageTargets = { usageTarget }; |
| |
| UsageViewManager.getInstance(project).searchAndShowUsages( |
| usageTargets, |
| searcherFactory, |
| processPresentation, |
| presentation, |
| new UsageViewManager.UsageViewStateListener() { |
| @Override |
| public void usageViewCreated(@NotNull UsageView usageView) { |
| usageView.addButtonToLowerPane(editAction, "&Edit Expression"); |
| } |
| |
| @Override |
| public void findingUsagesFinished(UsageView usageView) { |
| } |
| }); |
| } |
| |
| /** |
| * Opens an input box to input an XPath expression. The box will have a history dropdown from which |
| * previously entered expressions can be selected. |
| * @return The expression or <code>null</code> if the user hits the cancel button |
| * @param project The project to take the history from |
| */ |
| @Nullable |
| private EvalExpressionDialog.Context inputXPathExpression(final Project project, XmlElement contextNode) { |
| final XPathProjectComponent pc = XPathProjectComponent.getInstance(project); |
| LOG.assertTrue(pc != null); |
| |
| // get expression history from project component |
| final HistoryElement[] history = pc.getHistory(); |
| |
| final EvalExpressionDialog dialog = new EvalExpressionDialog(project, myComponent.getConfig(), history); |
| if (!dialog.show(contextNode)) { |
| // cancel |
| LOG.debug("Input canceled"); |
| return null; |
| } |
| |
| final InputExpressionDialog.Context context = dialog.getContext(); |
| LOG.debug("expression = " + context.input.expression); |
| |
| pc.addHistory(context.input); |
| |
| return context; |
| } |
| |
| /** |
| * <p>Process the result of an XPath query.</p> |
| * <p>If the result is a <code>java.util.List</code> object, iterate over all elements and |
| * add a highlighter object in the editor if the element is of type <code>PsiElement</code>. |
| * <p>If the result is a primitive value (String, Number, Boolean) a message box displaying |
| * the value will be displayed. </p> |
| * |
| * @param editor The editor object to apply the highlighting to |
| */ |
| private void highlightResult(XmlElement contextNode, @NotNull final Editor editor, final List<?> list) { |
| |
| final Config cfg = myComponent.getConfig(); |
| int lowestOffset = Integer.MAX_VALUE; |
| |
| for (final Object o : list) { |
| LOG.assertTrue(o != null, "null element?"); |
| |
| if (o instanceof PsiElement) { |
| final PsiElement element = (PsiElement)o; |
| |
| if (element.getContainingFile() == contextNode.getContainingFile()) { |
| lowestOffset = highlightElement(editor, element, cfg, lowestOffset); |
| } |
| } else { |
| LOG.info("Don't know what to do with " + o + " in a list context"); |
| } |
| LOG.debug("o = " + o); |
| } |
| |
| if (cfg.isScrollToFirst() && lowestOffset != Integer.MAX_VALUE) { |
| editor.getScrollingModel().scrollTo(editor.offsetToLogicalPosition(lowestOffset), ScrollType.MAKE_VISIBLE); |
| editor.getCaretModel().moveToOffset(lowestOffset); |
| } |
| |
| SwingUtilities.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| final StatusBar statusBar = WindowManager.getInstance().getStatusBar(editor.getProject()); |
| final String s = StringUtil.pluralize("match", list.size()); |
| statusBar.setInfo(list.size() + " XPath " + s + " found (press Escape to remove the highlighting)"); |
| } |
| }); |
| } |
| |
| private static int highlightElement(Editor editor, PsiElement element, Config cfg, int offset) { |
| final RangeHighlighter highlighter = HighlighterUtil.highlightNode(editor, element, cfg.getAttributes(), cfg); |
| HighlighterUtil.addHighlighter(editor, highlighter); |
| |
| return Math.min(highlighter.getStartOffset(), offset); |
| } |
| |
| public static class MyUsageTarget implements UsageTarget { |
| private final ItemPresentation myItemPresentation; |
| private final XmlElement myContextNode; |
| |
| public MyUsageTarget(String expression, XmlElement contextNode) { |
| myContextNode = contextNode; |
| myItemPresentation = new PresentationData(expression, null, null, null); |
| } |
| |
| @Override |
| public void findUsages() { |
| throw new IllegalArgumentException(); |
| } |
| |
| @Override |
| public void findUsagesInEditor(@NotNull FileEditor editor) { |
| throw new IllegalArgumentException(); |
| } |
| |
| @Override |
| public void highlightUsages(@NotNull PsiFile file, @NotNull Editor editor, boolean clearHighlights) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public boolean isValid() { |
| // re-run will become unavailable if the context node is invalid |
| return myContextNode == null || myContextNode.isValid(); |
| } |
| |
| @Override |
| public boolean isReadOnly() { |
| return true; |
| } |
| |
| @Override |
| @Nullable |
| public VirtualFile[] getFiles() { |
| return null; |
| } |
| |
| @Override |
| public void update() { |
| } |
| |
| @Override |
| public String getName() { |
| return "Expression"; |
| } |
| |
| @Override |
| public ItemPresentation getPresentation() { |
| return myItemPresentation; |
| } |
| |
| @Override |
| public void navigate(boolean requestFocus) { |
| } |
| |
| @Override |
| public boolean canNavigate() { |
| return false; |
| } |
| |
| @Override |
| public boolean canNavigateToSource() { |
| return false; |
| } |
| } |
| |
| private static class MyUsageSearcher implements UsageSearcher { |
| private final List<?> myResult; |
| private final XPath myXPath; |
| private final XmlElement myContextNode; |
| |
| public MyUsageSearcher(List<?> result, XPath xPath, XmlElement contextNode) { |
| myResult = result; |
| myXPath = xPath; |
| myContextNode = contextNode; |
| } |
| |
| @Override |
| public void generate(@NotNull final Processor<Usage> processor) { |
| Runnable runnable = new Runnable() { |
| @Override |
| @SuppressWarnings({"unchecked"}) |
| public void run() { |
| final List<?> list; |
| if (myResult.isEmpty()) { |
| try { |
| list = (List<?>)myXPath.selectNodes(myContextNode); |
| } catch (JaxenException e) { |
| LOG.debug(e); |
| Messages.showMessageDialog(myContextNode.getProject(), e.getMessage(), "XPath error", Messages.getErrorIcon()); |
| return; |
| } |
| } else { |
| list = myResult; |
| } |
| |
| final int size = list.size(); |
| final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); |
| indicator.setText("Collecting matches..."); |
| |
| Collections.sort(list, new Comparator() { |
| @Override |
| public int compare(Object o1, Object o2) { |
| indicator.checkCanceled(); |
| if (o1 instanceof PsiElement && o2 instanceof PsiElement) { |
| return ((PsiElement)o1).getTextRange().getStartOffset() - ((PsiElement)o2).getTextRange().getStartOffset(); |
| } else { |
| return String.valueOf(o1).compareTo(String.valueOf(o2)); |
| } |
| } |
| }); |
| for (int i = 0; i < size; i++) { |
| indicator.checkCanceled(); |
| Object o = list.get(i); |
| if (o instanceof PsiElement) { |
| final PsiElement element = (PsiElement)o; |
| processor.process(new UsageInfo2UsageAdapter(new UsageInfo(element))); |
| indicator.setText2(element.getContainingFile().getName()); |
| } |
| indicator.setFraction(i / (double)size); |
| } |
| list.clear(); |
| } |
| }; |
| ApplicationManager.getApplication().runReadAction(runnable); |
| } |
| } |
| |
| public abstract static class EditExpressionAction implements Runnable { |
| @Override |
| public void run() { |
| Runnable runnable = new Runnable() { |
| @Override |
| public void run() { |
| final Object o = saveState(); |
| try { |
| execute(); |
| } finally { |
| restoreState(o); |
| } |
| } |
| |
| }; |
| SwingUtilities.invokeLater(runnable); |
| } |
| |
| protected abstract void execute(); |
| |
| protected abstract Object saveState(); |
| |
| protected abstract void restoreState(Object o); |
| } |
| } |