un-inject language action in Java fix
[fedora-idea.git] / plugins / IntelliLang / src / org / intellij / plugins / intelliLang / inject / CustomLanguageInjector.java
blobdf065c0728bd2283ddf6b2272a49d483d1a95a85
1 /*
2 * Copyright 2006 Sascha Weinreuter
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package org.intellij.plugins.intelliLang.inject;
19 import com.intellij.lang.Language;
20 import com.intellij.lang.injection.InjectedLanguageManager;
21 import com.intellij.lang.injection.MultiHostInjector;
22 import com.intellij.lang.injection.MultiHostRegistrar;
23 import com.intellij.openapi.components.ProjectComponent;
24 import com.intellij.openapi.extensions.Extensions;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.util.*;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.psi.*;
29 import com.intellij.psi.filters.TrueFilter;
30 import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry;
31 import com.intellij.psi.xml.*;
32 import com.intellij.util.PairProcessor;
33 import com.intellij.util.containers.ContainerUtil;
34 import com.intellij.xml.util.XmlUtil;
35 import gnu.trove.THashSet;
36 import org.intellij.plugins.intelliLang.Configuration;
37 import org.intellij.plugins.intelliLang.inject.config.XmlAttributeInjection;
38 import org.intellij.plugins.intelliLang.inject.config.XmlTagInjection;
39 import org.intellij.plugins.intelliLang.util.PsiUtilEx;
40 import org.jetbrains.annotations.NotNull;
42 import java.util.*;
43 import java.util.concurrent.CopyOnWriteArrayList;
45 /**
46 * This is the main part of the injection code. The component registers a language injector, the reference provider that
47 * supplies completions for language-IDs and regular expression enum-values as well as the Quick Edit action.
48 * <p/>
49 * The injector obtains the static injection configuration for each XML tag, attribute or String literal and also
50 * dynamically computes the prefix/suffix for the language fragment from binary expressions.
51 * <p/>
52 * It also tries to deal with the "glued token" problem by removing or adding whitespace to the prefix/suffix.
54 public final class CustomLanguageInjector implements ProjectComponent {
55 private static final Comparator<TextRange> RANGE_COMPARATOR = new Comparator<TextRange>() {
56 public int compare(final TextRange o1, final TextRange o2) {
57 if (o1.intersects(o2)) return 0;
58 return o1.getStartOffset() - o2.getStartOffset();
62 private final Project myProject;
63 private final Configuration myInjectionConfiguration;
65 @SuppressWarnings({"unchecked"})
66 private final List<Pair<SmartPsiElementPointer<PsiLanguageInjectionHost>, InjectedLanguage>> myTempPlaces = new CopyOnWriteArrayList<Pair<SmartPsiElementPointer<PsiLanguageInjectionHost>, InjectedLanguage>>();
67 static final Key<Boolean> HAS_UNPARSABLE_FRAGMENTS = Key.create("HAS_UNPARSABLE_FRAGMENTS");
68 private final MyLanguageInjector myInjector = new MyLanguageInjector(this);
69 private final LanguageReferenceProvider myProvider = new LanguageReferenceProvider();
71 public CustomLanguageInjector(Project project, Configuration configuration) {
72 myProject = project;
73 myInjectionConfiguration = configuration;
76 public void initComponent() {
77 InjectedLanguageManager.getInstance(myProject).registerMultiHostInjector(myInjector);
78 ReferenceProvidersRegistry.getInstance(myProject).registerReferenceProvider(TrueFilter.INSTANCE, PsiLiteralExpression.class, myProvider);
80 public void disposeComponent() {
81 InjectedLanguageManager.getInstance(myProject).unregisterMultiHostInjector(myInjector);
82 ReferenceProvidersRegistry.getInstance(myProject).unregisterReferenceProvider(PsiLiteralExpression.class, myProvider);
85 private void getInjectedLanguage(final PsiElement place, final PairProcessor<Language, List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>>> processor) {
86 // optimization
87 if (place instanceof PsiLiteralExpression && !PsiUtilEx.isStringOrCharacterLiteral(place)) return;
89 myTempPlaces.removeAll(ContainerUtil.findAll(myTempPlaces, new Condition<Pair<SmartPsiElementPointer<PsiLanguageInjectionHost>, InjectedLanguage>>() {
90 public boolean value(final Pair<SmartPsiElementPointer<PsiLanguageInjectionHost>, InjectedLanguage> pair) {
91 return pair.first.getElement() == null;
93 }));
94 for (final Pair<SmartPsiElementPointer<PsiLanguageInjectionHost>, InjectedLanguage> pair : myTempPlaces) {
95 if (pair.first.getElement() == place) {
96 final PsiLanguageInjectionHost host = (PsiLanguageInjectionHost)place;
97 processor.process(pair.second.getLanguage(), Collections.singletonList(
98 Trinity.create(host, pair.second, ElementManipulators.getManipulator(host).getRangeInElement(host))));
99 return;
103 if (place instanceof XmlTag) {
104 final XmlTag xmlTag = (XmlTag)place;
105 for (final XmlTagInjection injection : myInjectionConfiguration.getTagInjections()) {
106 if (injection.isApplicable(xmlTag)) {
107 final Language language = InjectedLanguage.findLanguageById(injection.getInjectedLanguageId());
108 if (language == null) continue;
109 final boolean separateFiles = !injection.isSingleFile() && StringUtil.isNotEmpty(injection.getValuePattern());
111 final Ref<Boolean> hasSubTags = Ref.create(Boolean.FALSE);
112 final List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>> result = new ArrayList<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>>();
114 xmlTag.acceptChildren(new PsiElementVisitor() {
115 @Override
116 public void visitElement(final PsiElement element) {
117 if (element instanceof XmlText) {
118 if (element.getTextLength() == 0) return;
119 final List<TextRange> list = injection.getInjectedArea((XmlText)element);
120 final InjectedLanguage l = InjectedLanguage.create(injection.getInjectedLanguageId(), injection.getPrefix(), injection.getSuffix(), false);
121 for (TextRange textRange : list) {
122 result.add(Trinity.create((PsiLanguageInjectionHost)element, l, textRange));
125 else if (element instanceof XmlTag) {
126 hasSubTags.set(Boolean.TRUE);
127 if (injection.isApplyToSubTagTexts()) {
128 element.acceptChildren(this);
133 if (!result.isEmpty()) {
134 if (separateFiles) {
135 for (Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange> trinity : result) {
136 processor.process(language, Collections.singletonList(trinity));
139 else {
140 for (Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange> trinity : result) {
141 trinity.first.putUserData(HAS_UNPARSABLE_FRAGMENTS, hasSubTags.get());
143 processor.process(language, result);
146 if (injection.isTerminal()) {
147 break;
152 else if (place instanceof XmlAttributeValue) {
153 final XmlAttributeValue value = (XmlAttributeValue)place;
155 // Check that we don't inject anything into embedded (e.g. JavaScript) content:
156 // XmlToken: "
157 // JSEmbeddedContent
158 // XmlToken "
160 // Actually IDEA shouldn't ask for injected languages at all in this case.
161 final PsiElement[] children = value.getChildren();
162 if (children.length < 3 || !(children[1] instanceof XmlToken) ||
163 ((XmlToken)children[1]).getTokenType() != XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) {
164 return;
167 for (XmlAttributeInjection injection : myInjectionConfiguration.getAttributeInjections()) {
168 if (injection.isApplicable(value)) {
169 final Language language = InjectedLanguage.findLanguageById(injection.getInjectedLanguageId());
170 if (language == null) continue;
171 final boolean separateFiles = !injection.isSingleFile() && StringUtil.isNotEmpty(injection.getValuePattern());
173 final List<TextRange> ranges = injection.getInjectedArea(value);
174 if (ranges.isEmpty()) continue;
175 final List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>> result = new ArrayList<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>>();
176 final InjectedLanguage l = InjectedLanguage.create(injection.getInjectedLanguageId(), injection.getPrefix(), injection.getSuffix(), false);
177 for (TextRange textRange : ranges) {
178 result.add(Trinity.create((PsiLanguageInjectionHost)value, l, textRange));
180 if (separateFiles) {
181 for (Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange> trinity : result) {
182 processor.process(language, Collections.singletonList(trinity));
185 else {
186 processor.process(language, result);
188 if (injection.isTerminal()) {
189 break;
194 else {
195 for (CustomLanguageInjectorExtension o : Extensions.getExtensions(CustomLanguageInjectorExtension.EP_NAME)) {
196 o.getInjectedLanguage(myInjectionConfiguration, place, processor);
201 @NotNull
202 public String getComponentName() {
203 return "IntelliLang.CustomLanguageInjector";
206 public void projectOpened() {
209 public void projectClosed() {
212 public void addTempInjection(PsiLanguageInjectionHost host, InjectedLanguage selectedValue) {
213 final SmartPointerManager manager = SmartPointerManager.getInstance(myProject);
214 final SmartPsiElementPointer<PsiLanguageInjectionHost> pointer = manager.createSmartPsiElementPointer(host);
216 myTempPlaces.add(Pair.create(pointer, selectedValue));
219 private static class MyLanguageInjector implements MultiHostInjector {
220 private final CustomLanguageInjector myInjector;
222 MyLanguageInjector(CustomLanguageInjector injector) {
223 myInjector = injector;
227 @NotNull
228 public List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
229 final THashSet<Class<? extends PsiElement>> elements = new THashSet<Class<? extends PsiElement>>();
230 for (CustomLanguageInjectorExtension o : Extensions.getExtensions(CustomLanguageInjectorExtension.EP_NAME)) {
231 o.elementsToInjectIn(elements);
233 elements.addAll(Arrays.asList(PsiLiteralExpression.class, XmlTag.class, XmlAttributeValue.class, PsiBinaryExpression.class));
234 return Arrays.<Class<? extends PsiElement>>asList(elements.toArray(new Class[elements.size()]));
237 public void getLanguagesToInject(@NotNull final MultiHostRegistrar registrar, @NotNull PsiElement host) {
238 final TreeSet<TextRange> ranges = new TreeSet<TextRange>(RANGE_COMPARATOR);
239 final PsiFile containingFile = host.getContainingFile();
240 myInjector.getInjectedLanguage(host, new PairProcessor<Language, List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>>>() {
241 public boolean process(final Language language, List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>> list) {
242 for (Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange> trinity : list) {
243 if (ranges.contains(trinity.third.shiftRight(trinity.first.getTextRange().getStartOffset()))) return true;
245 for (Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange> trinity : list) {
246 final PsiLanguageInjectionHost host = trinity.first;
247 if (host.getContainingFile() != containingFile) continue;
248 final TextRange textRange = trinity.third;
249 ranges.add(textRange.shiftRight(host.getTextRange().getStartOffset()));
251 registerInjection(language, list, containingFile, registrar);
252 return true;
257 private static void addPlaceSafe(MultiHostRegistrar registrar, String prefix, String suffix, PsiLanguageInjectionHost host, TextRange textRange,
258 Language language) {
259 registrar.addPlace(prefix, suffix, host, textRange);
262 private static String getUnescapedText(final PsiElement host, final String text) {
263 if (host instanceof PsiLiteralExpression) {
264 return StringUtil.unescapeStringCharacters(text);
266 else if (host instanceof XmlElement) {
267 return XmlUtil.unescape(text);
269 else {
270 return text;
274 // Avoid sticking text and prefix/suffix together in a way that it would form a single token.
275 // See http://www.jetbrains.net/jira/browse/IDEADEV-8302#action_111865
276 // This code assumes that for the injected language a single space character is a token separator
277 // that doesn't (significantly) change the semantics if added to the prefix/suffix
279 // NOTE: This does not work in all cases, such as string literals in JavaScript where a
280 // space character isn't a token separator. See also comments in IDEA-8561
281 private static void adjustPrefixAndSuffix(String text, StringBuilder prefix, StringBuilder suffix) {
282 if (prefix.length() > 0) {
283 if (!endsWithSpace(prefix) && !startsWithSpace(text)) {
284 prefix.append(" ");
286 else if (endsWithSpace(prefix) && startsWithSpace(text)) {
287 trim(prefix);
290 if (suffix.length() > 0) {
291 if (text.length() == 0) {
292 // avoid to stick whitespace from prefix and suffix together
293 trim(suffix);
295 else if (!startsWithSpace(suffix) && !endsWithSpace(text)) {
296 suffix.insert(0, " ");
298 else if (startsWithSpace(suffix) && endsWithSpace(text)) {
299 trim(suffix);
304 private static void trim(StringBuilder string) {
305 while (startsWithSpace(string)) string.deleteCharAt(0);
306 while (endsWithSpace(string)) string.deleteCharAt(string.length() - 1);
309 private static boolean startsWithSpace(CharSequence sequence) {
310 final int length = sequence.length();
311 return length > 0 && sequence.charAt(0) <= ' ';
314 private static boolean endsWithSpace(CharSequence sequence) {
315 final int length = sequence.length();
316 return length > 0 && sequence.charAt(length - 1) <= ' ';
319 static void registerInjection(Language language, List<Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange>> list, PsiFile containingFile, MultiHostRegistrar registrar) {
320 // if language isn't injected when length == 0, subsequent edits will not cause the language to be injected as well.
321 // Maybe IDEA core is caching a bit too aggressively here?
322 if (language == null/* && (pair.second.getLength() > 0*/) {
323 return;
325 boolean injectionStarted = false;
326 for (Trinity<PsiLanguageInjectionHost, InjectedLanguage, TextRange> trinity : list) {
327 final PsiLanguageInjectionHost host = trinity.first;
328 if (host.getContainingFile() != containingFile) continue;
330 final TextRange textRange = trinity.third;
331 final InjectedLanguage injectedLanguage = trinity.second;
333 if (!injectionStarted) {
334 registrar.startInjecting(language);
335 injectionStarted = true;
337 if (injectedLanguage.isDynamic()) {
338 // Only adjust prefix/suffix if it has been computed dynamically. Otherwise some other
339 // useful cases may break. This system is far from perfect still...
340 final StringBuilder prefix = new StringBuilder(injectedLanguage.getPrefix());
341 final StringBuilder suffix = new StringBuilder(injectedLanguage.getSuffix());
342 adjustPrefixAndSuffix(getUnescapedText(host, textRange.substring(host.getText())), prefix, suffix);
344 addPlaceSafe(registrar, prefix.toString(), suffix.toString(), host, textRange, language);
346 else {
347 addPlaceSafe(registrar, injectedLanguage.getPrefix(), injectedLanguage.getSuffix(), host, textRange, language);
350 //try {
351 if (injectionStarted) {
352 registrar.doneInjecting();
355 //catch (AssertionError e) {
356 // logError(language, host, null, e);
358 //catch (Exception e) {
359 // logError(language, host, null, e);
363 public static CustomLanguageInjector getInstance(Project project) {
364 return project.getComponent(CustomLanguageInjector.class);