| /* |
| * Copyright 2000-2010 JetBrains s.r.o. |
| * |
| * 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 com.intellij.xml.actions; |
| |
| import com.intellij.codeInsight.CodeInsightUtilBase; |
| import com.intellij.codeInsight.CodeInsightUtilCore; |
| import com.intellij.codeInsight.actions.SimpleCodeInsightAction; |
| import com.intellij.codeInsight.hint.HintManager; |
| import com.intellij.codeInsight.lookup.impl.LookupCellRenderer; |
| import com.intellij.codeInsight.template.TemplateBuilder; |
| import com.intellij.codeInsight.template.TemplateBuilderFactory; |
| import com.intellij.codeInsight.template.impl.MacroCallNode; |
| import com.intellij.codeInsight.template.macro.CompleteMacro; |
| import com.intellij.codeInsight.template.macro.CompleteSmartMacro; |
| import com.intellij.lang.ASTNode; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.command.WriteCommandAction; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.editor.colors.EditorColorsManager; |
| import com.intellij.openapi.editor.colors.EditorColorsScheme; |
| import com.intellij.openapi.editor.colors.EditorFontType; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.popup.JBPopupFactory; |
| import com.intellij.openapi.util.Condition; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.psi.PsiDocumentManager; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.XmlElementFactory; |
| import com.intellij.psi.impl.source.tree.Factory; |
| import com.intellij.psi.impl.source.tree.LeafElement; |
| import com.intellij.psi.impl.source.xml.XmlContentDFA; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.psi.xml.*; |
| import com.intellij.refactoring.util.CommonRefactoringUtil; |
| import com.intellij.ui.components.JBList; |
| import com.intellij.util.Function; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.xml.XmlAttributeDescriptor; |
| import com.intellij.xml.XmlElementDescriptor; |
| import com.intellij.xml.XmlElementsGroup; |
| import com.intellij.xml.impl.schema.XmlElementDescriptorImpl; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.awt.*; |
| import java.util.*; |
| import java.util.List; |
| |
| /** |
| * @author Dmitry Avdeev |
| */ |
| public class GenerateXmlTagAction extends SimpleCodeInsightAction { |
| |
| public static final ThreadLocal<String> TEST_THREAD_LOCAL = new ThreadLocal<String>(); |
| private final static Logger LOG = Logger.getInstance(GenerateXmlTagAction.class); |
| |
| @Override |
| public void invoke(@NotNull final Project project, @NotNull final Editor editor, @NotNull final PsiFile file) { |
| if (!CodeInsightUtilBase.prepareEditorForWrite(editor)) return; |
| try { |
| final XmlTag contextTag = getContextTag(editor, file); |
| if (contextTag == null) { |
| throw new CommonRefactoringUtil.RefactoringErrorHintException("Caret should be positioned inside a tag"); |
| } |
| XmlElementDescriptor currentTagDescriptor = contextTag.getDescriptor(); |
| assert currentTagDescriptor != null; |
| final XmlElementDescriptor[] descriptors = currentTagDescriptor.getElementsDescriptors(contextTag); |
| Arrays.sort(descriptors, new Comparator<XmlElementDescriptor>() { |
| @Override |
| public int compare(XmlElementDescriptor o1, XmlElementDescriptor o2) { |
| return o1.getName().compareTo(o2.getName()); |
| } |
| }); |
| final JBList list = new JBList(descriptors); |
| list.setCellRenderer(new MyListCellRenderer()); |
| Runnable runnable = new Runnable() { |
| @Override |
| public void run() { |
| final XmlElementDescriptor selected = (XmlElementDescriptor)list.getSelectedValue(); |
| new WriteCommandAction.Simple(project, "Generate XML Tag", file) { |
| @Override |
| protected void run() { |
| if (selected == null) return; |
| XmlTag newTag = createTag(contextTag, selected); |
| |
| PsiElement anchor = getAnchor(contextTag, editor, selected); |
| if (anchor == null) { // insert it in the cursor position |
| int offset = editor.getCaretModel().getOffset(); |
| Document document = editor.getDocument(); |
| document.insertString(offset, newTag.getText()); |
| PsiDocumentManager.getInstance(project).commitDocument(document); |
| newTag = PsiTreeUtil.getParentOfType(file.findElementAt(offset + 1), XmlTag.class, false); |
| } |
| else { |
| newTag = (XmlTag)contextTag.addAfter(newTag, anchor); |
| } |
| if (newTag != null) { |
| generateTag(newTag, editor); |
| } |
| } |
| }.execute(); |
| } |
| }; |
| if (ApplicationManager.getApplication().isUnitTestMode()) { |
| XmlElementDescriptor descriptor = ContainerUtil.find(descriptors, new Condition<XmlElementDescriptor>() { |
| @Override |
| public boolean value(XmlElementDescriptor xmlElementDescriptor) { |
| return xmlElementDescriptor.getName().equals(TEST_THREAD_LOCAL.get()); |
| } |
| }); |
| list.setSelectedValue(descriptor, false); |
| runnable.run(); |
| } |
| else { |
| JBPopupFactory.getInstance().createListPopupBuilder(list) |
| .setTitle("Choose Tag Name") |
| .setItemChoosenCallback(runnable) |
| .setFilteringEnabled(new Function<Object, String>() { |
| @Override |
| public String fun(Object o) { |
| return ((XmlElementDescriptor)o).getName(); |
| } |
| }) |
| .createPopup() |
| .showInBestPositionFor(editor); |
| } |
| } |
| catch (CommonRefactoringUtil.RefactoringErrorHintException e) { |
| HintManager.getInstance().showErrorHint(editor, e.getMessage()); |
| } |
| } |
| |
| @Nullable |
| private static XmlTag getAnchor(@NotNull XmlTag contextTag, Editor editor, XmlElementDescriptor selected) { |
| XmlContentDFA contentDFA = XmlContentDFA.getContentDFA(contextTag); |
| int offset = editor.getCaretModel().getOffset(); |
| if (contentDFA == null) { |
| return null; |
| } |
| XmlTag anchor = null; |
| boolean previousPositionIsPossible = true; |
| for (XmlTag subTag : contextTag.getSubTags()) { |
| if (contentDFA.getPossibleElements().contains(selected)) { |
| if (subTag.getTextOffset() > offset) { |
| break; |
| } |
| anchor = subTag; |
| previousPositionIsPossible = true; |
| } |
| else { |
| previousPositionIsPossible = false; |
| } |
| contentDFA.transition(subTag); |
| } |
| return previousPositionIsPossible ? null : anchor; |
| } |
| |
| public static void generateTag(@NotNull XmlTag newTag, Editor editor) { |
| generateRaw(newTag); |
| final XmlTag restored = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(newTag); |
| if (restored == null) { |
| LOG.error("Could not restore tag: " + newTag.getText()); |
| } |
| TemplateBuilder builder = TemplateBuilderFactory.getInstance().createTemplateBuilder(restored); |
| replaceElements(restored, builder); |
| builder.run(editor, false); |
| } |
| |
| private static void generateRaw(final @NotNull XmlTag newTag) { |
| XmlElementDescriptor selected = newTag.getDescriptor(); |
| if (selected == null) return; |
| switch (selected.getContentType()) { |
| case XmlElementDescriptor.CONTENT_TYPE_EMPTY: |
| newTag.collapseIfEmpty(); |
| ASTNode node = newTag.getNode(); |
| assert node != null; |
| ASTNode elementEnd = node.findChildByType(XmlTokenType.XML_EMPTY_ELEMENT_END); |
| if (elementEnd == null) { |
| LeafElement emptyTagEnd = Factory.createSingleLeafElement(XmlTokenType.XML_EMPTY_ELEMENT_END, "/>", 0, 2, null, newTag.getManager()); |
| node.addChild(emptyTagEnd); |
| } |
| break; |
| case XmlElementDescriptor.CONTENT_TYPE_MIXED: |
| newTag.getValue().setText(""); |
| } |
| for (XmlAttributeDescriptor descriptor : selected.getAttributesDescriptors(newTag)) { |
| if (descriptor.isRequired()) { |
| newTag.setAttribute(descriptor.getName(), ""); |
| } |
| } |
| List<XmlElementDescriptor> tags = getRequiredSubTags(selected); |
| for (XmlElementDescriptor descriptor : tags) { |
| if (descriptor == null) { |
| XmlTag tag = XmlElementFactory.getInstance(newTag.getProject()).createTagFromText("<", newTag.getLanguage()); |
| newTag.addSubTag(tag, false); |
| } |
| else { |
| XmlTag subTag = newTag.addSubTag(createTag(newTag, descriptor), false); |
| generateRaw(subTag); |
| } |
| } |
| } |
| |
| public static List<XmlElementDescriptor> getRequiredSubTags(XmlElementDescriptor selected) { |
| XmlElementsGroup topGroup = selected.getTopGroup(); |
| if (topGroup == null) return Collections.emptyList(); |
| return computeRequiredSubTags(topGroup); |
| } |
| |
| private static void replaceElements(XmlTag tag, TemplateBuilder builder) { |
| for (XmlAttribute attribute : tag.getAttributes()) { |
| XmlAttributeValue value = attribute.getValueElement(); |
| if (value != null) { |
| builder.replaceElement(value, TextRange.from(1, 0), new MacroCallNode(new CompleteMacro())); |
| } |
| } |
| if ("<".equals(tag.getText())) { |
| builder.replaceElement(tag, TextRange.from(1, 0), new MacroCallNode(new CompleteSmartMacro())); |
| } |
| else if (tag.getSubTags().length == 0) { |
| int i = tag.getText().indexOf("></"); |
| if (i > 0) { |
| builder.replaceElement(tag, TextRange.from(i + 1, 0), new MacroCallNode(new CompleteMacro())); |
| } |
| } |
| for (XmlTag subTag : tag.getSubTags()) { |
| replaceElements(subTag, builder); |
| } |
| } |
| |
| private static XmlTag createTag(@NotNull XmlTag contextTag, @NotNull XmlElementDescriptor descriptor) { |
| String namespace = getNamespace(descriptor); |
| XmlTag tag = contextTag.createChildTag(descriptor.getName(), namespace, null, false); |
| PsiElement lastChild = tag.getLastChild(); |
| assert lastChild != null; |
| lastChild.delete(); // remove XML_EMPTY_ELEMENT_END |
| return tag; |
| } |
| |
| private static String getNamespace(XmlElementDescriptor descriptor) { |
| return descriptor instanceof XmlElementDescriptorImpl ? ((XmlElementDescriptorImpl)descriptor).getNamespace() : ""; |
| } |
| |
| @Nullable |
| private static XmlTag getContextTag(Editor editor, PsiFile file) { |
| PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); |
| XmlTag tag = null; |
| if (element != null) { |
| tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); |
| } |
| if (tag == null) { |
| tag = ((XmlFile)file).getRootTag(); |
| } |
| return tag; |
| } |
| |
| private static List<XmlElementDescriptor> computeRequiredSubTags(XmlElementsGroup group) { |
| |
| if (group.getMinOccurs() < 1) return Collections.emptyList(); |
| switch (group.getGroupType()) { |
| case LEAF: |
| XmlElementDescriptor descriptor = group.getLeafDescriptor(); |
| return descriptor == null ? Collections.<XmlElementDescriptor>emptyList() : Collections.singletonList(descriptor); |
| case CHOICE: |
| LinkedHashSet<XmlElementDescriptor> set = null; |
| for (XmlElementsGroup subGroup : group.getSubGroups()) { |
| List<XmlElementDescriptor> descriptors = computeRequiredSubTags(subGroup); |
| if (set == null) { |
| set = new LinkedHashSet<XmlElementDescriptor>(descriptors); |
| } |
| else { |
| set.retainAll(descriptors); |
| } |
| } |
| if (set == null || set.isEmpty()) { |
| return Collections.singletonList(null); // placeholder for smart completion |
| } |
| return new ArrayList<XmlElementDescriptor>(set); |
| |
| default: |
| ArrayList<XmlElementDescriptor> list = new ArrayList<XmlElementDescriptor>(); |
| for (XmlElementsGroup subGroup : group.getSubGroups()) { |
| list.addAll(computeRequiredSubTags(subGroup)); |
| } |
| return list; |
| } |
| } |
| |
| @Override |
| protected boolean isValidForFile(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) { |
| if (!(file instanceof XmlFile)) return false; |
| XmlTag contextTag = getContextTag(editor, file); |
| return contextTag != null && contextTag.getDescriptor() != null; |
| } |
| |
| @Override |
| public boolean startInWriteAction() { |
| return false; |
| } |
| |
| private static class MyListCellRenderer implements ListCellRenderer { |
| private final JPanel myPanel; |
| private final JLabel myNameLabel; |
| private final JLabel myNSLabel; |
| |
| public MyListCellRenderer() { |
| myPanel = new JPanel(new BorderLayout()); |
| myPanel.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 0)); |
| myNameLabel = new JLabel(); |
| |
| myPanel.add(myNameLabel, BorderLayout.WEST); |
| myPanel.add(new JLabel(" ")); |
| myNSLabel = new JLabel(); |
| myPanel.add(myNSLabel, BorderLayout.EAST); |
| |
| EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme(); |
| Font font = scheme.getFont(EditorFontType.PLAIN); |
| myNameLabel.setFont(font); |
| myNSLabel.setFont(font); |
| } |
| |
| @Override |
| public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { |
| |
| XmlElementDescriptor descriptor = (XmlElementDescriptor)value; |
| Color backgroundColor = isSelected ? list.getSelectionBackground() : list.getBackground(); |
| |
| myNameLabel.setText(descriptor.getName()); |
| myNameLabel.setForeground(isSelected ? list.getSelectionForeground() : list.getForeground()); |
| myPanel.setBackground(backgroundColor); |
| |
| myNSLabel.setText(getNamespace(descriptor)); |
| myNSLabel.setForeground(LookupCellRenderer.getGrayedForeground(isSelected)); |
| myNSLabel.setBackground(backgroundColor); |
| |
| return myPanel; |
| } |
| } |
| } |