blob: a819b65640f76cb8aa1efeb15eae8c387179d9eb [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.codeInsight.editorActions.smartEnter.SmartEnterProcessor;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author spleaner
*/
public class XmlSmartEnterProcessor extends SmartEnterProcessor {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.completion.XmlSmartEnterProcessor");
@Override
public boolean process(@NotNull final Project project, @NotNull final Editor editor, @NotNull final PsiFile psiFile) {
return completeEndTag(project, editor, psiFile);
}
private boolean completeEndTag(Project project, Editor editor, PsiFile psiFile) {
final PsiElement atCaret = getStatementAtCaret(editor, psiFile);
XmlTag tagAtCaret = PsiTreeUtil.getParentOfType(atCaret, XmlTag.class);
if (tagAtCaret == null) {
return false;
}
try {
final ASTNode emptyTagEnd = XmlChildRole.EMPTY_TAG_END_FINDER.findChild(tagAtCaret.getNode());
final ASTNode endTagEnd = XmlChildRole.START_TAG_END_FINDER.findChild(tagAtCaret.getNode());
final Document doc = editor.getDocument();
if (emptyTagEnd == null && endTagEnd == null) {
int insertionOffset = tagAtCaret.getTextRange().getEndOffset();
int caretAt = editor.getCaretModel().getOffset();
final CharSequence text = doc.getCharsSequence();
final int probableCommaOffset = CharArrayUtil.shiftForward(text, insertionOffset, " \t");
final PsiElement siebling = tagAtCaret.getNextSibling();
int caretTo = caretAt;
char ch;
if (caretAt < probableCommaOffset) {
final XmlAttribute xmlAttribute = PsiTreeUtil.getParentOfType(atCaret, XmlAttribute.class, false, XmlTag.class);
CharSequence tagNameText = null;
if (xmlAttribute != null) {
final ASTNode node = tagAtCaret.getNode();
if (node != null) {
final ASTNode tagName = XmlChildRole.START_TAG_NAME_FINDER.findChild(node);
if (tagName != null) {
tagNameText = tagName.getText();
}
}
final XmlAttributeValue valueElement = xmlAttribute.getValueElement();
final TextRange textRange = xmlAttribute.getTextRange();
caretAt = valueElement == null
? textRange.getStartOffset()
: getClosingQuote(xmlAttribute).length() == 0 ? textRange.getEndOffset() : caretAt;
}
if (tagNameText == null) {
tagNameText = text.subSequence(tagAtCaret.getTextRange().getStartOffset() + 1, caretAt);
}
final PsiElement element = psiFile.findElementAt(probableCommaOffset);
final XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class);
boolean shouldInsertClosingTag = shouldAfterWrapTextWithTag(caretAt, probableCommaOffset) || shouldInsertClosingTag(xmlAttribute, tagAtCaret);
final CharSequence text2insert = getClosingPart(xmlAttribute, tagAtCaret, !shouldInsertClosingTag);
if (tag != null && tag.getTextRange().getStartOffset() == probableCommaOffset) {
doc.insertString(caretAt, text2insert);
if (shouldInsertClosingTag) {
doc.insertString(tag.getTextRange().getEndOffset() + text2insert.length(), "</" + tagAtCaret.getName() + ">");
}
caretTo = tag.getTextRange().getEndOffset() + text2insert.length();
}
else {
doc.insertString(caretAt, text2insert);
if (shouldInsertClosingTag) {
doc.insertString(probableCommaOffset + text2insert.length(), "</" + tagNameText + ">");
}
caretTo = probableCommaOffset + text2insert.length();
}
}
else if (siebling instanceof XmlTag && siebling.getTextRange().getStartOffset() == caretAt) {
final XmlAttribute xmlAttribute = PsiTreeUtil.getParentOfType(atCaret, XmlAttribute.class, false, XmlTag.class);
final CharSequence text2insert = getClosingPart(xmlAttribute, tagAtCaret, false);
doc.insertString(caretAt, text2insert);
if (shouldInsertClosingTag(xmlAttribute, tagAtCaret)) {
doc.insertString(siebling.getTextRange().getEndOffset() + text2insert.length(), "</" + tagAtCaret.getName() + ">");
}
caretTo = siebling.getTextRange().getEndOffset() + text2insert.length();
}
else if (probableCommaOffset >= text.length() || ((ch = text.charAt(probableCommaOffset)) != '/' && ch != '>')) {
final XmlAttribute xmlAttribute = PsiTreeUtil.getParentOfType(atCaret, XmlAttribute.class, false, XmlTag.class);
final CharSequence text2insert = getClosingPart(xmlAttribute, tagAtCaret, true);
doc.insertString(insertionOffset, text2insert);
caretTo = insertionOffset + text2insert.length();
}
commitChanges(project, editor, psiFile, caretTo, null);
return true;
}
else {
final XmlTag unclosedTag = findClosestUnclosedTag(tagAtCaret);
if (unclosedTag == null) {
return false;
}
final String closingTagString = "</" + unclosedTag.getName() + ">";
final XmlTag parentTag = unclosedTag.getParentTag();
final ASTNode parentEndTagNode = parentTag != null ? XmlChildRole.CLOSING_TAG_START_FINDER.findChild(parentTag.getNode()) : null;
final int offset = parentEndTagNode != null
? parentEndTagNode.getTextRange().getStartOffset()
: unclosedTag.getTextRange().getEndOffset();
doc.insertString(offset, closingTagString);
commitChanges(project, editor, psiFile, offset, parentTag != null ? parentTag : unclosedTag);
return true;
}
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
return false;
}
protected boolean shouldAfterWrapTextWithTag(int caretAt, int probableCommaOffset) {
return probableCommaOffset > caretAt;
}
private void commitChanges(Project project, Editor editor, PsiFile psiFile, int caretOffset, @Nullable XmlTag tagToReformat) {
if (isUncommited(project)) {
commit(editor);
if (tagToReformat == null) {
tagToReformat = PsiTreeUtil.getParentOfType(getStatementAtCaret(editor, psiFile), XmlTag.class);
}
editor.getCaretModel().moveToOffset(caretOffset);
}
if (tagToReformat != null) {
reformat(tagToReformat);
}
commit(editor);
}
@Nullable
private static XmlTag findClosestUnclosedTag(final XmlTag tag) {
XmlTag unclosedTag = tag;
while (unclosedTag != null) {
final PsiElement lastChild = unclosedTag.getLastChild();
if (isTagUnclosed(lastChild)) {
return unclosedTag;
}
final XmlTag prevTag = PsiTreeUtil.getPrevSiblingOfType(unclosedTag, XmlTag.class);
unclosedTag = prevTag != null ? prevTag : PsiTreeUtil.getParentOfType(unclosedTag, XmlTag.class);
}
return null;
}
protected static boolean isTagUnclosed(PsiElement lastChild) {
//strange approach, but it's universal for xml and html
return lastChild != null &&
lastChild.getNode().getElementType() != XmlTokenType.XML_TAG_END &&
lastChild.getNode().getElementType() != XmlTokenType.XML_EMPTY_ELEMENT_END;
}
protected boolean shouldInsertClosingTag(final XmlAttribute xmlAttribute, final XmlTag tagAtCaret) {
return xmlAttribute == null || getClosingQuote(xmlAttribute).length() != 0;
}
protected String getClosingPart(final XmlAttribute xmlAttribute, final XmlTag tagAtCaret, final boolean emptyTag) {
return getClosingQuote(xmlAttribute) + (emptyTag ? "/>" : ">");
}
@NotNull
protected static CharSequence getClosingQuote(@Nullable final XmlAttribute attribute) {
if (attribute == null) {
return "";
}
final XmlAttributeValue element = attribute.getValueElement();
if (element == null) {
return "";
}
final String s = element.getText();
if (s != null && s.length() > 0) {
if (s.charAt(0) == '"' && s.charAt(s.length() - 1) != '"') {
return "\"";
}
else if (s.charAt(0) == '\'' && s.charAt(s.length() - 1) != '\'') {
return "'";
}
}
return "";
}
}