/*
* Copyright 2000-2016 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;
import com.intellij.application.options.emmet.EmmetOptions;
import com.intellij.codeInsight.CodeInsightActionHandler;
import com.intellij.codeInsight.actions.BaseCodeInsightAction;
import com.intellij.codeInsight.template.CustomTemplateCallback;
import com.intellij.codeInsight.template.emmet.generators.XmlZenCodingGeneratorImpl;
import com.intellij.codeInsight.template.impl.TemplateImpl;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.PopupAction;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.PairProcessor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.XmlBundle;
import com.intellij.xml.util.HtmlUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
public class EmmetUpdateTagAction extends BaseCodeInsightAction implements DumbAware, PopupAction {
private static final String EMMET_RECENT_UPDATE_ABBREVIATIONS_KEY = "emmet.recent.update.abbreviations";
private static final String EMMET_LAST_UPDATE_ABBREVIATIONS_KEY = "emmet.last.update.abbreviations";
@NotNull
@Override
protected CodeInsightActionHandler getHandler() {
return new CodeInsightActionHandler() {
@Override
public void invoke(@NotNull final Project project, @NotNull final Editor editor, @NotNull final PsiFile file) {
final XmlTag tag = findTag(editor, file);
if (tag != null) {
new EmmetAbbreviationBalloon(EMMET_RECENT_UPDATE_ABBREVIATIONS_KEY, EMMET_LAST_UPDATE_ABBREVIATIONS_KEY,
new EmmetAbbreviationBalloon.Callback() {
@Override
public void onEnter(@NotNull String abbreviation) {
try {
doUpdateTag(abbreviation, tag, file, editor);
}
catch (EmmetException ignore) {
}
}
}, XmlBundle.message("emmet.update.tag.title")).show(new CustomTemplateCallback(editor, file));
}
}
@Override
public boolean startInWriteAction() {
return false;
}
};
}
public void doUpdateTag(@NotNull final String abbreviation,
@NotNull final XmlTag tag,
@NotNull PsiFile file,
@NotNull Editor editor) throws EmmetException {
if (tag.isValid()) {
String templateText = expandTemplate(abbreviation, file, editor);
final Collection<String> classNames = ContainerUtil.newLinkedHashSet();
ContainerUtil.addAll(classNames, HtmlUtil.splitClassNames(tag.getAttributeValue(HtmlUtil.CLASS_ATTRIBUTE_NAME)));
final Map<String, String> attributes = ContainerUtil.newLinkedHashMap();
final Ref<String> newTagName = Ref.create();
processTags(file.getProject(), templateText, (tag1, firstTag) -> {
if (firstTag && !abbreviation.isEmpty() && StringUtil.isJavaIdentifierPart(abbreviation.charAt(0))) {
newTagName.set(tag1.getName());
}
for (String clazz : HtmlUtil.splitClassNames(tag1.getAttributeValue(HtmlUtil.CLASS_ATTRIBUTE_NAME))) {
if (StringUtil.startsWithChar(clazz, '+')) {
classNames.add(clazz.substring(1));
}
else if (StringUtil.startsWithChar(clazz, '-')) {
classNames.remove(clazz.substring(1));
}
else {
classNames.clear();
classNames.add(clazz);
}
}
if (!firstTag) {
classNames.add(tag1.getName());
}
for (XmlAttribute xmlAttribute : tag1.getAttributes()) {
if (!HtmlUtil.CLASS_ATTRIBUTE_NAME.equalsIgnoreCase(xmlAttribute.getName())) {
attributes.put(xmlAttribute.getName(), StringUtil.notNullize(xmlAttribute.getValue()));
}
}
return true;
});
doUpdateTagAttributes(tag, file, newTagName.get(), classNames, attributes).execute();
}
}
@Nullable
private static String expandTemplate(@NotNull String abbreviation, @NotNull PsiFile file, @NotNull Editor editor) throws EmmetException {
final CollectCustomTemplateCallback callback = new CollectCustomTemplateCallback(editor, file);
ZenCodingTemplate.expand(abbreviation, callback, XmlZenCodingGeneratorImpl.INSTANCE, Collections.emptyList(),
true, Registry.intValue("emmet.segments.limit"));
TemplateImpl template = callback.getGeneratedTemplate();
return template != null ? template.getTemplateText() : null;
}
private static void processTags(@NotNull Project project,
@Nullable String templateText,
@NotNull PairProcessor<XmlTag, Boolean> processor) {
if (StringUtil.isNotEmpty(templateText)) {
final PsiFileFactory psiFileFactory = PsiFileFactory.getInstance(project);
XmlFile xmlFile = (XmlFile)psiFileFactory.createFileFromText("dummy.xml", StdFileTypes.HTML, templateText);
XmlTag tag = xmlFile.getRootTag();
boolean firstTag = true;
while (tag != null) {
processor.process(tag, firstTag);
firstTag = false;
tag = PsiTreeUtil.getNextSiblingOfType(tag, XmlTag.class);
}
}
}
@NotNull
private static WriteCommandAction<Void> doUpdateTagAttributes(@NotNull final XmlTag tag,
@NotNull final PsiFile file,
@Nullable final String newTagName,
@NotNull final Collection<String> classes,
@NotNull final Map<String, String> attributes) {
return new WriteCommandAction<Void>(file.getProject(), file) {
@Override
protected void run(@NotNull Result<Void> result) throws Throwable {
if (tag.isValid()) {
if (!ReadonlyStatusHandler.getInstance(file.getProject()).ensureFilesWritable(file.getVirtualFile()).hasReadonlyFiles()) {
tag.setAttribute(HtmlUtil.CLASS_ATTRIBUTE_NAME, StringUtil.join(classes, " ").trim());
for (Map.Entry<String, String> attribute : attributes.entrySet()) {
final String attributeName = attribute.getKey();
if (StringUtil.startsWithChar(attributeName, '+')) {
final XmlAttribute existingAttribute = tag.getAttribute(attributeName.substring(1));
if (existingAttribute != null) {
existingAttribute.setValue(StringUtil.notNullize(existingAttribute.getValue() + attribute.getValue()));
}
else {
tag.setAttribute(attributeName.substring(1), attribute.getValue());
}
}
else if (StringUtil.startsWithChar(attributeName, '-')) {
final XmlAttribute existingAttribute = tag.getAttribute(attributeName.substring(1));
if (existingAttribute != null) {
existingAttribute.delete();
}
}
else {
tag.setAttribute(attributeName, attribute.getValue());
}
}
if (newTagName != null) {
tag.setName(newTagName);
}
}
}
}
};
}
@Override
public void update(AnActionEvent event) {
super.update(event);
event.getPresentation().setVisible(event.getPresentation().isEnabled());
}
@Override
protected boolean isValidForFile(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
return super.isValidForFile(project, editor, file) && EmmetOptions.getInstance().isEmmetEnabled() && findTag(editor, file) != null;
}
@Nullable
private static XmlTag findTag(@NotNull Editor editor, @NotNull PsiFile file) {
final XmlTag tag = PsiTreeUtil.getNonStrictParentOfType(file.findElementAt(editor.getCaretModel().getOffset()), XmlTag.class);
return tag != null && HtmlUtil.isHtmlTag(tag) ? tag : null;
}
}