2 * Copyright 2000-2017 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.codeInsight.completion;
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;
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();
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);
82 final XmlTag tag = PsiTreeUtil.getContextOfType(current, XmlTag.class, true);
84 if (tag == null) return;
86 if (context.getCompletionChar() != Lookup.COMPLETE_STATEMENT_SELECT_CHAR) {
87 context.setAddCompletionChar(false);
90 final XmlElementDescriptor descriptor = tag.getDescriptor();
92 if (XmlUtil.getTokenOfType(tag, XmlTokenType.XML_TAG_END) == null &&
93 XmlUtil.getTokenOfType(tag, XmlTokenType.XML_EMPTY_ELEMENT_END) == null) {
95 if (descriptor != null) {
96 insertIncompleteTag(context.getCompletionChar(), editor, tag);
99 else if (context.getCompletionChar() == Lookup.REPLACE_SELECT_CHAR) {
100 PsiDocumentManager.getInstance(project).commitAllDocuments();
102 int caretOffset = editor.getCaretModel().getOffset();
104 PsiElement otherTag = PsiTreeUtil.getParentOfType(context.getFile().findElementAt(caretOffset), XmlTag.class);
106 PsiElement endTagStart = XmlUtil.getTokenOfType(otherTag, XmlTokenType.XML_END_TAG_START);
108 if (endTagStart != null) {
109 PsiElement sibling = endTagStart.getNextSibling();
111 assert sibling != null;
112 ASTNode node = sibling.getNode();
114 if (node.getElementType() == XmlTokenType.XML_NAME) {
115 int sOffset = sibling.getTextRange().getStartOffset();
116 int eOffset = sibling.getTextRange().getEndOffset();
118 editor.getDocument().deleteString(sOffset, eOffset);
119 editor.getDocument().insertString(sOffset, ((XmlTag)otherTag).getName());
123 editor.getCaretModel().moveToOffset(caretOffset + 1);
124 editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
125 editor.getSelectionModel().removeSelection();
128 if (context.getCompletionChar() == ' ' && TemplateManager.getInstance(project).getActiveTemplate(editor) != null) {
132 final TailType tailType = LookupItem.handleCompletionChar(editor, item, context.getCompletionChar());
133 tailType.processTail(editor, editor.getCaretModel().getOffset());
136 public static void insertIncompleteTag(char completionChar,
139 XmlElementDescriptor descriptor = tag.getDescriptor();
140 final Project project = editor.getProject();
141 TemplateManager templateManager = TemplateManager.getInstance(project);
142 Template template = templateManager.createTemplate("", "");
144 template.setToIndent(true);
147 PsiFile containingFile = tag.getContainingFile();
148 boolean htmlCode = HtmlUtil.hasHtml(containingFile) || HtmlUtil.supportsXmlTypedHandlers(containingFile);
149 template.setToReformat(!htmlCode);
151 StringBuilder indirectRequiredAttrs = addRequiredAttributes(descriptor, tag, template, containingFile);
152 final boolean chooseAttributeName = addTail(completionChar, descriptor, htmlCode, tag, template, indirectRequiredAttrs);
154 templateManager.startTemplate(editor, template, new TemplateEditingAdapter() {
155 private RangeMarker myAttrValueMarker;
158 public void waitingForInput(Template template) {
159 int offset = editor.getCaretModel().getOffset();
160 myAttrValueMarker = editor.getDocument().createRangeMarker(offset + 1, offset + 4);
164 public void templateFinished(final Template template, boolean brokenOff) {
165 final int offset = editor.getCaretModel().getOffset();
167 if (chooseAttributeName && offset > 0) {
168 char c = editor.getDocument().getCharsSequence().charAt(offset - 1);
169 if (c == '/' || (c == ' ' && brokenOff)) {
170 new WriteCommandAction.Simple(project) {
172 protected void run() throws Throwable {
173 editor.getDocument().replaceString(offset, offset + 3, ">");
181 public void templateCancelled(final Template template) {
182 if (myAttrValueMarker == null) {
186 final UndoManager manager = UndoManager.getInstance(project);
187 if (manager.isUndoInProgress() || manager.isRedoInProgress()) {
191 if (chooseAttributeName && myAttrValueMarker.isValid()) {
192 final int startOffset = myAttrValueMarker.getStartOffset();
193 final int endOffset = myAttrValueMarker.getEndOffset();
194 new WriteCommandAction.Simple(project) {
196 protected void run() throws Throwable {
197 editor.getDocument().replaceString(startOffset, endOffset, ">");
206 private static StringBuilder addRequiredAttributes(XmlElementDescriptor descriptor,
207 @Nullable XmlTag tag,
209 PsiFile containingFile) {
211 boolean htmlCode = HtmlUtil.hasHtml(containingFile) || HtmlUtil.supportsXmlTypedHandlers(containingFile);
212 Set<String> notRequiredAttributes = Collections.emptySet();
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);
219 if (inspection != null) {
220 StringTokenizer tokenizer = new StringTokenizer(inspection.getAdditionalEntries());
221 notRequiredAttributes = new HashSet<>();
223 while(tokenizer.hasMoreElements()) notRequiredAttributes.add(tokenizer.nextToken());
227 XmlAttributeDescriptor[] attributes = descriptor.getAttributesDescriptors(tag);
228 StringBuilder indirectRequiredAttrs = null;
230 if (WebEditorOptions.getInstance().isAutomaticallyInsertRequiredAttributes()) {
231 final XmlExtension extension = XmlExtension.getExtension(containingFile);
233 for (XmlAttributeDescriptor attributeDecl : attributes) {
234 String attributeName = attributeDecl.getName(tag);
236 boolean shouldBeInserted = extension.shouldBeInserted(attributeDecl);
237 if (!shouldBeInserted) continue;
239 AttributeValuePresentation presenter =
240 extension.getAttributeValuePresentation(attributeDecl, XmlEditUtil.getAttributeQuote(htmlCode));
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());
250 if (indirectRequiredAttrs == null) indirectRequiredAttrs = new StringBuilder();
251 indirectRequiredAttrs.append("\n<jsp:attribute name=\"").append(attributeName).append("\"></jsp:attribute>\n");
255 else if (attributeDecl.isFixed() && attributeDecl.getDefaultValue() != null && !htmlCode) {
256 template.addTextSegment(" " + attributeName + "=" +
257 presenter.getPrefix() + attributeDecl.getDefaultValue() + presenter.getPostfix());
261 return indirectRequiredAttrs;
264 protected static boolean addTail(char completionChar,
265 XmlElementDescriptor descriptor,
269 StringBuilder indirectRequiredAttrs) {
270 boolean htmlCode = HtmlUtil.hasHtml(tag.getContainingFile()) || HtmlUtil.supportsXmlTypedHandlers(tag.getContainingFile());
272 if (completionChar == '>' || (completionChar == '/' && indirectRequiredAttrs != null)) {
273 template.addTextSegment(">");
274 boolean toInsertCDataEnd = false;
276 if (descriptor instanceof XmlElementDescriptorWithCDataContent) {
277 final XmlElementDescriptorWithCDataContent cDataContainer = (XmlElementDescriptorWithCDataContent)descriptor;
279 if (cDataContainer.requiresCdataBracesInContext(tag)) {
280 template.addTextSegment("<![CDATA[\n");
281 toInsertCDataEnd = true;
285 if (indirectRequiredAttrs != null) template.addTextSegment(indirectRequiredAttrs.toString());
286 template.addEndVariable();
288 if (toInsertCDataEnd) template.addTextSegment("\n]]>");
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);
294 template.addTextSegment("</");
295 template.addTextSegment(name);
296 template.addTextSegment(">");
301 else if (completionChar == '/') {
302 template.addTextSegment(closeTag(tag));
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);
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));
316 if (needAlLeastOneAttribute(tag) && WebEditorOptions.getInstance().isAutomaticallyStartAttribute() && tag.getAttributes().length == 0
317 && template.getSegmentsCount() == 0) {
318 completeAttribute(template, htmlCode);
322 completeTagTail(template, descriptor, tag.getContainingFile(), tag, true);
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 ? " />" : "/>";
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));
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)) {
359 private static boolean addRequiredSubTags(Template template, XmlElementDescriptor descriptor, PsiFile file, XmlTag context) {
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);
367 for (XmlElementDescriptor subTag : requiredSubTags) {
368 if (subTag == null) { // placeholder for smart completion
369 template.addTextSegment("<");
370 template.addVariable(new MacroCallNode(new CompleteSmartMacro()), true);
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();
380 template.addTextSegment("<" + qname);
381 addRequiredAttributes(subTag, null, template, file);
382 completeTagTail(template, subTag, file, context, false);
384 if (!requiredSubTags.isEmpty()) {
385 addTagEnd(template, descriptor, context);
387 return !requiredSubTags.isEmpty();
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:
395 case XmlElementDescriptor.CONTENT_TYPE_EMPTY:
397 template.addTextSegment(closeTag(context));
400 case XmlElementDescriptor.CONTENT_TYPE_MIXED:
402 template.addTextSegment(">");
404 template.addEndVariable();
407 template.addVariable(new MacroCallNode(new CompleteMacro()), true);
409 addTagEnd(template, descriptor, context);
413 if (!addRequiredSubTags(template, descriptor, file, context)) {
415 template.addTextSegment(">");
416 template.addEndVariable();
417 addTagEnd(template, descriptor, context);
424 private static void addTagEnd(Template template, XmlElementDescriptor descriptor, XmlTag context) {
425 template.addTextSegment("</" + descriptor.getName(context) + ">");
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);