Merge branch 'da/dumb_run'
[idea/community.git] / xml / impl / src / com / intellij / codeInsight / completion / XmlTagInsertHandler.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
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 com.intellij.codeInsight.completion;
17
18 import com.intellij.application.options.editor.WebEditorOptions;
19 import com.intellij.codeInsight.TailType;
20 import com.intellij.codeInsight.daemon.impl.quickfix.EmptyExpression;
21 import com.intellij.codeInsight.editorActions.XmlEditUtil;
22 import com.intellij.codeInsight.lookup.Lookup;
23 import com.intellij.codeInsight.lookup.LookupElement;
24 import com.intellij.codeInsight.lookup.LookupItem;
25 import com.intellij.codeInsight.template.Template;
26 import com.intellij.codeInsight.template.TemplateEditingAdapter;
27 import com.intellij.codeInsight.template.TemplateManager;
28 import com.intellij.codeInsight.template.impl.MacroCallNode;
29 import com.intellij.codeInsight.template.macro.CompleteMacro;
30 import com.intellij.codeInsight.template.macro.CompleteSmartMacro;
31 import com.intellij.codeInspection.InspectionProfile;
32 import com.intellij.codeInspection.htmlInspections.XmlEntitiesInspection;
33 import com.intellij.lang.ASTNode;
34 import com.intellij.openapi.command.WriteCommandAction;
35 import com.intellij.openapi.command.undo.UndoManager;
36 import com.intellij.openapi.editor.Editor;
37 import com.intellij.openapi.editor.RangeMarker;
38 import com.intellij.openapi.editor.ScrollType;
39 import com.intellij.openapi.project.Project;
40 import com.intellij.openapi.util.Key;
41 import com.intellij.openapi.util.text.StringUtil;
42 import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
43 import com.intellij.psi.PsiDocumentManager;
44 import com.intellij.psi.PsiElement;
45 import com.intellij.psi.PsiFile;
46 import com.intellij.psi.codeStyle.CodeStyleSettings;
47 import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
48 import com.intellij.psi.formatter.xml.XmlCodeStyleSettings;
49 import com.intellij.psi.html.HtmlTag;
50 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
51 import com.intellij.psi.util.PsiTreeUtil;
52 import com.intellij.psi.xml.XmlTag;
53 import com.intellij.psi.xml.XmlTokenType;
54 import com.intellij.xml.*;
55 import com.intellij.xml.XmlExtension.AttributeValuePresentation;
56 import com.intellij.xml.actions.GenerateXmlTagAction;
57 import com.intellij.xml.impl.schema.XmlElementDescriptorImpl;
58 import com.intellij.xml.util.HtmlUtil;
59 import com.intellij.xml.util.XmlUtil;
60 import org.jetbrains.annotations.NotNull;
61 import org.jetbrains.annotations.Nullable;
62
63 import java.util.*;
64
65 public class XmlTagInsertHandler implements InsertHandler<LookupElement> {
66   public static final Key<Boolean> ENFORCING_TAG = Key.create("xml.insert.handler.enforcing.tag");
67   public static final XmlTagInsertHandler INSTANCE = new XmlTagInsertHandler();
68
69   @Override
70   public void handleInsert(InsertionContext context, LookupElement item) {
71     Project project = context.getProject();
72     Editor editor = context.getEditor();
73     // Need to insert " " to prevent creating tags like <tagThis is my text
74     InjectedLanguageUtil.getTopLevelEditor(editor).getDocument().putUserData(ENFORCING_TAG, Boolean.TRUE);
75     final int offset = editor.getCaretModel().getOffset();
76     editor.getDocument().insertString(offset, " ");
77     PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
78     PsiElement current = context.getFile().findElementAt(context.getStartOffset());
79     editor.getDocument().deleteString(offset, offset + 1);
80     InjectedLanguageUtil.getTopLevelEditor(editor).getDocument().putUserData(ENFORCING_TAG, null);
81
82     final XmlTag tag = PsiTreeUtil.getContextOfType(current, XmlTag.class, true);
83
84     if (tag == null) return;
85
86     if (context.getCompletionChar() != Lookup.COMPLETE_STATEMENT_SELECT_CHAR) {
87       context.setAddCompletionChar(false);
88     }
89
90     final XmlElementDescriptor descriptor = tag.getDescriptor();
91
92     if (XmlUtil.getTokenOfType(tag, XmlTokenType.XML_TAG_END) == null &&
93         XmlUtil.getTokenOfType(tag, XmlTokenType.XML_EMPTY_ELEMENT_END) == null) {
94
95       if (descriptor != null) {
96         insertIncompleteTag(context.getCompletionChar(), editor, tag);
97       }
98     }
99     else if (context.getCompletionChar() == Lookup.REPLACE_SELECT_CHAR) {
100       PsiDocumentManager.getInstance(project).commitAllDocuments();
101
102       int caretOffset = editor.getCaretModel().getOffset();
103
104       PsiElement otherTag = PsiTreeUtil.getParentOfType(context.getFile().findElementAt(caretOffset), XmlTag.class);
105
106       PsiElement endTagStart = XmlUtil.getTokenOfType(otherTag, XmlTokenType.XML_END_TAG_START);
107
108       if (endTagStart != null) {
109         PsiElement sibling = endTagStart.getNextSibling();
110
111         assert sibling != null;
112         ASTNode node = sibling.getNode();
113         assert node != null;
114         if (node.getElementType() == XmlTokenType.XML_NAME) {
115           int sOffset = sibling.getTextRange().getStartOffset();
116           int eOffset = sibling.getTextRange().getEndOffset();
117
118           editor.getDocument().deleteString(sOffset, eOffset);
119           editor.getDocument().insertString(sOffset, ((XmlTag)otherTag).getName());
120         }
121       }
122
123       editor.getCaretModel().moveToOffset(caretOffset + 1);
124       editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
125       editor.getSelectionModel().removeSelection();
126     }
127
128     if (context.getCompletionChar() == ' ' && TemplateManager.getInstance(project).getActiveTemplate(editor) != null) {
129       return;
130     }
131
132     final TailType tailType = LookupItem.handleCompletionChar(editor, item, context.getCompletionChar());
133     tailType.processTail(editor, editor.getCaretModel().getOffset());
134   }
135
136   public static void insertIncompleteTag(char completionChar,
137                                           final Editor editor,
138                                           XmlTag tag) {
139     XmlElementDescriptor descriptor = tag.getDescriptor();
140     final Project project = editor.getProject();
141     TemplateManager templateManager = TemplateManager.getInstance(project);
142     Template template = templateManager.createTemplate("", "");
143
144     template.setToIndent(true);
145
146     // temp code
147     PsiFile containingFile = tag.getContainingFile();
148     boolean htmlCode = HtmlUtil.hasHtml(containingFile) || HtmlUtil.supportsXmlTypedHandlers(containingFile);
149     template.setToReformat(!htmlCode);
150
151     StringBuilder indirectRequiredAttrs = addRequiredAttributes(descriptor, tag, template, containingFile);
152     final boolean chooseAttributeName = addTail(completionChar, descriptor, htmlCode, tag, template, indirectRequiredAttrs);
153
154     templateManager.startTemplate(editor, template, new TemplateEditingAdapter() {
155       private RangeMarker myAttrValueMarker;
156
157       @Override
158       public void waitingForInput(Template template) {
159         int offset = editor.getCaretModel().getOffset();
160         myAttrValueMarker = editor.getDocument().createRangeMarker(offset + 1, offset + 4);
161       }
162
163       @Override
164       public void templateFinished(final Template template, boolean brokenOff) {
165         final int offset = editor.getCaretModel().getOffset();
166
167         if (chooseAttributeName && offset > 0) {
168           char c = editor.getDocument().getCharsSequence().charAt(offset - 1);
169           if (c == '/' || (c == ' ' && brokenOff)) {
170             new WriteCommandAction.Simple(project) {
171               @Override
172               protected void run() throws Throwable {
173                 editor.getDocument().replaceString(offset, offset + 3, ">");
174               }
175             }.execute();
176           }
177         }
178       }
179
180       @Override
181       public void templateCancelled(final Template template) {
182         if (myAttrValueMarker == null) {
183           return;
184         }
185
186         final UndoManager manager = UndoManager.getInstance(project);
187         if (manager.isUndoInProgress() || manager.isRedoInProgress()) {
188           return;
189         }
190
191         if (chooseAttributeName && myAttrValueMarker.isValid()) {
192           final int startOffset = myAttrValueMarker.getStartOffset();
193           final int endOffset = myAttrValueMarker.getEndOffset();
194           new WriteCommandAction.Simple(project) {
195             @Override
196             protected void run() throws Throwable {
197               editor.getDocument().replaceString(startOffset, endOffset, ">");
198             }
199           }.execute();
200         }
201       }
202     });
203   }
204
205   @Nullable
206   private static StringBuilder addRequiredAttributes(XmlElementDescriptor descriptor,
207                                                      @Nullable XmlTag tag,
208                                                      Template template,
209                                                      PsiFile containingFile) {
210
211     boolean htmlCode = HtmlUtil.hasHtml(containingFile) || HtmlUtil.supportsXmlTypedHandlers(containingFile);
212     Set<String> notRequiredAttributes = Collections.emptySet();
213
214     if (tag instanceof HtmlTag) {
215       final InspectionProfile profile = InspectionProjectProfileManager.getInstance(tag.getProject()).getCurrentProfile();
216       XmlEntitiesInspection inspection = (XmlEntitiesInspection)profile.getUnwrappedTool(
217         XmlEntitiesInspection.REQUIRED_ATTRIBUTES_SHORT_NAME, tag);
218
219       if (inspection != null) {
220         StringTokenizer tokenizer = new StringTokenizer(inspection.getAdditionalEntries());
221         notRequiredAttributes = new HashSet<>();
222
223         while(tokenizer.hasMoreElements()) notRequiredAttributes.add(tokenizer.nextToken());
224       }
225     }
226
227     XmlAttributeDescriptor[] attributes = descriptor.getAttributesDescriptors(tag);
228     StringBuilder indirectRequiredAttrs = null;
229
230     if (WebEditorOptions.getInstance().isAutomaticallyInsertRequiredAttributes()) {
231       final XmlExtension extension = XmlExtension.getExtension(containingFile);
232
233       for (XmlAttributeDescriptor attributeDecl : attributes) {
234         String attributeName = attributeDecl.getName(tag);
235
236         boolean shouldBeInserted = extension.shouldBeInserted(attributeDecl);
237         if (!shouldBeInserted) continue;
238
239         AttributeValuePresentation presenter =
240           extension.getAttributeValuePresentation(attributeDecl, XmlEditUtil.getAttributeQuote(htmlCode));
241
242         if (tag == null || tag.getAttributeValue(attributeName) == null) {
243           if (!notRequiredAttributes.contains(attributeName)) {
244             if (!extension.isIndirectSyntax(attributeDecl)) {
245               template.addTextSegment(" " + attributeName + "=" + presenter.getPrefix());
246               template.addVariable(presenter.showAutoPopup() ? new MacroCallNode(new CompleteMacro()) : new EmptyExpression(), true);
247               template.addTextSegment(presenter.getPostfix());
248             }
249             else {
250               if (indirectRequiredAttrs == null) indirectRequiredAttrs = new StringBuilder();
251               indirectRequiredAttrs.append("\n<jsp:attribute name=\"").append(attributeName).append("\"></jsp:attribute>\n");
252             }
253           }
254         }
255         else if (attributeDecl.isFixed() && attributeDecl.getDefaultValue() != null && !htmlCode) {
256           template.addTextSegment(" " + attributeName + "=" +
257                                   presenter.getPrefix() + attributeDecl.getDefaultValue() + presenter.getPostfix());
258         }
259       }
260     }
261     return indirectRequiredAttrs;
262   }
263
264   protected static boolean addTail(char completionChar,
265                                    XmlElementDescriptor descriptor,
266                                    boolean isHtmlCode,
267                                    XmlTag tag,
268                                    Template template,
269                                    StringBuilder indirectRequiredAttrs) {
270     boolean htmlCode = HtmlUtil.hasHtml(tag.getContainingFile()) || HtmlUtil.supportsXmlTypedHandlers(tag.getContainingFile());
271
272     if (completionChar == '>' || (completionChar == '/' && indirectRequiredAttrs != null)) {
273       template.addTextSegment(">");
274       boolean toInsertCDataEnd = false;
275
276       if (descriptor instanceof XmlElementDescriptorWithCDataContent) {
277         final XmlElementDescriptorWithCDataContent cDataContainer = (XmlElementDescriptorWithCDataContent)descriptor;
278
279         if (cDataContainer.requiresCdataBracesInContext(tag)) {
280           template.addTextSegment("<![CDATA[\n");
281           toInsertCDataEnd = true;
282         }
283       }
284
285       if (indirectRequiredAttrs != null) template.addTextSegment(indirectRequiredAttrs.toString());
286       template.addEndVariable();
287
288       if (toInsertCDataEnd) template.addTextSegment("\n]]>");
289
290       if ((!(tag instanceof HtmlTag) || !HtmlUtil.isSingleHtmlTag(tag.getName())) && tag.getAttributes().length == 0) {
291         if (WebEditorOptions.getInstance().isAutomaticallyInsertClosingTag()) {
292           final String name = descriptor.getName(tag);
293           if (name != null) {
294             template.addTextSegment("</");
295             template.addTextSegment(name);
296             template.addTextSegment(">");
297           }
298         }
299       }
300     }
301     else if (completionChar == '/') {
302       template.addTextSegment(closeTag(tag));
303     }
304     else if (completionChar == ' ' && template.getSegmentsCount() == 0) {
305       if (WebEditorOptions.getInstance().isAutomaticallyStartAttribute() &&
306           (descriptor.getAttributesDescriptors(tag).length > 0 || isTagFromHtml(tag) && !HtmlUtil.isTagWithoutAttributes(tag.getName()))) {
307         completeAttribute(template, htmlCode);
308         return true;
309       }
310     }
311     else if (completionChar == Lookup.AUTO_INSERT_SELECT_CHAR || completionChar == Lookup.NORMAL_SELECT_CHAR || completionChar == Lookup.REPLACE_SELECT_CHAR) {
312       if (WebEditorOptions.getInstance().isAutomaticallyInsertClosingTag() && isHtmlCode && HtmlUtil.isSingleHtmlTag(tag.getName())) {
313         template.addTextSegment(HtmlUtil.isHtmlTag(tag) ? ">" : closeTag(tag));
314       }
315       else {
316         if (needAlLeastOneAttribute(tag) && WebEditorOptions.getInstance().isAutomaticallyStartAttribute() && tag.getAttributes().length == 0
317             && template.getSegmentsCount() == 0) {
318           completeAttribute(template, htmlCode);
319           return true;
320         }
321         else {
322           completeTagTail(template, descriptor, tag.getContainingFile(), tag, true);
323         }
324       }
325     }
326
327     return false;
328   }
329
330   @NotNull
331   private static String closeTag(XmlTag tag) {
332     CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(tag.getProject());
333     boolean html = HtmlUtil.isHtmlTag(tag);
334     boolean needsSpace = (html && settings.HTML_SPACE_INSIDE_EMPTY_TAG) ||
335                          (!html && settings.getCustomSettings(XmlCodeStyleSettings.class).XML_SPACE_INSIDE_EMPTY_TAG);
336     return needsSpace ? " />" : "/>";
337   }
338
339   private static void completeAttribute(Template template, boolean htmlCode) {
340     template.addTextSegment(" ");
341     template.addVariable(new MacroCallNode(new CompleteMacro()), true);
342     template.addTextSegment("=" + XmlEditUtil.getAttributeQuote(htmlCode));
343     template.addEndVariable();
344     template.addTextSegment(XmlEditUtil.getAttributeQuote(htmlCode));
345   }
346
347   private static boolean needAlLeastOneAttribute(XmlTag tag) {
348     for (XmlTagRuleProvider ruleProvider : XmlTagRuleProvider.EP_NAME.getExtensions()) {
349       for (XmlTagRuleProvider.Rule rule : ruleProvider.getTagRule(tag)) {
350         if (rule.needAtLeastOneAttribute(tag)) {
351           return true;
352         }
353       }
354     }
355
356     return false;
357   }
358
359   private static boolean addRequiredSubTags(Template template, XmlElementDescriptor descriptor, PsiFile file, XmlTag context) {
360
361     if (!WebEditorOptions.getInstance().isAutomaticallyInsertRequiredSubTags()) return false;
362     List<XmlElementDescriptor> requiredSubTags = GenerateXmlTagAction.getRequiredSubTags(descriptor);
363     if (!requiredSubTags.isEmpty()) {
364       template.addTextSegment(">");
365       template.setToReformat(true);
366     }
367     for (XmlElementDescriptor subTag : requiredSubTags) {
368       if (subTag == null) { // placeholder for smart completion
369         template.addTextSegment("<");
370         template.addVariable(new MacroCallNode(new CompleteSmartMacro()), true);
371         continue;
372       }
373       String qname = subTag.getName();
374       if (subTag instanceof XmlElementDescriptorImpl) {
375         String prefixByNamespace = context.getPrefixByNamespace(((XmlElementDescriptorImpl)subTag).getNamespace());
376         if (StringUtil.isNotEmpty(prefixByNamespace)) {
377           qname = prefixByNamespace + ":" + subTag.getName();
378         }
379       }
380       template.addTextSegment("<" + qname);
381       addRequiredAttributes(subTag, null, template, file);
382       completeTagTail(template, subTag, file, context, false);
383     }
384     if (!requiredSubTags.isEmpty()) {
385       addTagEnd(template, descriptor, context);
386     }
387     return !requiredSubTags.isEmpty();
388   }
389
390   private static void completeTagTail(Template template, XmlElementDescriptor descriptor, PsiFile file, XmlTag context, boolean firstLevel) {
391     boolean completeIt = !firstLevel || descriptor.getAttributesDescriptors(null).length == 0;
392     switch (descriptor.getContentType()) {
393       case XmlElementDescriptor.CONTENT_TYPE_UNKNOWN:
394         return;
395       case XmlElementDescriptor.CONTENT_TYPE_EMPTY:
396         if (completeIt) {
397           template.addTextSegment(closeTag(context));
398         }
399         break;
400       case XmlElementDescriptor.CONTENT_TYPE_MIXED:
401          if (completeIt) {
402            template.addTextSegment(">");
403            if (firstLevel) {
404              template.addEndVariable();
405            }
406            else {
407              template.addVariable(new MacroCallNode(new CompleteMacro()), true);
408            }
409            addTagEnd(template, descriptor, context);
410          }
411          break;
412        default:
413          if (!addRequiredSubTags(template, descriptor, file, context)) {
414            if (completeIt) {
415              template.addTextSegment(">");
416              template.addEndVariable();
417              addTagEnd(template, descriptor, context);
418            }
419          }
420          break;
421     }
422   }
423
424   private static void addTagEnd(Template template, XmlElementDescriptor descriptor, XmlTag context) {
425     template.addTextSegment("</" + descriptor.getName(context) + ">");
426   }
427
428   private static boolean isTagFromHtml(final XmlTag tag) {
429     final String ns = tag.getNamespace();
430     return XmlUtil.XHTML_URI.equals(ns) || XmlUtil.HTML_URI.equals(ns);
431   }
432 }