| /* |
| * Copyright 2006 Sascha Weinreuter |
| * |
| * 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.config; |
| |
| import com.intellij.openapi.components.PersistentStateComponent; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.patterns.StringPattern; |
| import com.intellij.patterns.compiler.PatternCompiler; |
| import com.intellij.patterns.compiler.PatternCompilerFactory; |
| import com.intellij.psi.ElementManipulators; |
| import com.intellij.psi.LiteralTextEscaper; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiLanguageInjectionHost; |
| import com.intellij.util.ArrayUtil; |
| import com.intellij.util.Function; |
| import com.intellij.util.SmartList; |
| import com.intellij.util.containers.ContainerUtil; |
| import org.intellij.lang.annotations.RegExp; |
| import org.intellij.plugins.intelliLang.inject.InjectorUtils; |
| import org.jdom.CDATA; |
| import org.jdom.Element; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Injection base class: Contains properties for language-id, prefix and suffix. |
| */ |
| public class BaseInjection implements Injection, PersistentStateComponent<Element> { |
| |
| public static final Key<BaseInjection> INJECTION_KEY = Key.create("INJECTION_KEY"); |
| |
| @NotNull private final String mySupportId; |
| |
| private String myDisplayName = ""; |
| |
| private String myInjectedLanguageId = ""; |
| private String myPrefix = ""; |
| private String mySuffix = ""; |
| |
| @NonNls |
| private String myValuePattern = ""; |
| private Pattern myCompiledValuePattern; |
| private boolean mySingleFile; |
| |
| public BaseInjection(@NotNull final String id) { |
| mySupportId = id; |
| } |
| |
| @NotNull |
| private InjectionPlace[] myPlaces = InjectionPlace.EMPTY_ARRAY; |
| |
| @NotNull |
| public InjectionPlace[] getInjectionPlaces() { |
| return myPlaces; |
| } |
| |
| public void setInjectionPlaces(@NotNull InjectionPlace... places) { |
| myPlaces = places; |
| } |
| |
| @NotNull |
| public String getSupportId() { |
| return mySupportId; |
| } |
| |
| @NotNull |
| public String getInjectedLanguageId() { |
| return myInjectedLanguageId; |
| } |
| |
| @NotNull |
| public String getDisplayName() { |
| return myDisplayName; |
| } |
| |
| public void setDisplayName(@NotNull String displayName) { |
| myDisplayName = displayName; |
| } |
| |
| public void setInjectedLanguageId(@NotNull String injectedLanguageId) { |
| myInjectedLanguageId = injectedLanguageId; |
| } |
| |
| @NotNull |
| public String getPrefix() { |
| return myPrefix; |
| } |
| |
| public void setPrefix(@NotNull String prefix) { |
| myPrefix = prefix; |
| } |
| |
| @NotNull |
| public String getSuffix() { |
| return mySuffix; |
| } |
| |
| public void setSuffix(@NotNull String suffix) { |
| mySuffix = suffix; |
| } |
| |
| @NotNull |
| public List<TextRange> getInjectedArea(final PsiElement element) { |
| final TextRange textRange = ElementManipulators.getValueTextRange(element); |
| if (myCompiledValuePattern == null) { |
| return Collections.singletonList(textRange); |
| } |
| else { |
| final LiteralTextEscaper<? extends PsiLanguageInjectionHost> textEscaper = |
| ((PsiLanguageInjectionHost)element).createLiteralTextEscaper(); |
| final StringBuilder sb = new StringBuilder(); |
| textEscaper.decode(textRange, sb); |
| final List<TextRange> ranges = getMatchingRanges(myCompiledValuePattern.matcher(StringPattern.newBombedCharSequence(sb)), sb.length()); |
| return !ranges.isEmpty() ? ContainerUtil.map(ranges, new Function<TextRange, TextRange>() { |
| public TextRange fun(TextRange s) { |
| return new TextRange(textEscaper.getOffsetInHost(s.getStartOffset(), textRange), textEscaper.getOffsetInHost(s.getEndOffset(), textRange)); |
| } |
| }) : Collections.<TextRange>emptyList(); |
| } |
| } |
| |
| public boolean isEnabled() { |
| for (InjectionPlace place : myPlaces) { |
| if (place.getElementPattern() != null && place.isEnabled()) return true; |
| } |
| return false; |
| } |
| |
| public boolean acceptsPsiElement(final PsiElement element) { |
| ProgressManager.checkCanceled(); |
| for (InjectionPlace place : myPlaces) { |
| if (place.isEnabled() && place.getElementPattern() != null && place.getElementPattern().accepts(element)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean intersectsWith(final BaseInjection template) { |
| if (!Comparing.equal(getInjectedLanguageId(), template.getInjectedLanguageId())) return false; |
| for (InjectionPlace other : template.getInjectionPlaces()) { |
| if (ArrayUtil.contains(other, myPlaces)) return true; |
| } |
| return false; |
| } |
| |
| public boolean sameLanguageParameters(final BaseInjection that) { |
| if (!myInjectedLanguageId.equals(that.myInjectedLanguageId)) return false; |
| if (!myPrefix.equals(that.myPrefix)) return false; |
| if (!mySuffix.equals(that.mySuffix)) return false; |
| if (!myValuePattern.equals(that.myValuePattern)) return false; |
| if (mySingleFile != that.mySingleFile) return false; |
| return true; |
| } |
| |
| @SuppressWarnings({"unchecked"}) |
| public BaseInjection copy() { |
| return new BaseInjection(mySupportId).copyFrom(this); |
| } |
| |
| @SuppressWarnings({"RedundantIfStatement"}) |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || !(o instanceof BaseInjection)) return false; |
| |
| final BaseInjection that = (BaseInjection)o; |
| |
| if (!Comparing.equal(getDisplayName(), that.getDisplayName())) return false; |
| if (!sameLanguageParameters(that)) return false; |
| if (myPlaces.length != that.myPlaces.length) return false; |
| for (int i = 0, len = myPlaces.length; i < len; i++) { |
| if (myPlaces[i].isEnabled() != that.myPlaces[i].isEnabled()) { |
| return false; |
| } |
| } |
| // enabled flag is not counted this way: |
| if (!Arrays.equals(myPlaces, that.myPlaces)) return false; |
| return true; |
| } |
| |
| public int hashCode() { |
| int result; |
| result = myInjectedLanguageId.hashCode(); |
| result = 31 * result + myPrefix.hashCode(); |
| result = 31 * result + mySuffix.hashCode(); |
| result = 31 * result + myValuePattern.hashCode(); |
| return result; |
| } |
| |
| public BaseInjection copyFrom(@NotNull BaseInjection other) { |
| assert this != other; |
| |
| myInjectedLanguageId = other.getInjectedLanguageId(); |
| myPrefix = other.getPrefix(); |
| mySuffix = other.getSuffix(); |
| |
| myDisplayName = other.getDisplayName(); |
| |
| setValuePattern(other.getValuePattern()); |
| mySingleFile = other.mySingleFile; |
| |
| myPlaces = other.getInjectionPlaces().clone(); |
| return this; |
| } |
| |
| public void loadState(Element element) { |
| final PatternCompiler<PsiElement> helper = getCompiler(); |
| myDisplayName = StringUtil.notNullize(element.getChildText("display-name")); |
| myInjectedLanguageId = StringUtil.notNullize(element.getAttributeValue("language")); |
| myPrefix = StringUtil.notNullize(element.getChildText("prefix")); |
| mySuffix = StringUtil.notNullize(element.getChildText("suffix")); |
| setValuePattern(element.getChildText("value-pattern")); |
| mySingleFile = element.getChild("single-file") != null; |
| readExternalImpl(element); |
| final List<Element> placeElements = element.getChildren("place"); |
| myPlaces = InjectionPlace.ARRAY_FACTORY.create(placeElements.size()); |
| for (int i = 0, placeElementsSize = placeElements.size(); i < placeElementsSize; i++) { |
| Element placeElement = placeElements.get(i); |
| final boolean enabled = !Boolean.parseBoolean(placeElement.getAttributeValue("disabled")); |
| final String text = placeElement.getText(); |
| myPlaces[i] = new InjectionPlace(helper.createElementPattern(text, getDisplayName()), enabled); |
| } |
| if (myPlaces.length == 0) { |
| generatePlaces(); |
| } |
| } |
| |
| |
| public PatternCompiler<PsiElement> getCompiler() { |
| return PatternCompilerFactory.getFactory().getPatternCompiler(InjectorUtils.getPatternClasses(getSupportId())); |
| } |
| |
| public void generatePlaces() { |
| } |
| |
| protected void readExternalImpl(Element e) {} |
| |
| public final Element getState() { |
| final Element e = new Element("injection"); |
| e.setAttribute("language", myInjectedLanguageId); |
| e.setAttribute("injector-id", mySupportId); |
| e.addContent(new Element("display-name").setText(getDisplayName())); |
| if (StringUtil.isNotEmpty(myPrefix)) { |
| e.addContent(new Element("prefix").setText(myPrefix)); |
| } |
| if (StringUtil.isNotEmpty(mySuffix)) { |
| e.addContent(new Element("suffix").setText(mySuffix)); |
| } |
| if (StringUtil.isNotEmpty(myValuePattern)) { |
| e.addContent(new Element("value-pattern").setText(myValuePattern)); |
| } |
| if (mySingleFile) { |
| e.addContent(new Element("single-file")); |
| } |
| Arrays.sort(myPlaces, new Comparator<InjectionPlace>() { |
| public int compare(final InjectionPlace o1, final InjectionPlace o2) { |
| return Comparing.compare(o1.getText(), o2.getText()); |
| } |
| }); |
| for (InjectionPlace place : myPlaces) { |
| final Element child = new Element("place").setContent(new CDATA(place.getText())); |
| if (!place.isEnabled()) child.setAttribute("disabled", "true"); |
| e.addContent(child); |
| } |
| writeExternalImpl(e); |
| return e; |
| } |
| |
| protected void writeExternalImpl(Element e) {} |
| |
| @NotNull |
| public String getValuePattern() { |
| return myValuePattern; |
| } |
| |
| public void setValuePattern(@RegExp @Nullable String pattern) { |
| try { |
| if (pattern != null && pattern.length() > 0) { |
| myValuePattern = pattern; |
| myCompiledValuePattern = Pattern.compile(pattern, Pattern.DOTALL); |
| } |
| else { |
| myValuePattern = ""; |
| myCompiledValuePattern = null; |
| } |
| } |
| catch (Exception e1) { |
| myCompiledValuePattern = null; |
| Logger.getInstance(getClass().getName()).info("Invalid pattern", e1); |
| } |
| } |
| |
| public boolean isSingleFile() { |
| return mySingleFile; |
| } |
| |
| public void setSingleFile(final boolean singleFile) { |
| mySingleFile = singleFile; |
| } |
| |
| /** |
| * Determines if further injections should be examined if <code>isApplicable</code> has returned true. |
| * <p/> |
| * This is determined by the presence of a value-pattern: If none is present, the entry is considered |
| * to be a terminal one. |
| * |
| * @return true to stop, false to continue |
| */ |
| public boolean isTerminal() { |
| return myCompiledValuePattern == null; |
| } |
| |
| |
| private static List<TextRange> getMatchingRanges(Matcher matcher, final int length) { |
| final List<TextRange> list = new SmartList<TextRange>(); |
| int start = 0; |
| while (start < length && matcher.find(start)) { |
| final int groupCount = matcher.groupCount(); |
| if (groupCount == 0) { |
| start = matcher.end(); |
| } |
| else { |
| for (int i=1; i<=groupCount; i++) { |
| start = matcher.start(i); |
| if (start == -1) continue; |
| list.add(new TextRange(start, matcher.end(i))); |
| } |
| if (start >= matcher.end()) break; |
| start = matcher.end(); |
| } |
| } |
| return list; |
| } |
| |
| public void mergeOriginalPlacesFrom(final BaseInjection injection, final boolean enabled) { |
| for (InjectionPlace place : injection.getInjectionPlaces()) { |
| if (!ArrayUtil.contains(place, myPlaces)) { |
| myPlaces = ArrayUtil.append(myPlaces, enabled || !place.isEnabled() ? place : place.enabled(false), InjectionPlace.ARRAY_FACTORY); |
| } |
| } |
| } |
| |
| public void setPlaceEnabled(@Nullable final String text, final boolean enabled) { |
| for (int i = 0; i < myPlaces.length; i++) { |
| final InjectionPlace cur = myPlaces[i]; |
| if (text == null || Comparing.equal(text, cur.getText())) { |
| if (cur.isEnabled() != enabled) { |
| myPlaces[i] = cur.enabled(enabled); |
| } |
| } |
| } |
| } |
| |
| public boolean acceptForReference(PsiElement element) { |
| return acceptsPsiElement(element); |
| } |
| |
| @Override |
| public String toString() { |
| return getInjectedLanguageId()+ "->" +getDisplayName(); |
| } |
| } |