blob: 1b4e43ec4df1a7ce3f147e077b1a83e4f70480dd [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 org.intellij.plugins.intelliLang.inject;
import com.intellij.codeInsight.completion.CompletionUtil;
import com.intellij.lang.Language;
import com.intellij.lang.injection.MultiHostRegistrar;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.injected.MultiHostRegistrarImpl;
import com.intellij.psi.impl.source.tree.injected.Place;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.Producer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.text.StringSearcher;
import gnu.trove.TIntArrayList;
import org.intellij.plugins.intelliLang.Configuration;
import org.intellij.plugins.intelliLang.inject.config.BaseInjection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Gregory.Shrago
*/
public class InjectorUtils {
public static final Comparator<TextRange> RANGE_COMPARATOR = new Comparator<TextRange>() {
public int compare(final TextRange o1, final TextRange o2) {
if (o1.intersects(o2)) return 0;
return o1.getStartOffset() - o2.getStartOffset();
}
};
private InjectorUtils() {
}
public static boolean registerInjectionSimple(@NotNull PsiLanguageInjectionHost host,
@NotNull BaseInjection injection,
@Nullable LanguageInjectionSupport support,
@NotNull MultiHostRegistrar registrar) {
Language language = InjectedLanguage.findLanguageById(injection.getInjectedLanguageId());
if (language == null) return false;
InjectedLanguage injectedLanguage =
InjectedLanguage.create(injection.getInjectedLanguageId(), injection.getPrefix(), injection.getSuffix(), false);
List<TextRange> ranges = injection.getInjectedArea(host);
List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>> list = ContainerUtil.newArrayListWithCapacity(ranges.size());
for (TextRange range : ranges) {
list.add(Trinity.create(host, injectedLanguage, range));
}
//if (host.getChildren().length > 0) {
// host.putUserData(LanguageInjectionSupport.HAS_UNPARSABLE_FRAGMENTS, Boolean.TRUE);
//}
registerInjection(language, list, host.getContainingFile(), registrar);
if (support != null) {
registerSupport(support, true, registrar);
}
return !ranges.isEmpty();
}
public static void registerInjection(Language language, List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>> list, PsiFile containingFile, MultiHostRegistrar registrar) {
// if language isn't injected when length == 0, subsequent edits will not cause the language to be injected as well.
// Maybe IDEA core is caching a bit too aggressively here?
if (language == null/* && (pair.second.getLength() > 0*/) {
return;
}
boolean injectionStarted = false;
for (Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange> trinity : list) {
final PsiLanguageInjectionHost host = trinity.first;
if (host.getContainingFile() != containingFile) continue;
final TextRange textRange = trinity.third;
final InjectedLanguage injectedLanguage = trinity.second;
if (!injectionStarted) {
registrar.startInjecting(language);
injectionStarted = true;
}
registrar.addPlace(injectedLanguage.getPrefix(), injectedLanguage.getSuffix(), host, textRange);
}
if (injectionStarted) {
registrar.doneInjecting();
}
}
private static final Map<String, LanguageInjectionSupport> ourSupports;
static {
ourSupports = new LinkedHashMap<String, LanguageInjectionSupport>();
for (LanguageInjectionSupport support : Arrays.asList(Extensions.getExtensions(LanguageInjectionSupport.EP_NAME))) {
ourSupports.put(support.getId(), support);
}
}
@NotNull
public static Collection<String> getActiveInjectionSupportIds() {
return ourSupports.keySet();
}
public static Collection<LanguageInjectionSupport> getActiveInjectionSupports() {
return ourSupports.values();
}
@Nullable
public static LanguageInjectionSupport findInjectionSupport(final String id) {
return ourSupports.get(id);
}
@NotNull
public static Class[] getPatternClasses(final String supportId) {
final LanguageInjectionSupport support = findInjectionSupport(supportId);
return support == null ? ArrayUtil.EMPTY_CLASS_ARRAY : support.getPatternClasses();
}
@NotNull
public static LanguageInjectionSupport findNotNullInjectionSupport(final String id) {
final LanguageInjectionSupport result = findInjectionSupport(id);
assert result != null: id+" injector not found";
return result;
}
public static StringBuilder appendStringPattern(@NotNull StringBuilder sb, @NotNull String prefix, @NotNull String text, @NotNull String suffix) {
sb.append(prefix).append("string().");
final String[] parts = text.split("[,|\\s]+");
boolean useMatches = false;
for (String part : parts) {
if (isRegexp(part)) {
useMatches = true;
break;
}
}
if (useMatches) {
sb.append("matches(\"").append(text).append("\")");
}
else if (parts.length > 1) {
sb.append("oneOf(");
boolean first = true;
for (String part : parts) {
if (first) first = false;
else sb.append(", ");
sb.append("\"").append(part).append("\"");
}
sb.append(")");
}
else {
sb.append("equalTo(\"").append(text).append("\")");
}
sb.append(suffix);
return sb;
}
public static boolean isRegexp(final String s) {
boolean hasReChars = false;
for (int i = 0, len = s.length(); i < len; i++) {
final char c = s.charAt(i);
if (c == ' ' || c == '_' || c == '-' || Character.isLetterOrDigit(c)) continue;
hasReChars = true;
break;
}
if (hasReChars) {
try {
new URL(s);
}
catch (MalformedURLException e) {
return true;
}
}
return false;
}
public static void registerSupport(@NotNull LanguageInjectionSupport support, boolean settingsAvailable, @NotNull MultiHostRegistrar registrar) {
putInjectedFileUserData(registrar, LanguageInjectionSupport.INJECTOR_SUPPORT, support);
if (settingsAvailable) {
putInjectedFileUserData(registrar, LanguageInjectionSupport.SETTINGS_EDITOR, support);
}
}
public static <T> void putInjectedFileUserData(MultiHostRegistrar registrar, Key<T> key, T value) {
PsiFile psiFile = getInjectedFile(registrar);
if (psiFile != null) psiFile.putUserData(key, value);
}
public static PsiFile getInjectedFile(MultiHostRegistrar registrar) {
final List<Pair<Place,PsiFile>> result = ((MultiHostRegistrarImpl)registrar).getResult();
return result == null || result.isEmpty() ? null : result.get(result.size() - 1).second;
}
@SuppressWarnings("UnusedParameters")
public static Configuration getEditableInstance(Project project) {
return Configuration.getInstance();
}
public static boolean canBeRemoved(BaseInjection injection) {
if (injection.isEnabled()) return false;
if (StringUtil.isNotEmpty(injection.getPrefix()) || StringUtil.isNotEmpty(injection.getSuffix())) return false;
if (StringUtil.isNotEmpty(injection.getValuePattern())) return false;
return true;
}
@Nullable
public static BaseInjection findCommentInjection(@NotNull PsiElement context, @NotNull String supportId, @Nullable Ref<PsiElement> causeRef) {
PsiElement target = CompletionUtil.getOriginalOrSelf(context);
PsiFile file = target.getContainingFile();
TreeMap<TextRange, BaseInjection> map = getInjectionMap(file);
Map.Entry<TextRange, BaseInjection> entry = map == null ? null : map.lowerEntry(target.getTextRange());
if (entry == null) return null;
PsiComment psiComment = PsiTreeUtil.findElementOfClassAtOffset(file, entry.getKey().getStartOffset(), PsiComment.class, false);
if (psiComment == null) return null;
TextRange r0 = psiComment.getTextRange();
// calulate topmost siblings & heights
PsiElement commonParent = PsiTreeUtil.findCommonParent(psiComment, target);
int h1 = 0, h2 = 0;
PsiElement e1 = psiComment, e2 = target;
for (PsiElement e = e1; e != commonParent; e1 = e, e = e.getParent(), h1++);
for (PsiElement e = e2; e != commonParent; e2 = e, e = e.getParent(), h2++);
// make sure comment is close enough and ...
int off1 = r0.getEndOffset();
int off2 = e2.getTextRange().getStartOffset();
if (off2 - off1 > 120) {
return null;
}
else if (off2 - off1 > 2) {
// ... there's no non-empty valid host in between comment and e2
Producer<PsiElement> producer = prevWalker(e2, commonParent);
PsiElement e;
while ( (e = producer.produce()) != null && e != psiComment) {
if (e instanceof PsiLanguageInjectionHost &&
((PsiLanguageInjectionHost)e).isValidHost() &&
!StringUtil.isEmptyOrSpaces(e.getText())) {
return null;
}
}
}
if (causeRef != null) {
causeRef.set(psiComment);
}
return new BaseInjection(supportId).copyFrom(entry.getValue());
}
@Nullable
private static TreeMap<TextRange, BaseInjection> getInjectionMap(@Nullable final PsiFile file) {
if (file == null) return null; // e.g. null for synthetic groovy variables
return CachedValuesManager.getCachedValue(file, new CachedValueProvider<TreeMap<TextRange, BaseInjection>>() {
@Nullable
@Override
public Result<TreeMap<TextRange, BaseInjection>> compute() {
TreeMap<TextRange, BaseInjection> map = calcInjections(file);
return Result.create(map.isEmpty() ? null : map, file);
}
});
}
@NotNull
protected static TreeMap<TextRange, BaseInjection> calcInjections(PsiFile file) {
final TreeMap<TextRange, BaseInjection> injectionMap = new TreeMap<TextRange, BaseInjection>(RANGE_COMPARATOR);
TIntArrayList ints = new TIntArrayList();
StringSearcher searcher = new StringSearcher("language=", true, true, false);
CharSequence contents = file.getViewProvider().getContents();
final char[] contentsArray = CharArrayUtil.fromSequenceWithoutCopying(contents);
int s0 = 0, s1 = contents.length();
for (int idx = searcher.scan(contents, contentsArray, s0, s1);
idx != -1;
idx = searcher.scan(contents, contentsArray, idx + 1, s1)) {
ints.add(idx);
PsiComment element = PsiTreeUtil.findElementOfClassAtOffset(file, idx, PsiComment.class, false);
if (element != null) {
String str = ElementManipulators.getValueText(element).trim();
BaseInjection injection = detectInjectionFromText("", str);
if (injection != null) {
injectionMap.put(element.getTextRange(), injection);
}
}
}
return injectionMap;
}
private static final Pattern MAP_ENTRY_PATTERN = Pattern.compile("([\\S&&[^=]]+)=(\"(?:[^\"]|\\\\\")*\"|\\S*)");
public static Map<String, String> decodeMap(CharSequence charSequence) {
if (StringUtil.isEmpty(charSequence)) return Collections.emptyMap();
final Matcher matcher = MAP_ENTRY_PATTERN.matcher(charSequence);
final LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
while (matcher.find()) {
map.put(StringUtil.unescapeStringCharacters(matcher.group(1)),
StringUtil.unescapeStringCharacters(StringUtil.unquoteString(matcher.group(2))));
}
return map;
}
@Nullable
public static BaseInjection detectInjectionFromText(String supportId, String text) {
if (text == null || !text.startsWith("language=")) return null;
Map<String, String> map = decodeMap(text);
String languageId = map.get("language");
String prefix = ObjectUtils.notNull(map.get("prefix"), "");
String suffix = ObjectUtils.notNull(map.get("suffix"), "");
BaseInjection injection = new BaseInjection(supportId);
injection.setDisplayName(text);
injection.setInjectedLanguageId(languageId);
injection.setPrefix(prefix);
injection.setSuffix(suffix);
return injection;
}
private static Producer<PsiElement> prevWalker(final PsiElement element, final PsiElement scope) {
return new Producer<PsiElement>() {
PsiElement e = element;
@Nullable
@Override
public PsiElement produce() {
if (e == null || e == scope) return null;
PsiElement prev = e.getPrevSibling();
if (prev != null) {
return e = PsiTreeUtil.getDeepestLast(prev);
}
else {
PsiElement parent = e.getParent();
return e = parent == scope || parent instanceof PsiFile ? null : parent;
}
}
};
}
}