/*
* 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.impl;
import com.intellij.AbstractBundle;
import com.intellij.codeInsight.template.Template;
import com.intellij.ide.plugins.cl.PluginClassLoader;
import com.intellij.openapi.application.ex.DecodeDefaultsUtil;
import com.intellij.openapi.components.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.options.BaseSchemeProcessor;
import com.intellij.openapi.options.SchemesManager;
import com.intellij.openapi.options.SchemesManagerFactory;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.xmlb.Converter;
import com.intellij.util.xmlb.annotations.OptionTag;
import consulo.codeInsight.template.impl.BundleLiveTemplateSetEP;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
@State(
name = "TemplateSettings",
storages = {
@Storage(file = StoragePathMacros.APP_CONFIG + "/other.xml", deprecated = true),
@Storage(file = StoragePathMacros.APP_CONFIG + "/templates.xml")
},
additionalExportFile = TemplateSettings.TEMPLATES_DIR_PATH
)
public class TemplateSettings implements PersistentStateComponent<TemplateSettings.State> {
private static final Logger LOG = Logger.getInstance(TemplateSettings.class);
@NonNls public static final String USER_GROUP_NAME = "user";
@NonNls private static final String TEMPLATE_SET = "templateSet";
@NonNls private static final String GROUP = "group";
@NonNls private static final String TEMPLATE = "template";
public static final char SPACE_CHAR = ' ';
public static final char TAB_CHAR = '\t';
public static final char ENTER_CHAR = '\n';
public static final char DEFAULT_CHAR = 'D';
public static final char CUSTOM_CHAR = 'C';
@NonNls private static final String SPACE = "SPACE";
@NonNls private static final String TAB = "TAB";
@NonNls private static final String ENTER = "ENTER";
@NonNls private static final String CUSTOM = "CUSTOM";
@NonNls private static final String NAME = "name";
@NonNls private static final String VALUE = "value";
@NonNls private static final String DESCRIPTION = "description";
@NonNls private static final String SHORTCUT = "shortcut";
@NonNls private static final String VARIABLE = "variable";
@NonNls private static final String EXPRESSION = "expression";
@NonNls private static final String DEFAULT_VALUE = "defaultValue";
@NonNls private static final String ALWAYS_STOP_AT = "alwaysStopAt";
@NonNls private static final String CONTEXT = "context";
@NonNls private static final String TO_REFORMAT = "toReformat";
@NonNls private static final String TO_SHORTEN_FQ_NAMES = "toShortenFQNames";
@NonNls private static final String USE_STATIC_IMPORT = "useStaticImport";
@NonNls private static final String DEACTIVATED = "deactivated";
@NonNls private static final String RESOURCE_BUNDLE = "resource-bundle";
@NonNls private static final String KEY = "key";
@NonNls private static final String ID = "id";
static final String TEMPLATES_DIR_PATH = StoragePathMacros.ROOT_CONFIG + "/templates";
private final MultiMap<String, TemplateImpl> myTemplates = MultiMap.createLinked();
private final Map<String, Template> myTemplatesById = new LinkedHashMap<String, Template>();
private final Map<TemplateKey, TemplateImpl> myDefaultTemplates = new LinkedHashMap<TemplateKey, TemplateImpl>();
private int myMaxKeyLength = 0;
private final SchemesManager<TemplateGroup, TemplateGroup> mySchemesManager;
private State myState = new State();
static final class ShortcutConverter extends Converter<Character> {
@Nullable
@Override
public Character fromString(@NotNull String shortcut) {
return TAB.equals(shortcut) ? TAB_CHAR :
ENTER.equals(shortcut) ? ENTER_CHAR :
CUSTOM.equals(shortcut) ? CUSTOM_CHAR :
SPACE_CHAR;
}
@NotNull
@Override
public String toString(@NotNull Character shortcut) {
return shortcut == TAB_CHAR ? TAB :
shortcut == ENTER_CHAR ? ENTER :
shortcut == CUSTOM_CHAR ? CUSTOM :
SPACE;
}
}
final static class State {
@OptionTag(nameAttribute = "", valueAttribute = "shortcut", converter = ShortcutConverter.class)
public char defaultShortcut = TAB_CHAR;
public List<TemplateSettings.TemplateKey> deletedKeys = new SmartList<TemplateKey>();
}
public static class TemplateKey {
private String groupName;
private String key;
@SuppressWarnings("UnusedDeclaration")
public TemplateKey() {}
private TemplateKey(String groupName, String key) {
this.groupName = groupName;
this.key = key;
}
public static TemplateKey keyOf(TemplateImpl template) {
return new TemplateKey(template.getGroupName(), template.getKey());
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TemplateKey that = (TemplateKey)o;
return Comparing.equal(groupName, that.groupName) && Comparing.equal(key, that.key);
}
public int hashCode() {
int result = groupName != null ? groupName.hashCode() : 0;
result = 31 * result + (key != null ? key.hashCode() : 0);
return result;
}
public String getGroupName() {
return groupName;
}
@SuppressWarnings("UnusedDeclaration")
public void setGroupName(String groupName) {
this.groupName = groupName;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
@Override
public String toString() {
return getKey() + "@" + getGroupName();
}
}
private TemplateKey myLastSelectedTemplate;
public TemplateSettings(SchemesManagerFactory schemesManagerFactory) {
mySchemesManager = schemesManagerFactory.createSchemesManager(TEMPLATES_DIR_PATH, new BaseSchemeProcessor<TemplateGroup>() {
@Override
@Nullable
public TemplateGroup readScheme(@NotNull final Document schemeContent) throws InvalidDataException {
return readTemplateFile(schemeContent, schemeContent.getRootElement().getAttributeValue("group"), false, false,
getClass().getClassLoader());
}
@Override
public boolean shouldBeSaved(@NotNull final TemplateGroup template) {
for (TemplateImpl t : template.getElements()) {
if (differsFromDefault(t)) {
return true;
}
}
return false;
}
@Override
public Element writeScheme(@NotNull TemplateGroup template) {
Element templateSetElement = new Element(TEMPLATE_SET);
templateSetElement.setAttribute(GROUP, template.getName());
for (TemplateImpl t : template.getElements()) {
if (differsFromDefault(t)) {
saveTemplate(t, templateSetElement);
}
}
return templateSetElement;
}
@Override
public void initScheme(@NotNull final TemplateGroup scheme) {
for (TemplateImpl template : scheme.getElements()) {
addTemplateImpl(template);
}
}
@Override
public void onSchemeAdded(@NotNull final TemplateGroup scheme) {
for (TemplateImpl template : scheme.getElements()) {
addTemplateImpl(template);
}
}
@Override
public void onSchemeDeleted(@NotNull final TemplateGroup scheme) {
for (TemplateImpl template : scheme.getElements()) {
removeTemplate(template);
}
}
}, RoamingType.PER_USER);
for (TemplateGroup group : mySchemesManager.loadSchemes()) {
for (TemplateImpl template : group.getElements()) {
addTemplateImpl(template);
}
}
loadTemplates();
}
public static TemplateSettings getInstance() {
return ServiceManager.getService(TemplateSettings.class);
}
private boolean differsFromDefault(TemplateImpl t) {
TemplateImpl def = getDefaultTemplate(t);
return def == null || !t.equals(def) || !t.contextsEqual(def);
}
@Nullable
public TemplateImpl getDefaultTemplate(TemplateImpl t) {
return myDefaultTemplates.get(TemplateKey.keyOf(t));
}
@Override
public State getState() {
return myState;
}
@Override
public void loadState(State state) {
myState = state;
applyNewDeletedTemplates();
}
void applyNewDeletedTemplates() {
for (TemplateKey templateKey : myState.deletedKeys) {
if (templateKey.groupName == null) {
for (TemplateImpl template : new ArrayList<TemplateImpl>(myTemplates.get(templateKey.key))) {
removeTemplate(template);
}
}
else {
TemplateImpl toDelete = getTemplate(templateKey.key, templateKey.groupName);
if (toDelete != null) {
removeTemplate(toDelete);
}
}
}
}
@Nullable
public String getLastSelectedTemplateKey() {
return myLastSelectedTemplate != null ? myLastSelectedTemplate.key : null;
}
@Nullable
public String getLastSelectedTemplateGroup() {
return myLastSelectedTemplate != null ? myLastSelectedTemplate.groupName : null;
}
public void setLastSelectedTemplate(@Nullable String group, @Nullable String key) {
myLastSelectedTemplate = group == null ? null : new TemplateKey(group, key);
}
@SuppressWarnings("unused")
public Collection<? extends TemplateImpl> getTemplatesAsList() {
return myTemplates.values();
}
public TemplateImpl[] getTemplates() {
final Collection<? extends TemplateImpl> all = myTemplates.values();
return all.toArray(new TemplateImpl[all.size()]);
}
public char getDefaultShortcutChar() {
return myState.defaultShortcut;
}
public void setDefaultShortcutChar(char defaultShortcutChar) {
myState.defaultShortcut = defaultShortcutChar;
}
public Collection<TemplateImpl> getTemplates(@NonNls String key) {
return myTemplates.get(key);
}
@Nullable
public TemplateImpl getTemplate(@NonNls String key, String group) {
final Collection<TemplateImpl> templates = myTemplates.get(key);
for (TemplateImpl template : templates) {
if (template.getGroupName().equals(group)) {
return template;
}
}
return null;
}
public Template getTemplateById(@NonNls String id) {
return myTemplatesById.get(id);
}
public int getMaxKeyLength() {
return myMaxKeyLength;
}
public void addTemplate(Template template) {
clearPreviouslyRegistered(template);
addTemplateImpl(template);
TemplateImpl templateImpl = (TemplateImpl)template;
String groupName = templateImpl.getGroupName();
TemplateGroup group = mySchemesManager.findSchemeByName(groupName);
if (group == null) {
group = new TemplateGroup(groupName);
mySchemesManager.addNewScheme(group, true);
}
group.addElement(templateImpl);
}
private void clearPreviouslyRegistered(final Template template) {
TemplateImpl existing = getTemplate(template.getKey(), ((TemplateImpl) template).getGroupName());
if (existing != null) {
LOG.info("Template with key " + template.getKey() + " and id " + template.getId() + " already registered");
TemplateGroup group = mySchemesManager.findSchemeByName(existing.getGroupName());
if (group != null) {
group.removeElement(existing);
if (group.isEmpty()) {
mySchemesManager.removeScheme(group);
}
}
myTemplates.remove(template.getKey(), existing);
}
}
private void addTemplateImpl(@NotNull Template template) {
TemplateImpl templateImpl = (TemplateImpl)template;
if (getTemplate(templateImpl.getKey(), templateImpl.getGroupName()) == null) {
myTemplates.putValue(template.getKey(), templateImpl);
}
myMaxKeyLength = Math.max(myMaxKeyLength, template.getKey().length());
myState.deletedKeys.remove(TemplateKey.keyOf((TemplateImpl)template));
}
private void addTemplateById(Template template) {
if (!myTemplatesById.containsKey(template.getId())) {
final String id = template.getId();
if (id != null) {
myTemplatesById.put(id, template);
}
}
}
public void removeTemplate(@NotNull Template template) {
myTemplates.remove(template.getKey(), (TemplateImpl)template);
TemplateGroup group = mySchemesManager.findSchemeByName(((TemplateImpl)template).getGroupName());
if (group != null) {
group.removeElement((TemplateImpl)template);
if (group.isEmpty()) {
mySchemesManager.removeScheme(group);
}
}
}
private TemplateImpl addTemplate(String key, String string, String group, String description, String shortcut, boolean isDefault,
final String id) {
TemplateImpl template = new TemplateImpl(key, string, group);
template.setId(id);
template.setDescription(description);
if (TAB.equals(shortcut)) {
template.setShortcutChar(TAB_CHAR);
}
else if (ENTER.equals(shortcut)) {
template.setShortcutChar(ENTER_CHAR);
}
else if (SPACE.equals(shortcut)) {
template.setShortcutChar(SPACE_CHAR);
}
else {
template.setShortcutChar(DEFAULT_CHAR);
}
if (isDefault) {
myDefaultTemplates.put(TemplateKey.keyOf(template), template);
}
return template;
}
private void loadTemplates() {
loadBundledLiveTemplateSets();
loadDefaultLiveTemplates();
}
private void loadBundledLiveTemplateSets() {
try {
for (BundleLiveTemplateSetEP it : BundleLiveTemplateSetEP.EP_NAME.getExtensions()) {
ClassLoader loaderForClass = it.getLoaderForClass();
InputStream inputStream = loaderForClass.getResourceAsStream(it.path + ".xml");
if (inputStream != null) {
TemplateGroup group = readTemplateFile(JDOMUtil.loadDocument(inputStream), it.path, true, it.register, loaderForClass);
if (group != null && group.getReplace() != null) {
Collection<TemplateImpl> templates = myTemplates.get(group.getReplace());
for (TemplateImpl template : templates) {
removeTemplate(template);
}
}
}
else {
LOG.warn("Cannot find path for '" + it.path + "'. Plugin: " + it.getPluginDescriptor().getPluginId());
}
}
}
catch (Exception e) {
LOG.error(e);
}
}
@Deprecated
@SuppressWarnings("deprecation")
private void loadDefaultLiveTemplates() {
try {
for (DefaultLiveTemplatesProvider provider : DefaultLiveTemplatesProvider.EP_NAME.getExtensions()) {
for (String defTemplate : provider.getDefaultLiveTemplateFiles()) {
readDefTemplate(provider, defTemplate, true);
}
try {
String[] hidden = provider.getHiddenLiveTemplateFiles();
if (hidden != null) {
for (String s : hidden) {
readDefTemplate(provider, s, false);
}
}
}
catch (AbstractMethodError ignore) {
}
}
}
catch (Exception e) {
LOG.error(e);
}
}
@Deprecated
@SuppressWarnings("deprecation")
private void readDefTemplate(DefaultLiveTemplatesProvider provider, String defTemplate, boolean registerTemplate)
throws JDOMException, InvalidDataException, IOException {
InputStream inputStream = DecodeDefaultsUtil.getDefaultsInputStream(provider, defTemplate);
if (inputStream != null) {
TemplateGroup group =
readTemplateFile(JDOMUtil.loadDocument(inputStream), defTemplate, true, registerTemplate, provider.getClass().getClassLoader());
if (group != null && group.getReplace() != null) {
Collection<TemplateImpl> templates = myTemplates.get(group.getReplace());
for (TemplateImpl template : templates) {
removeTemplate(template);
}
}
}
}
private static String getDefaultTemplateName(String defTemplate) {
return defTemplate.substring(defTemplate.lastIndexOf('/') + 1);
}
@Nullable
private TemplateGroup readTemplateFile(Document document, @NonNls String path, boolean isDefault, boolean registerTemplate,
ClassLoader classLoader) throws InvalidDataException {
if (document == null) {
throw new InvalidDataException();
}
Element root = document.getRootElement();
if (root == null || !TEMPLATE_SET.equals(root.getName())) {
throw new InvalidDataException();
}
String groupName = root.getAttributeValue(GROUP);
if (StringUtil.isEmpty(groupName)) {
groupName = path.substring(path.lastIndexOf("/") + 1);
LOG.warn("Group attribute is empty. Path '" + path + "'. Plugin: " + ((PluginClassLoader)classLoader).getPluginId());
}
TemplateGroup result = new TemplateGroup(groupName, root.getAttributeValue("REPLACE"));
Map<String, TemplateImpl> created = new LinkedHashMap<String, TemplateImpl>();
for (final Element element : root.getChildren(TEMPLATE)) {
TemplateImpl template = readTemplateFromElement(isDefault, groupName, element, classLoader);
TemplateImpl existing = getTemplate(template.getKey(), template.getGroupName());
boolean defaultTemplateModified = isDefault && (myState.deletedKeys.contains(TemplateKey.keyOf(template)) ||
myTemplatesById.containsKey(template.getId()) ||
existing != null);
if(!defaultTemplateModified) {
created.put(template.getKey(), template);
}
if (isDefault && existing != null) {
existing.getTemplateContext().setDefaultContext(template.getTemplateContext());
}
}
if (registerTemplate) {
TemplateGroup existingScheme = mySchemesManager.findSchemeByName(result.getName());
if (existingScheme != null) {
result = existingScheme;
}
}
for (TemplateImpl template : created.values()) {
if (registerTemplate) {
clearPreviouslyRegistered(template);
addTemplateImpl(template);
}
addTemplateById(template);
result.addElement(template);
}
if (registerTemplate) {
TemplateGroup existingScheme = mySchemesManager.findSchemeByName(result.getName());
if (existingScheme == null && !result.isEmpty()) {
mySchemesManager.addNewScheme(result, false);
}
}
return result.isEmpty() ? null : result;
}
private TemplateImpl readTemplateFromElement(final boolean isDefault,
final String groupName,
final Element element,
ClassLoader classLoader) throws InvalidDataException {
String name = element.getAttributeValue(NAME);
String value = element.getAttributeValue(VALUE);
String description;
String resourceBundle = element.getAttributeValue(RESOURCE_BUNDLE);
String key = element.getAttributeValue(KEY);
String id = element.getAttributeValue(ID);
if (resourceBundle != null && key != null) {
if (classLoader == null) {
classLoader = getClass().getClassLoader();
}
ResourceBundle bundle = AbstractBundle.getResourceBundle(resourceBundle, classLoader);
description = bundle.getString(key);
}
else {
description = element.getAttributeValue(DESCRIPTION);
}
String shortcut = element.getAttributeValue(SHORTCUT);
TemplateImpl template = addTemplate(name, value, groupName, description, shortcut, isDefault, id);
template.setToReformat(Boolean.parseBoolean(element.getAttributeValue(TO_REFORMAT)));
template.setToShortenLongNames(Boolean.parseBoolean(element.getAttributeValue(TO_SHORTEN_FQ_NAMES)));
template.setDeactivated(Boolean.parseBoolean(element.getAttributeValue(DEACTIVATED)));
String useStaticImport = element.getAttributeValue(USE_STATIC_IMPORT);
if (useStaticImport != null) {
template.setValue(TemplateImpl.Property.USE_STATIC_IMPORT_IF_POSSIBLE, Boolean.parseBoolean(useStaticImport));
}
for (final Object o : element.getChildren(VARIABLE)) {
Element e = (Element)o;
String variableName = e.getAttributeValue(NAME);
String expression = e.getAttributeValue(EXPRESSION);
String defaultValue = e.getAttributeValue(DEFAULT_VALUE);
boolean isAlwaysStopAt = Boolean.parseBoolean(e.getAttributeValue(ALWAYS_STOP_AT));
template.addVariable(variableName, expression, defaultValue, isAlwaysStopAt);
}
Element context = element.getChild(CONTEXT);
if (context != null) {
template.getTemplateContext().readTemplateContext(context);
}
return template;
}
private void saveTemplate(TemplateImpl template, Element templateSetElement) {
Element element = new Element(TEMPLATE);
final String id = template.getId();
if (id != null) {
element.setAttribute(ID, id);
}
element.setAttribute(NAME, template.getKey());
element.setAttribute(VALUE, template.getString());
if (template.getShortcutChar() == TAB_CHAR) {
element.setAttribute(SHORTCUT, TAB);
} else if (template.getShortcutChar() == ENTER_CHAR) {
element.setAttribute(SHORTCUT, ENTER);
} else if (template.getShortcutChar() == SPACE_CHAR) {
element.setAttribute(SHORTCUT, SPACE);
}
if (template.getDescription() != null) {
element.setAttribute(DESCRIPTION, template.getDescription());
}
element.setAttribute(TO_REFORMAT, Boolean.toString(template.isToReformat()));
element.setAttribute(TO_SHORTEN_FQ_NAMES, Boolean.toString(template.isToShortenLongNames()));
if (template.getValue(Template.Property.USE_STATIC_IMPORT_IF_POSSIBLE)
!= Template.getDefaultValue(Template.Property.USE_STATIC_IMPORT_IF_POSSIBLE))
{
element.setAttribute(USE_STATIC_IMPORT, Boolean.toString(template.getValue(Template.Property.USE_STATIC_IMPORT_IF_POSSIBLE)));
}
if (template.isDeactivated()) {
element.setAttribute(DEACTIVATED, Boolean.toString(true));
}
for (int i = 0; i < template.getVariableCount(); i++) {
Element variableElement = new Element(VARIABLE);
variableElement.setAttribute(NAME, template.getVariableNameAt(i));
variableElement.setAttribute(EXPRESSION, template.getExpressionStringAt(i));
variableElement.setAttribute(DEFAULT_VALUE, template.getDefaultValueStringAt(i));
variableElement.setAttribute(ALWAYS_STOP_AT, Boolean.toString(template.isAlwaysStopAt(i)));
element.addContent(variableElement);
}
try {
Element contextElement = new Element(CONTEXT);
TemplateImpl def = getDefaultTemplate(template);
template.getTemplateContext().writeTemplateContext(contextElement, def == null ? null : def.getTemplateContext());
element.addContent(contextElement);
} catch (WriteExternalException ignore) {
}
templateSetElement.addContent(element);
}
public void setTemplates(@NotNull List<TemplateGroup> newGroups) {
myTemplates.clear();
myState.deletedKeys.clear();
for (TemplateImpl template : myDefaultTemplates.values()) {
myState.deletedKeys.add(TemplateKey.keyOf(template));
}
mySchemesManager.clearAllSchemes();
myMaxKeyLength = 0;
for (TemplateGroup group : newGroups) {
if (!group.isEmpty()) {
mySchemesManager.addNewScheme(group, true);
for (TemplateImpl template : group.getElements()) {
clearPreviouslyRegistered(template);
addTemplateImpl(template);
}
}
}
}
public SchemesManager<TemplateGroup,TemplateGroup> getSchemesManager() {
return mySchemesManager;
}
public List<TemplateGroup> getTemplateGroups() {
return mySchemesManager.getAllSchemes();
}
public List<TemplateImpl> collectMatchingCandidates(String key, @Nullable Character shortcutChar, boolean hasArgument) {
final Collection<TemplateImpl> templates = getTemplates(key);
List<TemplateImpl> candidates = new ArrayList<TemplateImpl>();
for (TemplateImpl template : templates) {
if (template.isDeactivated()) {
continue;
}
if (shortcutChar != null && getShortcutChar(template) != shortcutChar) {
continue;
}
if (hasArgument && !template.hasArgument()) {
continue;
}
candidates.add(template);
}
return candidates;
}
public char getShortcutChar(TemplateImpl template) {
char c = template.getShortcutChar();
return c == DEFAULT_CHAR ? getDefaultShortcutChar() : c;
}
public List<TemplateKey> getDeletedTemplates() {
return myState.deletedKeys;
}
public void reset() {
myState.deletedKeys.clear();
loadDefaultLiveTemplates();
}
}