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
;
43 import java
.util
.concurrent
.CopyOnWriteArrayList
;
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.
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.
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
) {
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
) {
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;
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
))));
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() {
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()) {
135 for (Trinity
<PsiLanguageInjectionHost
, InjectedLanguage
, TextRange
> trinity
: result
) {
136 processor
.process(language
, Collections
.singletonList(trinity
));
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()) {
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:
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
) {
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
));
181 for (Trinity
<PsiLanguageInjectionHost
, InjectedLanguage
, TextRange
> trinity
: result
) {
182 processor
.process(language
, Collections
.singletonList(trinity
));
186 processor
.process(language
, result
);
188 if (injection
.isTerminal()) {
195 for (CustomLanguageInjectorExtension o
: Extensions
.getExtensions(CustomLanguageInjectorExtension
.EP_NAME
)) {
196 o
.getInjectedLanguage(myInjectionConfiguration
, place
, processor
);
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
;
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
);
257 private static void addPlaceSafe(MultiHostRegistrar registrar
, String prefix
, String suffix
, PsiLanguageInjectionHost host
, TextRange textRange
,
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
);
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
)) {
286 else if (endsWithSpace(prefix
) && startsWithSpace(text
)) {
290 if (suffix
.length() > 0) {
291 if (text
.length() == 0) {
292 // avoid to stick whitespace from prefix and suffix together
295 else if (!startsWithSpace(suffix
) && !endsWithSpace(text
)) {
296 suffix
.insert(0, " ");
298 else if (startsWithSpace(suffix
) && endsWithSpace(text
)) {
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*/) {
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
);
347 addPlaceSafe(registrar
, injectedLanguage
.getPrefix(), injectedLanguage
.getSuffix(), host
, textRange
, language
);
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);