blob: 4b50a0cbdb01f2b423801876b94a202b34c05524 [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;
import com.intellij.application.options.emmet.EmmetOptions;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.template.*;
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.ZenCodingGenerator;
import com.intellij.codeInsight.template.emmet.nodes.*;
import com.intellij.codeInsight.template.emmet.tokens.TemplateToken;
import com.intellij.codeInsight.template.emmet.tokens.TextToken;
import com.intellij.codeInsight.template.emmet.tokens.ZenCodingToken;
import com.intellij.codeInsight.template.impl.*;
import com.intellij.diagnostic.AttachmentFactory;
import com.intellij.ide.IdeEventQueue;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.JBPopupListener;
import com.intellij.openapi.ui.popup.LightweightWindowEvent;
import com.intellij.openapi.ui.popup.util.PopupUtil;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.patterns.StandardPatterns;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.ui.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.XmlBundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.event.DocumentEvent;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;
/**
* @author Eugene.Kudelevsky
*/
public class ZenCodingTemplate extends CustomLiveTemplateBase {
public static final char MARKER = '\0';
private static final String EMMET_RECENT_WRAP_ABBREVIATIONS_KEY = "emmet.recent.wrap.abbreviations";
private static final String EMMET_LAST_WRAP_ABBREVIATIONS_KEY = "emmet.last.wrap.abbreviations";
private static final Logger LOG = Logger.getInstance(ZenCodingTemplate.class);
@Nullable
public static ZenCodingGenerator findApplicableDefaultGenerator(@NotNull PsiElement context, boolean wrapping) {
for (ZenCodingGenerator generator : ZenCodingGenerator.getInstances()) {
if (generator.isMyContext(context, wrapping) && generator.isAppliedByDefault(context)) {
return generator;
}
}
return null;
}
@Nullable
public static ZenCodingNode parse(@NotNull String text,
@NotNull CustomTemplateCallback callback,
@NotNull ZenCodingGenerator generator,
@Nullable String surroundedText) {
List<ZenCodingToken> tokens = new EmmetLexer().lex(text);
if (tokens == null) {
return null;
}
if (!validate(tokens, generator)) {
return null;
}
EmmetParser parser = generator.createParser(tokens, callback, generator, surroundedText != null);
ZenCodingNode node = parser.parse();
if (parser.getIndex() != tokens.size() || node instanceof TextNode) {
return null;
}
return node;
}
private static boolean validate(@NotNull List<ZenCodingToken> tokens, @NotNull ZenCodingGenerator generator) {
for (ZenCodingToken token : tokens) {
if (token instanceof TextToken && !(generator instanceof XmlZenCodingGenerator)) {
return false;
}
}
return true;
}
public static boolean checkTemplateKey(@NotNull String key, CustomTemplateCallback callback, @NotNull ZenCodingGenerator generator) {
return parse(key, callback, generator, null) != null;
}
@Override
public void expand(@NotNull String key, @NotNull CustomTemplateCallback callback) {
ZenCodingGenerator defaultGenerator = findApplicableDefaultGenerator(callback.getContext(), false);
assert defaultGenerator != null;
try {
expand(key, callback, defaultGenerator, Collections.<ZenCodingFilter>emptyList(), true, Registry.intValue("emmet.segments.limit"));
}
catch (EmmetException e) {
CommonRefactoringUtil.showErrorHint(callback.getProject(), callback.getEditor(), e.getMessage(), "Emmet error", "");
}
}
@Nullable
private static ZenCodingGenerator findApplicableGenerator(ZenCodingNode node, PsiElement context, boolean wrapping) {
ZenCodingGenerator defaultGenerator = null;
List<ZenCodingGenerator> generators = ZenCodingGenerator.getInstances();
for (ZenCodingGenerator generator : generators) {
if (defaultGenerator == null && generator.isMyContext(context, wrapping) && generator.isAppliedByDefault(context)) {
defaultGenerator = generator;
}
}
while (node instanceof FilterNode) {
FilterNode filterNode = (FilterNode)node;
String suffix = filterNode.getFilter();
for (ZenCodingGenerator generator : generators) {
if (generator.isMyContext(context, wrapping)) {
if (suffix != null && suffix.equals(generator.getSuffix())) {
return generator;
}
}
}
node = filterNode.getNode();
}
return defaultGenerator;
}
private static List<ZenCodingFilter> getFilters(ZenCodingNode node, PsiElement context) {
List<ZenCodingFilter> result = new ArrayList<ZenCodingFilter>();
while (node instanceof FilterNode) {
FilterNode filterNode = (FilterNode)node;
String filterSuffix = filterNode.getFilter();
boolean filterFound = false;
for (ZenCodingFilter filter : ZenCodingFilter.getInstances()) {
if (filter.isMyContext(context) && filter.getSuffix().equals(filterSuffix)) {
filterFound = true;
result.add(filter);
}
}
assert filterFound;
node = filterNode.getNode();
}
for (ZenCodingFilter filter : ZenCodingFilter.getInstances()) {
if (filter.isMyContext(context) && filter.isAppliedByDefault(context)) {
result.add(filter);
}
}
Collections.reverse(result);
return result;
}
public static void expand(@NotNull String key, @NotNull CustomTemplateCallback callback,
@NotNull ZenCodingGenerator defaultGenerator,
@NotNull Collection<? extends ZenCodingFilter> extraFilters,
boolean expandPrimitiveAbbreviations, int segmentsLimit) throws EmmetException {
final ZenCodingNode node = parse(key, callback, defaultGenerator, null);
if (node == null) {
return;
}
if (node instanceof TemplateNode) {
if (key.equals(((TemplateNode)node).getTemplateToken().getKey()) && callback.findApplicableTemplates(key).size() > 1) {
TemplateManagerImpl templateManager = (TemplateManagerImpl)callback.getTemplateManager();
Map<TemplateImpl, String> template2Argument = templateManager.findMatchingTemplates(callback.getFile(), callback.getEditor(), null, TemplateSettings.getInstance());
Runnable runnable = templateManager.startNonCustomTemplates(template2Argument, callback.getEditor(), null);
if (runnable != null) {
runnable.run();
}
return;
}
}
PsiElement context = callback.getContext();
ZenCodingGenerator generator = findApplicableGenerator(node, context, false);
List<ZenCodingFilter> filters = getFilters(node, context);
filters.addAll(extraFilters);
checkTemplateOutputLength(node, callback);
callback.deleteTemplateKey(key);
expand(node, generator, filters, null, callback, expandPrimitiveAbbreviations, segmentsLimit);
}
private static void expand(ZenCodingNode node,
ZenCodingGenerator generator,
List<ZenCodingFilter> filters,
String surroundedText,
CustomTemplateCallback callback, boolean expandPrimitiveAbbreviations, int segmentsLimit) throws EmmetException {
checkTemplateOutputLength(node, callback);
if (surroundedText != null) {
surroundedText = surroundedText.trim();
}
GenerationNode fakeParentNode = new GenerationNode(TemplateToken.EMPTY_TEMPLATE_TOKEN, -1, 1, surroundedText, true, null);
node.expand(-1, 1, surroundedText, callback, true, fakeParentNode);
if (!expandPrimitiveAbbreviations) {
if (isPrimitiveNode(node)) {
return;
}
}
List<GenerationNode> genNodes = fakeParentNode.getChildren();
LiveTemplateBuilder builder = new LiveTemplateBuilder(segmentsLimit);
int end = -1;
for (int i = 0, genNodesSize = genNodes.size(); i < genNodesSize; i++) {
GenerationNode genNode = genNodes.get(i);
TemplateImpl template = genNode.generate(callback, generator, filters, true, segmentsLimit);
int e = builder.insertTemplate(builder.length(), template, null);
if (i < genNodesSize - 1 && genNode.isInsertNewLineBetweenNodes()) {
builder.insertText(e, "\n", false);
e++;
}
if (end == -1 && end < builder.length()) {
end = e;
}
}
for (ZenCodingFilter filter : filters) {
if (filter instanceof SingleLineEmmetFilter) {
builder.setIsToReformat(false);
break;
}
}
callback.startTemplate(builder.buildTemplate(), null, new TemplateEditingAdapter() {
private TextRange myEndVarRange;
private Editor myEditor;
@Override
public void beforeTemplateFinished(TemplateState state, Template template) {
int variableNumber = state.getCurrentVariableNumber();
if (variableNumber >= 0 && template instanceof TemplateImpl) {
TemplateImpl t = (TemplateImpl)template;
while (variableNumber < t.getVariableCount()) {
String varName = t.getVariableNameAt(variableNumber);
if (LiveTemplateBuilder.isEndVariable(varName)) {
myEndVarRange = state.getVariableRange(varName);
myEditor = state.getEditor();
break;
}
variableNumber++;
}
}
}
@Override
public void templateFinished(Template template, boolean brokenOff) {
if (brokenOff && myEndVarRange != null && myEditor != null) {
int offset = myEndVarRange.getStartOffset();
if (offset >= 0 && offset != myEditor.getCaretModel().getOffset()) {
myEditor.getCaretModel().moveToOffset(offset);
myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
}
}
});
}
private static void checkTemplateOutputLength(ZenCodingNode node, CustomTemplateCallback callback) throws EmmetException {
int predictedOutputLength = node.getApproximateOutputLength(callback);
if (predictedOutputLength > 15 * 1024) {
throw new EmmetException();
}
}
private static boolean isPrimitiveNode(@NotNull ZenCodingNode node) {
if (node instanceof TemplateNode) {
final TemplateToken token = ((TemplateNode)node).getTemplateToken();
if (token != null) {
final List<Couple<String>> attributes = token.getAttribute2Value();
final Couple<String> singleAttribute = ContainerUtil.getFirstItem(attributes);
if (singleAttribute == null || "class".equalsIgnoreCase(singleAttribute.first) && StringUtil.isEmpty(singleAttribute.second)) {
return true;
}
}
}
return false;
}
@Override
public void wrap(@NotNull final String selection, @NotNull final CustomTemplateCallback callback) {
final TextFieldWithStoredHistory field = new TextFieldWithStoredHistory(EMMET_RECENT_WRAP_ABBREVIATIONS_KEY);
final Dimension fieldPreferredSize = field.getPreferredSize();
field.setPreferredSize(new Dimension(Math.max(220, fieldPreferredSize.width), fieldPreferredSize.height));
field.setHistorySize(10);
final JBPopupFactory popupFactory = JBPopupFactory.getInstance();
final BalloonImpl balloon = (BalloonImpl)popupFactory.createDialogBalloonBuilder(field, XmlBundle.message("emmet.title"))
.setCloseButtonEnabled(false)
.setBlockClicksThroughBalloon(true)
.setAnimationCycle(0)
.setHideOnKeyOutside(true)
.createBalloon();
field.addDocumentListener(new DocumentAdapter() {
@Override
protected void textChanged(DocumentEvent e) {
validateTemplateKey(field, balloon, field.getText(), callback);
}
});
field.addKeyboardListener(new KeyAdapter() {
@Override
public void keyPressed(@NotNull KeyEvent e) {
if (!field.isPopupVisible()) {
switch (e.getKeyCode()) {
case KeyEvent.VK_ENTER:
final String abbreviation = field.getText();
if (validateTemplateKey(field, balloon, abbreviation, callback)) {
doWrap(abbreviation, callback);
PropertiesComponent.getInstance().setValue(EMMET_LAST_WRAP_ABBREVIATIONS_KEY, abbreviation);
field.addCurrentTextToHistory();
balloon.hide(true);
}
break;
case KeyEvent.VK_ESCAPE:
balloon.hide(false);
break;
}
}
}
});
IdeEventQueue.getInstance().addDispatcher(new IdeEventQueue.EventDispatcher() {
@Override
public boolean dispatch(AWTEvent e) {
if (e instanceof MouseEvent) {
if (e.getID() == MouseEvent.MOUSE_PRESSED) {
if (!balloon.isInsideBalloon((MouseEvent)e) && !PopupUtil.isComboPopupKeyEvent((ComponentEvent)e, field)) {
balloon.hide();
}
}
}
return false;
}
}, balloon);
balloon.addListener(new JBPopupListener.Adapter() {
@Override
public void beforeShown(LightweightWindowEvent event) {
field.setText(PropertiesComponent.getInstance().getValue(EMMET_LAST_WRAP_ABBREVIATIONS_KEY, ""));
}
});
balloon.show(popupFactory.guessBestPopupLocation(callback.getEditor()), Balloon.Position.below);
final IdeFocusManager focusManager = IdeFocusManager.getInstance(callback.getProject());
focusManager.doWhenFocusSettlesDown(new Runnable() {
@Override
public void run() {
focusManager.requestFocus(field, true);
field.selectText();
}
});
}
private static boolean validateTemplateKey(@NotNull TextFieldWithHistory field,
@Nullable Balloon balloon,
@NotNull String abbreviation,
@NotNull CustomTemplateCallback callback) {
final boolean correct = checkTemplateKey(abbreviation, callback);
field.getTextEditor().setBackground(correct ? LightColors.SLIGHTLY_GREEN : LightColors.RED);
if (balloon != null && !balloon.isDisposed()) {
balloon.revalidate();
}
return correct;
}
static boolean checkTemplateKey(String inputString, CustomTemplateCallback callback) {
ZenCodingGenerator generator = findApplicableDefaultGenerator(callback.getContext(), true);
if (generator == null) {
int offset = callback.getEditor().getCaretModel().getOffset();
LOG.error("Emmet is disabled for context for file " + callback.getFileType().getName() + " in offset: " + offset,
AttachmentFactory.createAttachment(callback.getEditor().getDocument()));
return false;
}
return checkTemplateKey(inputString, callback, generator);
}
@Override
public boolean isApplicable(PsiFile file, int offset, boolean wrapping) {
if (file == null) {
return false;
}
PsiElement element = CustomTemplateCallback.getContext(file, offset);
final ZenCodingGenerator applicableGenerator = findApplicableDefaultGenerator(element, wrapping);
return applicableGenerator != null && applicableGenerator.isEnabled();
}
@Override
public boolean hasCompletionItem(@NotNull PsiFile file, int offset) {
PsiElement element = CustomTemplateCallback.getContext(file, offset);
final ZenCodingGenerator applicableGenerator = findApplicableDefaultGenerator(element, false);
return applicableGenerator != null && applicableGenerator.isEnabled() && applicableGenerator.hasCompletionItem();
}
public static void doWrap(@NotNull final String abbreviation, @NotNull final CustomTemplateCallback callback) {
final ZenCodingGenerator defaultGenerator = findApplicableDefaultGenerator(callback.getContext(), true);
assert defaultGenerator != null;
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
CommandProcessor.getInstance().executeCommand(callback.getProject(), new Runnable() {
@Override
public void run() {
callback.getEditor().getCaretModel().runForEachCaret(new CaretAction() {
@Override
public void perform(Caret caret) {
String selectedText = callback.getEditor().getSelectionModel().getSelectedText();
if (selectedText != null) {
String selection = selectedText.trim();
ZenCodingNode node = parse(abbreviation, callback, defaultGenerator, selection);
assert node != null;
PsiElement context = callback.getContext();
ZenCodingGenerator generator = findApplicableGenerator(node, context, true);
List<ZenCodingFilter> filters = getFilters(node, context);
EditorModificationUtil.deleteSelectedText(callback.getEditor());
PsiDocumentManager.getInstance(callback.getProject()).commitAllDocuments();
try {
expand(node, generator, filters, selection, callback, true, Registry.intValue("emmet.segments.limit"));
}
catch (EmmetException e) {
CommonRefactoringUtil.showErrorHint(callback.getProject(), callback.getEditor(), e.getMessage(), "Emmet error", "");
}
}
}
});
}
}, CodeInsightBundle.message("insert.code.template.command"), null);
}
});
}
@Override
@NotNull
public String getTitle() {
return XmlBundle.message("emmet.title");
}
@Override
public char getShortcut() {
return (char)EmmetOptions.getInstance().getEmmetExpandShortcut();
}
@Override
public String computeTemplateKey(@NotNull CustomTemplateCallback callback) {
ZenCodingGenerator generator = findApplicableDefaultGenerator(callback.getContext(), false);
if (generator == null) return null;
return generator.computeTemplateKey(callback);
}
@Override
public boolean supportsWrapping() {
return true;
}
@Override
public void addCompletions(CompletionParameters parameters, CompletionResultSet result) {
if (!parameters.isAutoPopup()) {
return;
}
PsiFile file = parameters.getPosition().getContainingFile();
int offset = parameters.getOffset();
Editor editor = parameters.getEditor();
ZenCodingGenerator generator = findApplicableDefaultGenerator(CustomTemplateCallback.getContext(file, offset), false);
if (generator != null && generator.hasCompletionItem()) {
final Ref<TemplateImpl> generatedTemplate = new Ref<TemplateImpl>();
final CustomTemplateCallback callback = new CustomTemplateCallback(editor, file) {
@Override
public void deleteTemplateKey(@NotNull String key) {
}
@Override
public void startTemplate(@NotNull Template template, Map<String, String> predefinedValues, TemplateEditingListener listener) {
if (template instanceof TemplateImpl && !((TemplateImpl)template).isDeactivated()) {
generatedTemplate.set((TemplateImpl)template);
}
}
};
final String templatePrefix = computeTemplateKeyWithoutContextChecking(callback);
if (templatePrefix != null) {
List<TemplateImpl> regularTemplates = TemplateManagerImpl.listApplicableTemplates(file, offset, false);
boolean regularTemplateWithSamePrefixExists = !ContainerUtil.filter(regularTemplates, new Condition<TemplateImpl>() {
@Override
public boolean value(TemplateImpl template) {
return templatePrefix.equals(template.getKey());
}
}).isEmpty();
CompletionResultSet resultSet = result.withPrefixMatcher(result.getPrefixMatcher().cloneWithPrefix(templatePrefix));
resultSet.restartCompletionOnPrefixChange(StandardPatterns.string().startsWith(templatePrefix));
if (!regularTemplateWithSamePrefixExists) {
// exclude perfect matches with existing templates because LiveTemplateCompletionContributor handles it
final Collection<SingleLineEmmetFilter> extraFilters = ContainerUtil.newLinkedList(new SingleLineEmmetFilter());
try {
expand(templatePrefix, callback, generator, extraFilters, false, 0);
}
catch (EmmetException e) {
generatedTemplate.set(null);
}
if (!generatedTemplate.isNull()) {
final TemplateImpl template = generatedTemplate.get();
template.setKey(templatePrefix);
template.setDescription(template.getTemplateText());
resultSet.addElement(new CustomLiveTemplateLookupElement(this, template.getKey(), template.getKey(), template.getDescription(),
!LiveTemplateCompletionContributor.shouldShowAllTemplates(), true));
}
}
}
else if(result.getPrefixMatcher().getPrefix().isEmpty()) {
result.restartCompletionOnPrefixChange(StandardPatterns.string().longerThan(0));
}
}
}
}