blob: aa983fc9bc5d2a95df7ad0b64623cc02fef03e87 [file] [log] [blame]
/*
* Copyright 2000-2014 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.codeInsight.completion;
import com.intellij.application.options.editor.WebEditorOptions;
import com.intellij.codeInsight.TailType;
import com.intellij.codeInsight.lookup.Lookup;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupItem;
import com.intellij.codeInsight.template.Template;
import com.intellij.codeInsight.template.TemplateEditingAdapter;
import com.intellij.codeInsight.template.TemplateManager;
import com.intellij.codeInsight.template.impl.MacroCallNode;
import com.intellij.codeInsight.template.macro.CompleteMacro;
import com.intellij.codeInsight.template.macro.CompleteSmartMacro;
import com.intellij.codeInspection.InspectionProfile;
import com.intellij.codeInspection.htmlInspections.XmlEntitiesInspection;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.command.undo.UndoManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.html.HtmlTag;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlTokenType;
import com.intellij.xml.*;
import com.intellij.xml.actions.GenerateXmlTagAction;
import com.intellij.xml.impl.schema.XmlElementDescriptorImpl;
import com.intellij.xml.util.HtmlUtil;
import com.intellij.xml.util.XmlUtil;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public class XmlTagInsertHandler implements InsertHandler<LookupElement> {
public static final XmlTagInsertHandler INSTANCE = new XmlTagInsertHandler();
@Override
public void handleInsert(InsertionContext context, LookupElement item) {
Project project = context.getProject();
Editor editor = context.getEditor();
// Need to insert " " to prevent creating tags like <tagThis is my text
final int offset = editor.getCaretModel().getOffset();
editor.getDocument().insertString(offset, " ");
PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
PsiElement current = context.getFile().findElementAt(context.getStartOffset());
editor.getDocument().deleteString(offset, offset + 1);
final XmlTag tag = PsiTreeUtil.getContextOfType(current, XmlTag.class, true);
if (tag == null) return;
if (context.getCompletionChar() != Lookup.COMPLETE_STATEMENT_SELECT_CHAR) {
context.setAddCompletionChar(false);
}
final XmlElementDescriptor descriptor = tag.getDescriptor();
if (XmlUtil.getTokenOfType(tag, XmlTokenType.XML_TAG_END) == null &&
XmlUtil.getTokenOfType(tag, XmlTokenType.XML_EMPTY_ELEMENT_END) == null) {
if (descriptor != null) {
insertIncompleteTag(context.getCompletionChar(), editor, project, descriptor, tag);
}
}
else if (context.getCompletionChar() == Lookup.REPLACE_SELECT_CHAR) {
PsiDocumentManager.getInstance(project).commitAllDocuments();
int caretOffset = editor.getCaretModel().getOffset();
PsiElement otherTag = PsiTreeUtil.getParentOfType(context.getFile().findElementAt(caretOffset), XmlTag.class);
PsiElement endTagStart = XmlUtil.getTokenOfType(otherTag, XmlTokenType.XML_END_TAG_START);
if (endTagStart != null) {
PsiElement sibling = endTagStart.getNextSibling();
assert sibling != null;
ASTNode node = sibling.getNode();
assert node != null;
if (node.getElementType() == XmlTokenType.XML_NAME) {
int sOffset = sibling.getTextRange().getStartOffset();
int eOffset = sibling.getTextRange().getEndOffset();
editor.getDocument().deleteString(sOffset, eOffset);
assert otherTag != null;
editor.getDocument().insertString(sOffset, ((XmlTag)otherTag).getName());
}
}
editor.getCaretModel().moveToOffset(caretOffset + 1);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
editor.getSelectionModel().removeSelection();
}
if (context.getCompletionChar() == ' ' && TemplateManager.getInstance(project).getActiveTemplate(editor) != null) {
return;
}
final TailType tailType = LookupItem.handleCompletionChar(editor, item, context.getCompletionChar());
tailType.processTail(editor, editor.getCaretModel().getOffset());
}
private static void insertIncompleteTag(char completionChar,
final Editor editor,
final Project project,
XmlElementDescriptor descriptor,
XmlTag tag) {
TemplateManager templateManager = TemplateManager.getInstance(project);
Template template = templateManager.createTemplate("", "");
template.setToIndent(true);
// temp code
PsiFile containingFile = tag.getContainingFile();
boolean htmlCode = HtmlUtil.hasHtml(containingFile);
template.setToReformat(!htmlCode);
StringBuilder indirectRequiredAttrs = addRequiredAttributes(descriptor, tag, template, containingFile);
final boolean chooseAttributeName = addTail(completionChar, descriptor, htmlCode, tag, template, indirectRequiredAttrs);
templateManager.startTemplate(editor, template, new TemplateEditingAdapter() {
private RangeMarker myAttrValueMarker;
@Override
public void waitingForInput(Template template) {
int offset = editor.getCaretModel().getOffset();
myAttrValueMarker = editor.getDocument().createRangeMarker(offset + 1, offset + 4);
}
@Override
public void templateFinished(final Template template, boolean brokenOff) {
final int offset = editor.getCaretModel().getOffset();
if (chooseAttributeName && offset >= 3) {
char c = editor.getDocument().getCharsSequence().charAt(offset - 3);
if (c == '/' || (c == ' ' && brokenOff)) {
new WriteCommandAction.Simple(project) {
@Override
protected void run() throws Throwable {
editor.getDocument().replaceString(offset - 2, offset + 1, ">");
}
}.execute();
}
}
}
@Override
public void templateCancelled(final Template template) {
if (myAttrValueMarker == null) {
return;
}
final UndoManager manager = UndoManager.getInstance(project);
if (manager.isUndoInProgress() || manager.isRedoInProgress()) {
return;
}
if (chooseAttributeName && myAttrValueMarker.isValid()) {
final int startOffset = myAttrValueMarker.getStartOffset();
final int endOffset = myAttrValueMarker.getEndOffset();
new WriteCommandAction.Simple(project) {
@Override
protected void run() throws Throwable {
editor.getDocument().replaceString(startOffset, endOffset, ">");
}
}.execute();
}
}
});
}
@Nullable
private static StringBuilder addRequiredAttributes(XmlElementDescriptor descriptor,
@Nullable XmlTag tag,
Template template,
PsiFile containingFile) {
boolean htmlCode = HtmlUtil.hasHtml(containingFile);
Set<String> notRequiredAttributes = Collections.emptySet();
if (tag instanceof HtmlTag) {
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(tag.getProject()).getInspectionProfile();
XmlEntitiesInspection inspection = (XmlEntitiesInspection)profile.getUnwrappedTool(
XmlEntitiesInspection.REQUIRED_ATTRIBUTES_SHORT_NAME, tag);
if (inspection != null) {
StringTokenizer tokenizer = new StringTokenizer(inspection.getAdditionalEntries());
notRequiredAttributes = new HashSet<String>();
while(tokenizer.hasMoreElements()) notRequiredAttributes.add(tokenizer.nextToken());
}
}
XmlAttributeDescriptor[] attributes = descriptor.getAttributesDescriptors(tag);
StringBuilder indirectRequiredAttrs = null;
if (WebEditorOptions.getInstance().isAutomaticallyInsertRequiredAttributes()) {
final XmlExtension extension = XmlExtension.getExtension(containingFile);
for (XmlAttributeDescriptor attributeDecl : attributes) {
String attributeName = attributeDecl.getName(tag);
if (attributeDecl.isRequired() && (tag == null || tag.getAttributeValue(attributeName) == null)) {
if (!notRequiredAttributes.contains(attributeName)) {
if (!extension.isIndirectSyntax(attributeDecl)) {
template.addTextSegment(" " + attributeName + "=\"");
template.addVariable(new MacroCallNode(new CompleteMacro()), true);
template.addTextSegment("\"");
}
else {
if (indirectRequiredAttrs == null) indirectRequiredAttrs = new StringBuilder();
indirectRequiredAttrs.append("\n<jsp:attribute name=\"").append(attributeName).append("\"></jsp:attribute>\n");
}
}
}
else if (attributeDecl.isRequired() && attributeDecl.isFixed() && attributeDecl.getDefaultValue() != null && !htmlCode) {
template.addTextSegment(" " + attributeName + "=\"" + attributeDecl.getDefaultValue() + "\"");
}
}
}
return indirectRequiredAttrs;
}
protected static boolean addTail(char completionChar,
XmlElementDescriptor descriptor,
boolean isHtmlCode,
XmlTag tag,
Template template,
StringBuilder indirectRequiredAttrs) {
if (completionChar == '>' || (completionChar == '/' && indirectRequiredAttrs != null)) {
template.addTextSegment(">");
boolean toInsertCDataEnd = false;
if (descriptor instanceof XmlElementDescriptorWithCDataContent) {
final XmlElementDescriptorWithCDataContent cDataContainer = (XmlElementDescriptorWithCDataContent)descriptor;
if (cDataContainer.requiresCdataBracesInContext(tag)) {
template.addTextSegment("<![CDATA[\n");
toInsertCDataEnd = true;
}
}
if (indirectRequiredAttrs != null) template.addTextSegment(indirectRequiredAttrs.toString());
template.addEndVariable();
if (toInsertCDataEnd) template.addTextSegment("\n]]>");
if ((!(tag instanceof HtmlTag) || !HtmlUtil.isSingleHtmlTag(tag.getName())) && tag.getAttributes().length == 0) {
if (WebEditorOptions.getInstance().isAutomaticallyInsertClosingTag()) {
final String name = descriptor.getName(tag);
if (name != null) {
template.addTextSegment("</");
template.addTextSegment(name);
template.addTextSegment(">");
}
}
}
}
else if (completionChar == '/') {
template.addTextSegment("/>");
}
else if (completionChar == ' ' && template.getSegmentsCount() == 0) {
if (WebEditorOptions.getInstance().isAutomaticallyStartAttribute() &&
(descriptor.getAttributesDescriptors(tag).length > 0 || isTagFromHtml(tag) && !HtmlUtil.isTagWithoutAttributes(tag.getName()))) {
completeAttribute(template);
return true;
}
}
else if (completionChar == Lookup.AUTO_INSERT_SELECT_CHAR || completionChar == Lookup.NORMAL_SELECT_CHAR || completionChar == Lookup.REPLACE_SELECT_CHAR) {
if (WebEditorOptions.getInstance().isAutomaticallyInsertClosingTag() && isHtmlCode && HtmlUtil.isSingleHtmlTag(tag.getName())) {
template.addTextSegment(HtmlUtil.isHtmlTag(tag) ? ">" : "/>");
}
else {
if (needAlLeastOneAttribute(tag) && WebEditorOptions.getInstance().isAutomaticallyStartAttribute() && tag.getAttributes().length == 0
&& template.getSegmentsCount() == 0) {
completeAttribute(template);
return true;
}
else {
completeTagTail(template, descriptor, tag.getContainingFile(), tag, true);
}
}
}
return false;
}
private static void completeAttribute(Template template) {
template.addTextSegment(" ");
template.addVariable(new MacroCallNode(new CompleteMacro()), true);
template.addTextSegment("=\"");
template.addEndVariable();
template.addTextSegment("\"");
}
private static boolean needAlLeastOneAttribute(XmlTag tag) {
for (XmlTagRuleProvider ruleProvider : XmlTagRuleProvider.EP_NAME.getExtensions()) {
for (XmlTagRuleProvider.Rule rule : ruleProvider.getTagRule(tag)) {
if (rule.needAtLeastOneAttribute(tag)) {
return true;
}
}
}
return false;
}
private static boolean addRequiredSubTags(Template template, XmlElementDescriptor descriptor, PsiFile file, XmlTag context) {
if (!WebEditorOptions.getInstance().isAutomaticallyInsertRequiredSubTags()) return false;
List<XmlElementDescriptor> requiredSubTags = GenerateXmlTagAction.getRequiredSubTags(descriptor);
if (!requiredSubTags.isEmpty()) {
template.addTextSegment(">");
template.setToReformat(true);
}
for (XmlElementDescriptor subTag : requiredSubTags) {
if (subTag == null) { // placeholder for smart completion
template.addTextSegment("<");
template.addVariable(new MacroCallNode(new CompleteSmartMacro()), true);
continue;
}
String qname = subTag.getName();
if (subTag instanceof XmlElementDescriptorImpl) {
String prefixByNamespace = context.getPrefixByNamespace(((XmlElementDescriptorImpl)subTag).getNamespace());
if (StringUtil.isNotEmpty(prefixByNamespace)) {
qname = prefixByNamespace + ":" + subTag.getName();
}
}
template.addTextSegment("<" + qname);
addRequiredAttributes(subTag, null, template, file);
completeTagTail(template, subTag, file, context, false);
}
if (!requiredSubTags.isEmpty()) {
addTagEnd(template, descriptor, context);
}
return !requiredSubTags.isEmpty();
}
private static void completeTagTail(Template template, XmlElementDescriptor descriptor, PsiFile file, XmlTag context, boolean firstLevel) {
boolean completeIt = !firstLevel || descriptor.getAttributesDescriptors(null).length == 0;
switch (descriptor.getContentType()) {
case XmlElementDescriptor.CONTENT_TYPE_UNKNOWN:
return;
case XmlElementDescriptor.CONTENT_TYPE_EMPTY:
if (completeIt) {
template.addTextSegment("/>");
}
break;
case XmlElementDescriptor.CONTENT_TYPE_MIXED:
if (completeIt) {
template.addTextSegment(">");
if (firstLevel) {
template.addEndVariable();
}
else {
template.addVariable(new MacroCallNode(new CompleteMacro()), true);
}
addTagEnd(template, descriptor, context);
}
break;
default:
if (!addRequiredSubTags(template, descriptor, file, context)) {
if (completeIt) {
template.addTextSegment(">");
template.addEndVariable();
addTagEnd(template, descriptor, context);
}
}
break;
}
}
private static void addTagEnd(Template template, XmlElementDescriptor descriptor, XmlTag context) {
template.addTextSegment("</" + descriptor.getName(context) + ">");
}
private static boolean isTagFromHtml(final XmlTag tag) {
final String ns = tag.getNamespace();
return XmlUtil.XHTML_URI.equals(ns) || XmlUtil.HTML_URI.equals(ns);
}
}