blob: 3f3c3f32e8a72da4bda95e6fa6d4cac84b711dda [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.template.emmet.nodes;
import com.google.common.base.Strings;
import com.intellij.codeInsight.template.CustomTemplateCallback;
import com.intellij.codeInsight.template.LiveTemplateBuilder;
import com.intellij.codeInsight.template.emmet.ZenCodingUtil;
import com.intellij.codeInsight.template.emmet.filters.SingleLineEmmetFilter;
import com.intellij.codeInsight.template.emmet.filters.ZenCodingFilter;
import com.intellij.codeInsight.template.emmet.generators.XmlZenCodingGenerator;
import com.intellij.codeInsight.template.emmet.generators.XmlZenCodingGeneratorImpl;
import com.intellij.codeInsight.template.emmet.generators.ZenCodingGenerator;
import com.intellij.codeInsight.template.emmet.tokens.TemplateToken;
import com.intellij.codeInsight.template.impl.TemplateImpl;
import com.intellij.injected.editor.DocumentWindowImpl;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.command.undo.UndoConstants;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.XmlElementFactory;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.LocalTimeCounter;
import com.intellij.util.containers.HashMap;
import com.intellij.util.containers.HashSet;
import com.intellij.xml.util.HtmlUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.google.common.collect.Lists.newArrayList;
/**
* @author Eugene.Kudelevsky
*/
public class GenerationNode extends UserDataHolderBase {
private final TemplateToken myTemplateToken;
private final List<GenerationNode> myChildren = newArrayList();
private final int myNumberInIteration;
private final int myTotalIterations;
private String mySurroundedText;
private final boolean myInsertSurroundedTextAtTheEnd;
private final boolean myInsertNewLineBetweenNodes;
private GenerationNode myParent;
private boolean myContainsSurroundedTextMarker = false;
public GenerationNode(TemplateToken templateToken,
int numberInIteration,
int totalIterations, String surroundedText,
boolean insertSurroundedTextAtTheEnd, GenerationNode parent) {
this(templateToken, numberInIteration, totalIterations, surroundedText, insertSurroundedTextAtTheEnd, parent, false);
}
public GenerationNode(TemplateToken templateToken,
int numberInIteration,
int totalIterations, String surroundedText,
boolean insertSurroundedTextAtTheEnd, GenerationNode parent, boolean insertNewLineBetweenNodes) {
myTemplateToken = templateToken;
myNumberInIteration = numberInIteration;
myTotalIterations = totalIterations;
mySurroundedText = surroundedText;
myInsertSurroundedTextAtTheEnd = insertSurroundedTextAtTheEnd;
myInsertNewLineBetweenNodes = insertNewLineBetweenNodes;
if(parent != null) {
parent.addChild(this);
}
}
public boolean isInsertNewLineBetweenNodes() {
return myInsertNewLineBetweenNodes;
}
public List<GenerationNode> getChildren() {
return myChildren;
}
public void addChild(GenerationNode child) {
child.setParent(this);
myChildren.add(child);
}
public void addChildren(Collection<GenerationNode> children) {
for (GenerationNode child : children) {
addChild(child);
}
}
public boolean isLeaf() {
return myChildren.size() == 0;
}
private boolean isBlockTag() {
if (myTemplateToken != null) {
XmlFile xmlFile = myTemplateToken.getFile();
XmlDocument document = xmlFile.getDocument();
if (document != null) {
XmlTag tag = document.getRootTag();
if (tag != null) {
return HtmlUtil.isHtmlBlockTagL(tag.getName());
}
}
}
return false;
}
@NotNull
public TemplateImpl generate(@NotNull CustomTemplateCallback callback,
@Nullable ZenCodingGenerator generator,
@NotNull Collection<ZenCodingFilter> filters,
boolean insertSurroundedText, int segmentsLimit) {
myContainsSurroundedTextMarker = !(insertSurroundedText && myInsertSurroundedTextAtTheEnd);
GenerationNode generationNode = this;
if (generationNode != this) {
return generationNode.generate(callback, generator, Collections.<ZenCodingFilter>emptyList(), insertSurroundedText, segmentsLimit);
}
boolean shouldNotReformatTemplate = false;
boolean oneLineTemplateExpanding = false;
for (ZenCodingFilter filter : filters) {
generationNode = filter.filterNode(generationNode);
if(filter instanceof SingleLineEmmetFilter) {
shouldNotReformatTemplate = true;
oneLineTemplateExpanding = true;
}
}
CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(callback.getProject());
String indentStr;
if (callback.isInInjectedFragment()) {
Editor editor = callback.getEditor();
Document document = editor.getDocument();
if(document instanceof DocumentWindowImpl && ((DocumentWindowImpl)document).isOneLine()) {
/*
* If document is one-line that in the moment of inserting text,
* new line chars will be filtered (see DocumentWindowImpl#insertString).
* So in this case we should filter text by SingleLineAvoid in order to avoid
* inconsistency of template segments.
*/
oneLineTemplateExpanding = true;
filters.add(new SingleLineEmmetFilter());
}
indentStr = "";
}
else if (settings.useTabCharacter(callback.getFileType())) {
indentStr = "\t";
}
else {
int tabSize = settings.getTabSize(callback.getFileType());
indentStr = StringUtil.repeatSymbol(' ', tabSize);
}
LiveTemplateBuilder builder = new LiveTemplateBuilder(segmentsLimit);
int end = -1;
boolean hasChildren = myChildren.size() > 0;
TemplateImpl parentTemplate;
Map<String, String> predefinedValues;
if (myTemplateToken instanceof TemplateToken && generator instanceof XmlZenCodingGenerator) {
TemplateToken xmlTemplateToken = myTemplateToken;
List<Couple<String>> attr2value = new ArrayList<Couple<String>>(xmlTemplateToken.getAttribute2Value());
parentTemplate = invokeXmlTemplate(xmlTemplateToken, callback, generator, hasChildren, attr2value);
predefinedValues = buildPredefinedValues(attr2value, (XmlZenCodingGenerator)generator, hasChildren);
}
else {
parentTemplate = invokeTemplate(myTemplateToken, hasChildren, callback, generator);
predefinedValues = null;
}
String s = parentTemplate.getString();
for (ZenCodingFilter filter : filters) {
s = filter.filterText(s, myTemplateToken);
}
parentTemplate = parentTemplate.copy();
parentTemplate.setString(s);
final String txt = hasChildren || myContainsSurroundedTextMarker ? null : mySurroundedText;
parentTemplate = expandTemplate(parentTemplate, predefinedValues, txt, segmentsLimit);
int offset = builder.insertTemplate(0, parentTemplate, null);
int newOffset = gotoChild(callback.getProject(), builder.getText(), offset, 0, builder.length());
if (offset < builder.length() && newOffset != offset) {
end = offset;
}
offset = newOffset;
if (end == -1 && offset < builder.length() && myChildren.size() == 0) {
end = offset;
}
LiveTemplateBuilder.Marker marker = offset < builder.length() ? builder.createMarker(offset) : null;
//noinspection ForLoopReplaceableByForEach
for (int i = 0, myChildrenSize = myChildren.size(); i < myChildrenSize; i++) {
GenerationNode child = myChildren.get(i);
TemplateImpl childTemplate = child.generate(callback, generator, filters, !myContainsSurroundedTextMarker, segmentsLimit);
boolean blockTag = child.isBlockTag();
if (!oneLineTemplateExpanding && blockTag && !isNewLineBefore(builder.getText(), offset)) {
builder.insertText(offset, "\n" + indentStr, false);
offset += indentStr.length() + 1;
}
int e = builder.insertTemplate(offset, childTemplate, null);
offset = marker != null ? marker.getEndOffset() : builder.length();
if (!oneLineTemplateExpanding && ((blockTag && !isNewLineAfter(builder.getText(), offset)) || myInsertNewLineBetweenNodes)) {
builder.insertText(offset, "\n" + indentStr, false);
offset += indentStr.length() + 1;
}
if (end == -1 && e < offset) {
end = e;
}
}
if(shouldNotReformatTemplate) {
builder.setIsToReformat(false);
}
return builder.buildTemplate();
}
private static TemplateImpl invokeTemplate(TemplateToken token,
boolean hasChildren,
final CustomTemplateCallback callback,
@Nullable ZenCodingGenerator generator) {
TemplateImpl template = token.getTemplate();
if (generator != null) {
assert template != null;
template = generator.generateTemplate(token, hasChildren, callback.getContext());
removeVariablesWhichHasNoSegment(template);
}
return template;
}
private TemplateImpl invokeXmlTemplate(final TemplateToken token,
CustomTemplateCallback callback,
@Nullable ZenCodingGenerator generator,
final boolean hasChildren,
final List<Couple<String>> attr2value) {
/*assert generator == null || generator instanceof XmlZenCodingGenerator :
"The generator cannot process TemplateToken because it doesn't inherit XmlZenCodingGenerator";*/
TemplateImpl template = token.getTemplate();
assert template != null;
final XmlFile xmlFile = token.getFile();
PsiFileFactory fileFactory = PsiFileFactory.getInstance(xmlFile.getProject());
XmlFile dummyFile = (XmlFile)fileFactory.createFileFromText("dummy.xml", StdFileTypes.XML, xmlFile.getText());
final XmlTag tag = dummyFile.getRootTag();
if (tag != null) {
for (Couple<String> pair : attr2value) {
if (Strings.isNullOrEmpty(pair.second)) {
template.addVariable(prepareVariableName(pair.first), "", "", true);
}
}
XmlTag tag1 = hasChildren ? expandEmptyTagIfNecessary(tag) : tag;
setAttributeValues(tag1, attr2value);
XmlFile physicalFile = (XmlFile)fileFactory.createFileFromText("dummy.xml", StdFileTypes.XML, tag1.getContainingFile().getText(),
LocalTimeCounter.currentTime(), true);
VirtualFile vFile = physicalFile.getVirtualFile();
if (vFile != null) {
vFile.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE);
}
token.setFile(physicalFile);
}
ZenCodingGenerator zenCodingGenerator = generator != null ? generator : XmlZenCodingGeneratorImpl.INSTANCE;
template = zenCodingGenerator.generateTemplate(token, hasChildren, callback.getContext());
removeVariablesWhichHasNoSegment(template);
return template;
}
private static String prepareVariableName(@NotNull String attributeName) {
return StringUtil.replaceChar(attributeName, '-', '_');
}
@NotNull
private static TemplateImpl expandTemplate(@NotNull TemplateImpl template,
Map<String, String> predefinedVarValues,
String surroundedText,
int segmentsLimit) {
LiveTemplateBuilder builder = new LiveTemplateBuilder(segmentsLimit);
if (predefinedVarValues == null && surroundedText == null) {
return template;
}
int offset = builder.insertTemplate(0, template, predefinedVarValues);
if (surroundedText != null) {
builder.insertText(offset, surroundedText, true);
builder.setIsToReformat(true);
}
return builder.buildTemplate();
}
@NotNull
private static XmlTag expandEmptyTagIfNecessary(@NotNull XmlTag tag) {
StringBuilder builder = new StringBuilder();
boolean flag = false;
for (PsiElement child : tag.getChildren()) {
if (child instanceof XmlToken && XmlTokenType.XML_EMPTY_ELEMENT_END.equals(((XmlToken)child).getTokenType())) {
flag = true;
break;
}
builder.append(child.getText());
}
if (flag) {
builder.append("></").append(tag.getName()).append('>');
return XmlElementFactory.getInstance(tag.getProject()).createTagFromText(builder.toString(), XMLLanguage.INSTANCE);
}
return tag;
}
private static int gotoChild(Project project, CharSequence text, int offset, int start, int end) {
PsiFile file = PsiFileFactory.getInstance(project)
.createFileFromText("dummy.xml", StdFileTypes.XML, text, LocalTimeCounter.currentTime(), false);
PsiElement element = file.findElementAt(offset);
if (offset < end && element instanceof XmlToken && ((XmlToken)element).getTokenType() == XmlTokenType.XML_END_TAG_START) {
return offset;
}
int newOffset = -1;
XmlTag tag = PsiTreeUtil.findElementOfClassAtRange(file, start, end, XmlTag.class);
if (tag != null) {
for (PsiElement child : tag.getChildren()) {
if (child instanceof XmlToken && ((XmlToken)child).getTokenType() == XmlTokenType.XML_END_TAG_START) {
newOffset = child.getTextOffset();
}
}
}
if (newOffset >= 0) {
return newOffset;
}
return offset;
}
private static void removeVariablesWhichHasNoSegment(TemplateImpl template) {
Set<String> segments = new HashSet<String>();
for (int i = 0; i < template.getSegmentsCount(); i++) {
segments.add(template.getSegmentName(i));
}
for (int i = template.getVariableCount() - 1; i >= 0; i--) {
String varName = template.getVariableNameAt(i);
if (!segments.contains(varName)) {
template.removeVariable(i);
} else {
segments.remove(varName);
}
}
}
@Nullable
private Map<String, String> buildPredefinedValues(List<Couple<String>> attribute2value,
@Nullable XmlZenCodingGenerator generator,
boolean hasChildren) {
if (generator == null) {
return Collections.emptyMap();
}
for (Couple<String> pair : attribute2value) {
if (ZenCodingUtil.containsSurroundedTextMarker(pair.second)) {
myContainsSurroundedTextMarker = true;
break;
}
}
String attributes = generator.buildAttributesString(attribute2value, hasChildren, myNumberInIteration, myTotalIterations, mySurroundedText);
attributes = attributes.length() > 0 ? ' ' + attributes : null;
Map<String, String> predefinedValues = null;
if (attributes != null) {
predefinedValues = new HashMap<String, String>();
predefinedValues.put(TemplateToken.ATTRS, attributes);
}
return predefinedValues;
}
private void setAttributeValues(XmlTag tag, List<Couple<String>> attr2value) {
for (Iterator<Couple<String>> iterator = attr2value.iterator(); iterator.hasNext();) {
Couple<String> pair = iterator.next();
if (tag.getAttribute(pair.first) != null) {
if (ZenCodingUtil.containsSurroundedTextMarker(pair.second)) {
myContainsSurroundedTextMarker = true;
}
tag.setAttribute(pair.first,
Strings.isNullOrEmpty(pair.second)
? "$" + prepareVariableName(pair.first) + "$"
: ZenCodingUtil.getValue(pair.second, myNumberInIteration, myTotalIterations, mySurroundedText));
iterator.remove();
}
}
}
private static boolean isNewLineBefore(CharSequence text, int offset) {
int i = offset - 1;
while (i >= 0 && Character.isWhitespace(text.charAt(i))) {
if (text.charAt(i) == '\n') {
return true;
}
i--;
}
return i < 0;
}
private static boolean isNewLineAfter(CharSequence text, int offset) {
int i = offset;
while (i < text.length() && Character.isWhitespace(text.charAt(i))) {
if (text.charAt(i) == '\n') {
return true;
}
i++;
}
return i == text.length();
}
public TemplateToken getTemplateToken() {
return myTemplateToken;
}
public String getSurroundedText() {
return mySurroundedText;
}
public void setSurroundedText(String surroundedText) {
mySurroundedText = surroundedText;
}
public GenerationNode getParent() {
return myParent;
}
public void setParent(GenerationNode parent) {
myParent = parent;
}
}