/** @author max */ public class FileContextUtil { public static final Key<SmartPsiElementPointer> INJECTED_IN_ELEMENT = Key.create("injectedIn"); public static final Key<PsiFile> CONTAINING_FILE_KEY = Key.create("CONTAINING_FILE_KEY"); private FileContextUtil() {} @Nullable public static PsiElement getFileContext(PsiFile file) { SmartPsiElementPointer pointer = file.getUserData(INJECTED_IN_ELEMENT); return pointer == null ? null : pointer.getElement(); } @Nullable public static PsiFile getContextFile(@NotNull PsiElement element) { if (!element.isValid()) return null; PsiFile file = element.getContainingFile(); if (file == null) return null; PsiElement context = file.getContext(); if (context == null) { return file; } else { return getContextFile(context); } } }
public abstract class RefJavaManager implements RefManagerExtension<RefJavaManager> { @NonNls public static final String CLASS = "class"; @NonNls public static final String METHOD = "method"; @NonNls public static final String IMPLICIT_CONSTRUCTOR = "implicit.constructor"; @NonNls public static final String FIELD = "field"; @NonNls public static final String PARAMETER = "parameter"; @NonNls public static final String JAVA_MODULE = "java.module"; // used in OfflineProjectDescriptor @NonNls public static final String PACKAGE = "package"; public static final Key<RefJavaManager> MANAGER = Key.create("RefJavaManager"); public abstract RefImplicitConstructor getImplicitConstructor(String classFQName); /** * Creates (if necessary) and returns the reference graph node for the package with the specified * name. * * @param packageName the name of the package for which the reference graph node is requested. * @return the node for the package. */ public abstract RefPackage getPackage(String packageName); /** * Creates (if necessary) and returns the reference graph node for the specified PSI parameter. * * @param param the parameter for which the reference graph node is requested. * @param index the index of the parameter in its parameter list. * @return the node for the element, or null if the element is not valid or does not have a * corresponding reference graph node type (is not a field, method, class or file). */ public abstract RefParameter getParameterReference(PsiParameter param, int index); public abstract RefPackage getDefaultPackage(); public abstract PsiMethod getAppMainPattern(); public abstract PsiMethod getAppPremainPattern(); public abstract PsiMethod getAppAgentmainPattern(); public abstract PsiClass getApplet(); public abstract PsiClass getServlet(); public abstract EntryPointsManager getEntryPointsManager(); @NotNull @Override public Language getLanguage() { return JavaLanguage.INSTANCE; } @NotNull @Override public Key<RefJavaManager> getID() { return MANAGER; } }
/** * @author Dmitry Avdeev * @author peter */ public class PerIndexDocumentVersionMap { private static final int INVALID_STAMP = -1; // 0 isn't acceptable as Document has 0 stamp when loaded from unchanged file private volatile int mapVersion; private static class IdVersionInfo { private final ID<?, ?> id; private int mapVersion; private long docVersion; private IdVersionInfo(@NotNull ID<?, ?> id, long docVersion, int mapVersion) { this.docVersion = docVersion; this.mapVersion = mapVersion; this.id = id; } } private static final Key<List<IdVersionInfo>> KEY = Key.create("UnsavedDocIdVersionInfo"); public long getAndSet(@NotNull Document document, @NotNull ID<?, ?> indexId, long value) { List<IdVersionInfo> list = document.getUserData(KEY); if (list == null) { list = ((UserDataHolderEx) document).putUserDataIfAbsent(KEY, new ArrayList<IdVersionInfo>()); } synchronized (list) { for (IdVersionInfo info : list) { if (info.id == indexId) { long old = info.docVersion; if (info.mapVersion != mapVersion) { old = INVALID_STAMP; info.mapVersion = mapVersion; } info.docVersion = value; return old; } } list.add(new IdVersionInfo(indexId, value, mapVersion)); return INVALID_STAMP; } } public void clear() { mapVersion++; } }
public interface IGosuMethod extends IGosuMembersDeclaration, PsiMethod, IGosuNamedElement, IGosuMember, IGosuParametersOwner, IGosuTypeParameterListOwner { Key<Boolean> BUILDER_METHOD = Key.create("BUILDER_METHOD"); IGosuMethod[] EMPTY_ARRAY = new IGosuMethod[0]; ArrayFactory<IGosuMethod> ARRAY_FACTORY = new ArrayFactory<IGosuMethod>() { @NotNull public IGosuMethod[] create(int count) { return count == 0 ? EMPTY_ARRAY : new IGosuMethod[count]; } }; IGosuTypeElement getReturnTypeElementGosu(); /** @return the static return type, which will appear in the compiled Gosu class */ @Nullable PsiType getReturnType(); @Nullable IGosuTypeElement setReturnType(PsiType newReturnType); @NotNull String getName(); @NotNull IGosuParameterList getParameterList(); @NotNull IGosuModifierList getModifierList(); String[] getNamedParametersArray(); boolean isForProperty(); boolean isForPropertySetter(); boolean isForPropertyGetter(); }
public abstract class HTMLJavaHTMLComposer implements HTMLComposerExtension<HTMLJavaHTMLComposer> { public static final Key<HTMLJavaHTMLComposer> COMPOSER = Key.create("HTMLJavaComposer"); public abstract void appendClassOrInterface( StringBuffer buf, RefClass refClass, boolean capitalizeFirstLetter); public static String getClassOrInterface(RefClass refClass, boolean capitalizeFirstLetter) { if (refClass.isInterface()) { return capitalizeFirstLetter ? InspectionsBundle.message("inspection.export.results.capitalized.interface") : InspectionsBundle.message("inspection.export.results.interface"); } else if (refClass.isAbstract()) { return capitalizeFirstLetter ? InspectionsBundle.message("inspection.export.results.capitalized.abstract.class") : InspectionsBundle.message("inspection.export.results.abstract.class"); } else { return capitalizeFirstLetter ? InspectionsBundle.message("inspection.export.results.capitalized.class") : InspectionsBundle.message("inspection.export.results.class"); } } public abstract void appendClassExtendsImplements(StringBuffer buf, RefClass refClass); public abstract void appendDerivedClasses(StringBuffer buf, RefClass refClass); public abstract void appendLibraryMethods(StringBuffer buf, RefClass refClass); public abstract void appendSuperMethods(StringBuffer buf, RefMethod refMethod); public abstract void appendDerivedMethods(StringBuffer buf, RefMethod refMethod); public abstract void appendTypeReferences(StringBuffer buf, RefClass refClass); public Key<HTMLJavaHTMLComposer> getID() { return COMPOSER; } public Language getLanguage() { return StdLanguages.JAVA; } }
private static final class CompileStatistics { private static final Key<CompileStatistics> KEY = Key.create("_Compile_Statistics_"); private int myClassesCount; private int myFilesCount; public int getClassesCount() { return myClassesCount; } public int incClassesCount() { return ++myClassesCount; } public int getFilesCount() { return myFilesCount; } public int incFilesCount() { return ++myFilesCount; } }
/** @author Eugene.Kudelevsky */ public class AndroidDependenciesCache { private static final Key<SoftReference<AndroidDependenciesCache>> KEY = Key.create("ANDROID_DEPENDENCIES_CACHE"); private final Module myModule; private final Ref<List<WeakReference<AndroidModuleExtension>>> myAllDependencies = Ref.create(); private final Ref<List<WeakReference<AndroidModuleExtension>>> myAllLibraryDependencies = Ref.create(); private AndroidDependenciesCache(@NotNull Module module) { myModule = module; module .getProject() .getMessageBus() .connect(module) .subscribe( ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() { @Override public void rootsChanged(ModuleRootEvent event) { dropCache(); } }); } private synchronized void dropCache() { myAllDependencies.set(null); myAllLibraryDependencies.set(null); } @NotNull public static AndroidDependenciesCache getInstance(@NotNull Module module) { AndroidDependenciesCache cache = SoftReference.dereference(module.getUserData(KEY)); if (cache == null) { cache = new AndroidDependenciesCache(module); module.putUserData(KEY, new SoftReference<AndroidDependenciesCache>(cache)); } return cache; } @NotNull public synchronized List<AndroidModuleExtension> getAllAndroidDependencies( boolean androidLibrariesOnly) { return getAllAndroidDependencies( myModule, androidLibrariesOnly, getListRef(androidLibrariesOnly)); } @NotNull private Ref<List<WeakReference<AndroidModuleExtension>>> getListRef( boolean androidLibrariesOnly) { return androidLibrariesOnly ? myAllLibraryDependencies : myAllDependencies; } @NotNull private static List<AndroidModuleExtension> getAllAndroidDependencies( @NotNull Module module, boolean androidLibrariesOnly, Ref<List<WeakReference<AndroidModuleExtension>>> listRef) { List<WeakReference<AndroidModuleExtension>> refs = listRef.get(); if (refs == null) { final List<AndroidModuleExtension> facets = new ArrayList<AndroidModuleExtension>(); collectAllAndroidDependencies( module, androidLibrariesOnly, facets, new HashSet<AndroidModuleExtension>()); refs = ContainerUtil.map( ContainerUtil.reverse(facets), new Function<AndroidModuleExtension, WeakReference<AndroidModuleExtension>>() { @Override public WeakReference<AndroidModuleExtension> fun(AndroidModuleExtension facet) { return new WeakReference<AndroidModuleExtension>(facet); } }); listRef.set(refs); } return dereference(refs); } @NotNull private static List<AndroidModuleExtension> dereference( @NotNull List<WeakReference<AndroidModuleExtension>> refs) { return ContainerUtil.mapNotNull( refs, new Function<WeakReference<AndroidModuleExtension>, AndroidModuleExtension>() { @Override public AndroidModuleExtension fun(WeakReference<AndroidModuleExtension> ref) { return ref.get(); } }); } private static void collectAllAndroidDependencies( Module module, boolean androidLibrariesOnly, List<AndroidModuleExtension> result, Set<AndroidModuleExtension> visited) { final OrderEntry[] entries = ModuleRootManager.getInstance(module).getOrderEntries(); // loop in the inverse order to resolve dependencies on the libraries, so that if a library // is required by two higher level libraries it can be inserted in the correct place for (int i = entries.length - 1; i >= 0; i--) { final OrderEntry orderEntry = entries[i]; if (orderEntry instanceof ModuleOrderEntry) { final ModuleOrderEntry moduleOrderEntry = (ModuleOrderEntry) orderEntry; if (moduleOrderEntry.getScope() == DependencyScope.COMPILE) { final Module depModule = moduleOrderEntry.getModule(); if (depModule != null) { final AndroidModuleExtension depFacet = ModuleUtilCore.getExtension(depModule, AndroidModuleExtension.class); if (depFacet != null && (!androidLibrariesOnly || depFacet.isLibraryProject()) && visited.add(depFacet)) { final List<WeakReference<AndroidModuleExtension>> cachedDepDeps = getInstance(depModule).getListRef(androidLibrariesOnly).get(); if (cachedDepDeps != null) { final List<AndroidModuleExtension> depDeps = dereference(cachedDepDeps); for (AndroidModuleExtension depDepFacet : depDeps) { if (visited.add(depDepFacet)) { result.add(depDepFacet); } } } else { collectAllAndroidDependencies(depModule, androidLibrariesOnly, result, visited); } result.add(depFacet); } } } } } } }
/** @author gregsh */ public class GeneratedParserUtilBase { private static final Logger LOG = Logger.getInstance("org.intellij.grammar.parser.GeneratedParserUtilBase"); private static final int MAX_RECURSION_LEVEL = 1000; private static final int MAX_VARIANTS_SIZE = 10000; private static final int MAX_VARIANTS_TO_DISPLAY = 50; private static final int INITIAL_VARIANTS_SIZE = 1000; private static final int VARIANTS_POOL_SIZE = 10000; private static final int FRAMES_POOL_SIZE = 500; public static final IElementType DUMMY_BLOCK = new DummyBlockElementType(); public interface Parser { boolean parse(PsiBuilder builder, int level); } public static final Parser TOKEN_ADVANCER = new Parser() { @Override public boolean parse(PsiBuilder builder, int level) { if (builder.eof()) return false; builder.advanceLexer(); return true; } }; public static final Parser TRUE_CONDITION = new Parser() { @Override public boolean parse(PsiBuilder builder, int level) { return true; } }; public static boolean eof(PsiBuilder builder, int level) { return builder.eof(); } public static int current_position_(PsiBuilder builder) { return builder.rawTokenIndex(); } public static boolean recursion_guard_(PsiBuilder builder, int level, String funcName) { if (level > MAX_RECURSION_LEVEL) { builder.error( "Maximum recursion level (" + MAX_RECURSION_LEVEL + ") reached in '" + funcName + "'"); return false; } return true; } public static boolean empty_element_parsed_guard_(PsiBuilder builder, String funcName, int pos) { if (pos == current_position_(builder)) { builder.error( "Empty element parsed in '" + funcName + "' at offset " + builder.getCurrentOffset()); return false; } return true; } public static boolean invalid_left_marker_guard_( PsiBuilder builder, PsiBuilder.Marker marker, String funcName) { // builder.error("Invalid left marker encountered in " + funcName_ +" at offset " + // builder.getCurrentOffset()); boolean goodMarker = marker != null; // && ((LighterASTNode)marker).getTokenType() != TokenType.ERROR_ELEMENT; if (!goodMarker) return false; ErrorState state = ErrorState.get(builder); return !state.frameStack.isEmpty(); } public static TokenSet create_token_set_(IElementType... tokenTypes) { return TokenSet.create(tokenTypes); } public static boolean leftMarkerIs(PsiBuilder builder, IElementType type) { LighterASTNode marker = builder.getLatestDoneMarker(); return marker != null && marker.getTokenType() == type; } private static boolean consumeTokens( PsiBuilder builder, boolean smart, int pin, IElementType... tokens) { ErrorState state = ErrorState.get(builder); if (state.completionState != null && state.predicateSign) { addCompletionVariant(builder, state.completionState, tokens); } // suppress single token completion CompletionState completionState = state.completionState; state.completionState = null; boolean result = true; boolean pinned = false; for (int i = 0, tokensLength = tokens.length; i < tokensLength; i++) { if (pin > 0 && i == pin) pinned = result; if (result || pinned) { boolean fast = smart && i == 0; if (!(fast ? consumeTokenFast(builder, tokens[i]) : consumeToken(builder, tokens[i]))) { result = false; if (pin < 0 || pinned) report_error_(builder, state, false); } } } state.completionState = completionState; return pinned || result; } public static boolean consumeTokens(PsiBuilder builder, int pin, IElementType... token) { return consumeTokens(builder, false, pin, token); } public static boolean consumeTokensSmart(PsiBuilder builder, int pin, IElementType... token) { return consumeTokens(builder, true, pin, token); } public static boolean parseTokens(PsiBuilder builder, int pin, IElementType... tokens) { return parseTokens(builder, false, pin, tokens); } public static boolean parseTokensSmart(PsiBuilder builder, int pin, IElementType... tokens) { return parseTokens(builder, true, pin, tokens); } public static boolean parseTokens( PsiBuilder builder, boolean smart, int pin, IElementType... tokens) { PsiBuilder.Marker marker = builder.mark(); boolean result = consumeTokens(builder, smart, pin, tokens); if (!result) { marker.rollbackTo(); } else { marker.drop(); } return result; } public static boolean consumeTokenSmart(PsiBuilder builder, IElementType token) { addCompletionVariantSmart(builder, token); return consumeTokenFast(builder, token); } public static boolean consumeTokenSmart(PsiBuilder builder, String token) { addCompletionVariantSmart(builder, token); return consumeTokenFast(builder, token); } public static boolean consumeToken(PsiBuilder builder, IElementType token) { addVariantSmart(builder, token, true); if (nextTokenIsFast(builder, token)) { builder.advanceLexer(); return true; } return false; } public static boolean consumeTokenFast(PsiBuilder builder, IElementType token) { if (nextTokenIsFast(builder, token)) { builder.advanceLexer(); return true; } return false; } public static boolean consumeToken(PsiBuilder builder, String text) { return consumeToken(builder, text, ErrorState.get(builder).caseSensitive); } public static boolean consumeToken(PsiBuilder builder, String text, boolean caseSensitive) { addVariantSmart(builder, text, true); int count = nextTokenIsFast(builder, text, caseSensitive); if (count > 0) { while (count-- > 0) builder.advanceLexer(); return true; } return false; } public static boolean consumeTokenFast(PsiBuilder builder, String text) { int count = nextTokenIsFast(builder, text, ErrorState.get(builder).caseSensitive); if (count > 0) { while (count-- > 0) builder.advanceLexer(); return true; } return false; } public static boolean nextTokenIsFast(PsiBuilder builder, IElementType token) { return builder.getTokenType() == token; } public static boolean nextTokenIsFast(PsiBuilder builder, IElementType... tokens) { IElementType tokenType = builder.getTokenType(); for (IElementType token : tokens) { if (token == tokenType) return true; } return false; } public static boolean nextTokenIs(PsiBuilder builder, String frameName, IElementType... tokens) { ErrorState state = ErrorState.get(builder); if (state.completionState != null) return true; boolean track = !state.suppressErrors && state.predicateCount < 2 && state.predicateSign; if (!track) return nextTokenIsFast(builder, tokens); IElementType tokenType = builder.getTokenType(); if (StringUtil.isNotEmpty(frameName)) { addVariantInner(state, builder.rawTokenIndex(), frameName); } else { for (IElementType token : tokens) { addVariant(builder, state, token); } } if (tokenType == null) return false; for (IElementType token : tokens) { if (tokenType == token) return true; } return false; } public static boolean nextTokenIs(PsiBuilder builder, IElementType token) { if (!addVariantSmart(builder, token, false)) return true; return nextTokenIsFast(builder, token); } public static boolean nextTokenIs(PsiBuilder builder, String tokenText) { if (!addVariantSmart(builder, tokenText, false)) return true; return nextTokenIsFast(builder, tokenText, ErrorState.get(builder).caseSensitive) > 0; } public static boolean nextTokenIsFast(PsiBuilder builder, String tokenText) { return nextTokenIsFast(builder, tokenText, ErrorState.get(builder).caseSensitive) > 0; } public static int nextTokenIsFast(PsiBuilder builder, String tokenText, boolean caseSensitive) { CharSequence sequence = builder.getOriginalText(); int offset = builder.getCurrentOffset(); int endOffset = offset + tokenText.length(); CharSequence subSequence = sequence.subSequence(offset, Math.min(endOffset, sequence.length())); if (!Comparing.equal(subSequence, tokenText, caseSensitive)) return 0; int count = 0; while (true) { int nextOffset = builder.rawTokenTypeStart(++count); if (nextOffset > endOffset) { return -count; } else if (nextOffset == endOffset) { break; } } return count; } private static void addCompletionVariantSmart(PsiBuilder builder, Object token) { ErrorState state = ErrorState.get(builder); CompletionState completionState = state.completionState; if (completionState != null && state.predicateSign) { addCompletionVariant(builder, completionState, token); } } private static boolean addVariantSmart(PsiBuilder builder, Object token, boolean force) { ErrorState state = ErrorState.get(builder); // skip FIRST check in completion mode if (state.completionState != null && !force) return false; builder.eof(); if (!state.suppressErrors && state.predicateCount < 2) { addVariant(builder, state, token); } return true; } public static void addVariant(PsiBuilder builder, String text) { addVariant(builder, ErrorState.get(builder), text); } private static void addVariant(PsiBuilder builder, ErrorState state, Object o) { builder.eof(); // skip whitespaces addVariantInner(state, builder.rawTokenIndex(), o); CompletionState completionState = state.completionState; if (completionState != null && state.predicateSign) { addCompletionVariant(builder, completionState, o); } } private static void addVariantInner(ErrorState state, int pos, Object o) { Variant variant = state.VARIANTS.alloc().init(pos, o); if (state.predicateSign) { state.variants.add(variant); if (state.lastExpectedVariantPos < variant.position) { state.lastExpectedVariantPos = variant.position; } } else { state.unexpected.add(variant); } } private static void addCompletionVariant( @NotNull PsiBuilder builder, @NotNull CompletionState completionState, Object o) { int offset = builder.getCurrentOffset(); if (!builder.eof() && offset == builder.rawTokenTypeStart(1)) return; // suppress for zero-length tokens boolean add = false; int diff = completionState.offset - offset; String text = completionState.convertItem(o); int length = text == null ? 0 : text.length(); if (length == 0) return; if (diff == 0) { add = true; } else if (diff > 0 && diff <= length) { CharSequence fragment = builder.getOriginalText().subSequence(offset, completionState.offset); add = completionState.prefixMatches(fragment.toString(), text); } else if (diff < 0) { for (int i = -1; ; i--) { IElementType type = builder.rawLookup(i); int tokenStart = builder.rawTokenTypeStart(i); if (isWhitespaceOrComment(builder, type)) { diff = completionState.offset - tokenStart; } else if (type != null && tokenStart < completionState.offset) { CharSequence fragment = builder.getOriginalText().subSequence(tokenStart, completionState.offset); if (completionState.prefixMatches(fragment.toString(), text)) { diff = completionState.offset - tokenStart; } break; } else break; } add = diff >= 0 && diff < length; } add = add && length > 1 && !(text.charAt(0) == '<' && text.charAt(length - 1) == '>') && !(text.charAt(0) == '\'' && text.charAt(length - 1) == '\'' && length < 5); if (add) { completionState.addItem(builder, text); } } public static boolean isWhitespaceOrComment( @NotNull PsiBuilder builder, @Nullable IElementType type) { return ((PsiBuilderImpl) ((Builder) builder).getDelegate()).whitespaceOrComment(type); } // here's the new section API for compact parsers & less IntelliJ platform API exposure public static final int _NONE_ = 0x0; public static final int _COLLAPSE_ = 0x1; public static final int _LEFT_ = 0x2; public static final int _LEFT_INNER_ = 0x4; public static final int _AND_ = 0x8; public static final int _NOT_ = 0x10; // simple enter/exit methods pair that doesn't require frame object public static PsiBuilder.Marker enter_section_(PsiBuilder builder) { return builder.mark(); } public static void exit_section_( PsiBuilder builder, PsiBuilder.Marker marker, @Nullable IElementType elementType, boolean result) { close_marker_impl_(ErrorState.get(builder).frameStack.peekLast(), marker, elementType, result); } // complex enter/exit methods pair with frame object public static PsiBuilder.Marker enter_section_( PsiBuilder builder, int level, int modifiers, @Nullable String frameName) { PsiBuilder.Marker marker = builder.mark(); enter_section_impl_(builder, level, modifiers, frameName); return marker; } private static void enter_section_impl_( PsiBuilder builder, int level, int modifiers, @Nullable String frameName) { ErrorState state = ErrorState.get(builder); Frame frame = state.FRAMES.alloc().init(builder, state, level, modifiers, frameName); Frame prevFrame = state.frameStack.peekLast(); if (prevFrame != null && prevFrame.errorReportedAt > frame.position) { // report error for previous unsuccessful frame reportError(builder, state, frame, null, true, false); } if (((frame.modifiers & _LEFT_) | (frame.modifiers & _LEFT_INNER_)) != 0) { PsiBuilder.Marker left = (PsiBuilder.Marker) builder.getLatestDoneMarker(); if (invalid_left_marker_guard_(builder, left, frameName)) { frame.leftMarker = left; } } state.frameStack.add(frame); if ((modifiers & _AND_) != 0) { if (state.predicateCount == 0 && !state.predicateSign) { throw new AssertionError("Incorrect false predicate sign"); } state.predicateCount++; } else if ((modifiers & _NOT_) != 0) { if (state.predicateCount == 0) { state.predicateSign = false; } else { state.predicateSign = !state.predicateSign; } state.predicateCount++; } } public static void exit_section_( PsiBuilder builder, int level, PsiBuilder.Marker marker, @Nullable IElementType elementType, boolean result, boolean pinned, @Nullable Parser eatMore) { ErrorState state = ErrorState.get(builder); Frame frame = state.frameStack.pollLast(); if (frame == null || level != frame.level) { LOG.error("Unbalanced error section: got " + frame + ", expected level " + level); if (frame != null) state.FRAMES.recycle(frame); close_marker_impl_(frame, marker, elementType, result); return; } if (((frame.modifiers & _AND_) | (frame.modifiers & _NOT_)) != 0) { close_marker_impl_(frame, marker, null, false); state.predicateCount--; if ((frame.modifiers & _NOT_) != 0) state.predicateSign = !state.predicateSign; state.FRAMES.recycle(frame); return; } close_frame_impl_(state, frame, builder, marker, elementType, result, pinned); exit_section_impl_(state, frame, builder, elementType, result, pinned, eatMore); state.FRAMES.recycle(frame); } private static void exit_section_impl_( ErrorState state, Frame frame, PsiBuilder builder, @Nullable IElementType elementType, boolean result, boolean pinned, @Nullable Parser eatMore) { int initialPos = builder.rawTokenIndex(); boolean willFail = !result && !pinned; if (willFail && initialPos == frame.position && state.lastExpectedVariantPos == frame.position && frame.name != null && state.variants.size() - frame.variantCount > 1) { state.clearVariants(true, frame.variantCount); addVariantInner(state, initialPos, frame.name); } int lastErrorPos = getLastVariantPos(state, initialPos); if (!state.suppressErrors && eatMore != null) { state.suppressErrors = true; final boolean eatMoreFlagOnce = !builder.eof() && eatMore.parse(builder, frame.level + 1); boolean eatMoreFlag = eatMoreFlagOnce || !result && frame.position == initialPos && lastErrorPos > frame.position; PsiBuilderImpl.ProductionMarker latestDoneMarker = (pinned || result) && (state.altMode || elementType != null) && eatMoreFlagOnce ? (PsiBuilderImpl.ProductionMarker) builder.getLatestDoneMarker() : null; PsiBuilder.Marker extensionMarker = null; IElementType extensionTokenType = null; // whitespace prefix makes the very first frame offset bigger than marker start offset which // is always 0 if (latestDoneMarker != null && frame.position >= latestDoneMarker.getStartIndex() && frame.position <= latestDoneMarker.getEndIndex()) { extensionMarker = ((PsiBuilder.Marker) latestDoneMarker).precede(); extensionTokenType = latestDoneMarker.getTokenType(); ((PsiBuilder.Marker) latestDoneMarker).drop(); } // advance to the last error pos // skip tokens until lastErrorPos. parseAsTree might look better here... int parenCount = 0; while ((eatMoreFlag || parenCount > 0) && builder.rawTokenIndex() < lastErrorPos) { if (state.braces != null) { if (builder.getTokenType() == state.braces[0].getLeftBraceType()) parenCount++; else if (builder.getTokenType() == state.braces[0].getRightBraceType()) parenCount--; } builder.advanceLexer(); eatMoreFlag = eatMore.parse(builder, frame.level + 1); } boolean errorReported = frame.errorReportedAt == initialPos || !result && frame.errorReportedAt >= frame.position; if (errorReported) { if (eatMoreFlag) { builder.advanceLexer(); parseAsTree(state, builder, frame.level + 1, DUMMY_BLOCK, true, TOKEN_ADVANCER, eatMore); } } else if (eatMoreFlag) { errorReported = reportError(builder, state, frame, null, true, true); parseAsTree(state, builder, frame.level + 1, DUMMY_BLOCK, true, TOKEN_ADVANCER, eatMore); } else if (eatMoreFlagOnce || (!result && frame.position != builder.rawTokenIndex()) || frame.errorReportedAt > initialPos) { errorReported = reportError(builder, state, frame, null, true, false); } if (extensionMarker != null) { extensionMarker.done(extensionTokenType); } state.suppressErrors = false; if (errorReported || result) { state.clearVariants(true, 0); state.clearVariants(false, 0); state.lastExpectedVariantPos = -1; } } else if (!result && pinned && frame.errorReportedAt < 0) { // do not report if there are errors beyond current position if (lastErrorPos == initialPos) { // do not force, inner recoverRoot might have skipped some tokens reportError(builder, state, frame, elementType, false, false); } else if (lastErrorPos > initialPos) { // set error pos here as if it is reported for future reference frame.errorReportedAt = lastErrorPos; } } // propagate errorReportedAt up the stack to avoid duplicate reporting Frame prevFrame = willFail && eatMore == null ? null : state.frameStack.peekLast(); if (prevFrame != null && prevFrame.errorReportedAt < frame.errorReportedAt) { prevFrame.errorReportedAt = frame.errorReportedAt; } } private static void close_frame_impl_( ErrorState state, Frame frame, PsiBuilder builder, PsiBuilder.Marker marker, IElementType elementType, boolean result, boolean pinned) { if (elementType != null && marker != null) { if ((frame.modifiers & _COLLAPSE_) != 0) { PsiBuilderImpl.ProductionMarker last = result || pinned ? (PsiBuilderImpl.ProductionMarker) builder.getLatestDoneMarker() : null; if (last != null && last.getStartIndex() == frame.position && state.typeExtends(last.getTokenType(), elementType)) { IElementType resultType = last.getTokenType(); ((PsiBuilder.Marker) last).drop(); marker.done(resultType); return; } } if (result || pinned) { if ((frame.modifiers & _LEFT_INNER_) != 0 && frame.leftMarker != null) { marker.done(elementType); frame.leftMarker.precede().done(((LighterASTNode) frame.leftMarker).getTokenType()); frame.leftMarker.drop(); } else if ((frame.modifiers & _LEFT_) != 0 && frame.leftMarker != null) { marker.drop(); frame.leftMarker.precede().done(elementType); } else { if (frame.level == 0) builder.eof(); // skip whitespaces marker.done(elementType); } } else { close_marker_impl_(frame, marker, null, false); } } else if (result || pinned) { if (marker != null) marker.drop(); if ((frame.modifiers & _LEFT_INNER_) != 0 && frame.leftMarker != null) { frame.leftMarker.precede().done(((LighterASTNode) frame.leftMarker).getTokenType()); frame.leftMarker.drop(); } } else { close_marker_impl_(frame, marker, null, false); } } private static void close_marker_impl_( Frame frame, PsiBuilder.Marker marker, IElementType elementType, boolean result) { if (marker == null) return; if (result) { if (elementType != null) { marker.done(elementType); } else { marker.drop(); } } else { if (frame != null) { int position = ((PsiBuilderImpl.ProductionMarker) marker).getStartIndex(); if (frame.errorReportedAt > position) { frame.errorReportedAt = frame.errorReportedAtPrev; } } marker.rollbackTo(); } } public static boolean report_error_(PsiBuilder builder, boolean result) { if (!result) report_error_(builder, ErrorState.get(builder), false); return result; } public static void report_error_(PsiBuilder builder, ErrorState state, boolean advance) { Frame frame = state.frameStack.isEmpty() ? null : state.frameStack.getLast(); if (frame == null) { LOG.error("unbalanced enter/exit section call: got null"); return; } int position = builder.rawTokenIndex(); if (frame.errorReportedAt < position && getLastVariantPos(state, position + 1) <= position) { reportError(builder, state, frame, null, true, advance); } } private static int getLastVariantPos(ErrorState state, int defValue) { return state.lastExpectedVariantPos < 0 ? defValue : state.lastExpectedVariantPos; } private static boolean reportError( PsiBuilder builder, ErrorState state, Frame frame, IElementType elementType, boolean force, boolean advance) { String expectedText = state.getExpectedText(builder); boolean notEmpty = StringUtil.isNotEmpty(expectedText); if (force || notEmpty || advance) { String gotText = builder.eof() ? "unexpected end of file" : notEmpty ? "got '" + builder.getTokenText() + "'" : "'" + builder.getTokenText() + "' unexpected"; String message = expectedText + gotText; if (advance) { PsiBuilder.Marker mark = builder.mark(); builder.advanceLexer(); mark.error(message); } else if (!force) { PsiBuilder.Marker extensionMarker = null; IElementType extensionTokenType = null; PsiBuilderImpl.ProductionMarker latestDoneMarker = elementType == null ? null : (PsiBuilderImpl.ProductionMarker) builder.getLatestDoneMarker(); if (latestDoneMarker != null && frame.position >= latestDoneMarker.getStartIndex() && frame.position <= latestDoneMarker.getEndIndex()) { extensionMarker = ((PsiBuilder.Marker) latestDoneMarker).precede(); extensionTokenType = latestDoneMarker.getTokenType(); ((PsiBuilder.Marker) latestDoneMarker).drop(); } builder.error(message); if (extensionMarker != null) extensionMarker.done(extensionTokenType); } else { builder.error(message); } builder.eof(); // skip whitespaces frame.errorReportedAt = builder.rawTokenIndex(); return true; } return false; } public static final Key<CompletionState> COMPLETION_STATE_KEY = Key.create("COMPLETION_STATE_KEY"); public static class CompletionState implements Function<Object, String> { public final int offset; public final Collection<String> items = ContainerUtil.newTroveSet(); public CompletionState(int offset_) { offset = offset_; } @Nullable public String convertItem(Object o) { return o instanceof Object[] ? StringUtil.join((Object[]) o, this, " ") : o.toString(); } @Override public String fun(Object o) { return o.toString(); } public void addItem(@NotNull PsiBuilder builder, @NotNull String text) { items.add(text); } public boolean prefixMatches(@NotNull String prefix, @NotNull String variant) { boolean matches = new CamelHumpMatcher(prefix, false).prefixMatches(variant.replace(' ', '_')); if (matches && StringUtil.isWhiteSpace(prefix.charAt(prefix.length() - 1))) { return StringUtil.startsWithIgnoreCase(variant, prefix); } return matches; } } public static class Builder extends PsiBuilderAdapter { public final ErrorState state; public final PsiParser parser; public Builder(PsiBuilder builder, ErrorState state_, PsiParser parser_) { super(builder); state = state_; parser = parser_; } public Lexer getLexer() { return ((PsiBuilderImpl) myDelegate).getLexer(); } } public static PsiBuilder adapt_builder_(IElementType root, PsiBuilder builder, PsiParser parser) { return adapt_builder_(root, builder, parser, null); } public static PsiBuilder adapt_builder_( IElementType root, PsiBuilder builder, PsiParser parser, TokenSet[] extendsSets) { ErrorState state = new ErrorState(); ErrorState.initState(state, builder, root, extendsSets); return new Builder(builder, state, parser); } public static class ErrorState { TokenSet[] extendsSets; public PairProcessor<IElementType, IElementType> altExtendsChecker; int predicateCount; boolean predicateSign = true; boolean suppressErrors; public final LinkedList<Frame> frameStack = new LinkedList<Frame>(); public CompletionState completionState; private boolean caseSensitive; public BracePair[] braces; public boolean altMode; int lastExpectedVariantPos = -1; MyList<Variant> variants = new MyList<Variant>(INITIAL_VARIANTS_SIZE); MyList<Variant> unexpected = new MyList<Variant>(INITIAL_VARIANTS_SIZE / 10); final LimitedPool<Variant> VARIANTS = new LimitedPool<Variant>( VARIANTS_POOL_SIZE, new LimitedPool.ObjectFactory<Variant>() { @NotNull @Override public Variant create() { return new Variant(); } @Override public void cleanup(@NotNull final Variant o) {} }); final LimitedPool<Frame> FRAMES = new LimitedPool<Frame>( FRAMES_POOL_SIZE, new LimitedPool.ObjectFactory<Frame>() { @NotNull @Override public Frame create() { return new Frame(); } @Override public void cleanup(@NotNull final Frame o) {} }); public static ErrorState get(PsiBuilder builder) { return ((Builder) builder).state; } public static void initState( ErrorState state, PsiBuilder builder, IElementType root, TokenSet[] extendsSets) { state.extendsSets = extendsSets; PsiFile file = builder.getUserDataUnprotected(FileContextUtil.CONTAINING_FILE_KEY); state.completionState = file == null ? null : file.getUserData(COMPLETION_STATE_KEY); Language language = file == null ? root.getLanguage() : file.getLanguage(); state.caseSensitive = language.isCaseSensitive(); PairedBraceMatcher matcher = LanguageBraceMatching.INSTANCE.forLanguage(language); state.braces = matcher == null ? null : matcher.getPairs(); if (state.braces != null && state.braces.length == 0) state.braces = null; } public String getExpectedText(PsiBuilder builder) { int position = builder.rawTokenIndex(); StringBuilder sb = new StringBuilder(); if (addExpected(sb, position, true)) { sb.append(" expected, "); } return sb.toString(); } private boolean addExpected(StringBuilder sb, int position, boolean expected) { MyList<Variant> list = expected ? variants : unexpected; String[] strings = new String[list.size()]; long[] hashes = new long[strings.length]; Arrays.fill(strings, ""); int count = 0; loop: for (Variant variant : list) { if (position == variant.position) { String text = variant.object.toString(); long hash = StringHash.calc(text); for (int i = 0; i < count; i++) { if (hashes[i] == hash) continue loop; } hashes[count] = hash; strings[count] = text; count++; } } Arrays.sort(strings); count = 0; for (String s : strings) { if (s.length() == 0) continue; if (count++ > 0) { if (count > MAX_VARIANTS_TO_DISPLAY) { sb.append(" and ..."); break; } else { sb.append(", "); } } char c = s.charAt(0); String displayText = c == '<' || StringUtil.isJavaIdentifierStart(c) ? s : '\'' + s + '\''; sb.append(displayText); } if (count > 1 && count < MAX_VARIANTS_TO_DISPLAY) { int idx = sb.lastIndexOf(", "); sb.replace(idx, idx + 1, " or"); } return count > 0; } public int getVariantsSize() { return variants.size(); } public void clearVariants(boolean expected, int start) { MyList<Variant> list = expected ? variants : unexpected; if (start < 0 || start >= list.size()) return; for (int i = start, len = list.size(); i < len; i++) { VARIANTS.recycle(list.get(i)); } list.setSize(start); if (expected) { lastExpectedVariantPos = -1; for (Variant variant : list) { if (lastExpectedVariantPos < variant.position) lastExpectedVariantPos = variant.position; } } } boolean typeExtends(IElementType child, IElementType parent) { if (child == parent) return true; if (extendsSets != null) { for (TokenSet set : extendsSets) { if (set.contains(child) && set.contains(parent)) return true; } } return altExtendsChecker != null && altExtendsChecker.process(child, parent); } } public static class Frame { public int offset; public int position; public int level; public int modifiers; public String name; public int variantCount; public int errorReportedAt; public int errorReportedAtPrev; public PsiBuilder.Marker leftMarker; public Frame() {} public Frame init( PsiBuilder builder, ErrorState state, int level_, int modifiers_, String name_) { offset = builder.getCurrentOffset(); position = builder.rawTokenIndex(); level = level_; modifiers = modifiers_; name = name_; variantCount = state.variants.size(); errorReportedAt = -1; Frame prev = state.frameStack.peekLast(); errorReportedAtPrev = prev == null ? -1 : prev.errorReportedAt; leftMarker = null; return this; } @Override public String toString() { String mod = modifiers == _NONE_ ? "_NONE_, " : ((modifiers & _COLLAPSE_) != 0 ? "_CAN_COLLAPSE_, " : "") + ((modifiers & _LEFT_) != 0 ? "_LEFT_, " : "") + ((modifiers & _LEFT_INNER_) != 0 ? "_LEFT_INNER_, " : "") + ((modifiers & _AND_) != 0 ? "_AND_, " : "") + ((modifiers & _NOT_) != 0 ? "_NOT_, " : ""); return String.format( "{%s:%s:%d, %d, %s%s}", offset, position, level, errorReportedAt, mod, name); } } private static class Variant { int position; Object object; public Variant init(int pos, Object o) { position = pos; object = o; return this; } @Override public String toString() { return "<" + position + ", " + object + ">"; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Variant variant = (Variant) o; if (position != variant.position) return false; if (!this.object.equals(variant.object)) return false; return true; } @Override public int hashCode() { int result = position; result = 31 * result + object.hashCode(); return result; } } private static final int MAX_CHILDREN_IN_TREE = 10; public static boolean parseAsTree( ErrorState state, final PsiBuilder builder, int level, final IElementType chunkType, boolean checkBraces, final Parser parser, final Parser eatMoreCondition) { final LinkedList<Pair<PsiBuilder.Marker, PsiBuilder.Marker>> parenList = new LinkedList<Pair<PsiBuilder.Marker, PsiBuilder.Marker>>(); final LinkedList<Pair<PsiBuilder.Marker, Integer>> siblingList = new LinkedList<Pair<PsiBuilder.Marker, Integer>>(); PsiBuilder.Marker marker = null; final Runnable checkSiblingsRunnable = new Runnable() { @Override public void run() { main: while (!siblingList.isEmpty()) { final Pair<PsiBuilder.Marker, PsiBuilder.Marker> parenPair = parenList.peek(); final int rating = siblingList.getFirst().second; int count = 0; for (Pair<PsiBuilder.Marker, Integer> pair : siblingList) { if (pair.second != rating || parenPair != null && pair.first == parenPair.second) break main; if (++count >= MAX_CHILDREN_IN_TREE) { PsiBuilder.Marker parentMarker = pair.first.precede(); parentMarker.setCustomEdgeTokenBinders( WhitespacesBinders.GREEDY_LEFT_BINDER, null); while (count-- > 0) { siblingList.removeFirst(); } parentMarker.done(chunkType); siblingList.addFirst(Pair.create(parentMarker, rating + 1)); continue main; } } break; } } }; boolean checkParens = state.braces != null && checkBraces; int totalCount = 0; int tokenCount = 0; if (checkParens) { int tokenIdx = -1; while (builder.rawLookup(tokenIdx) == TokenType.WHITE_SPACE) tokenIdx--; LighterASTNode doneMarker = builder.rawLookup(tokenIdx) == state.braces[0].getLeftBraceType() ? builder.getLatestDoneMarker() : null; if (doneMarker != null && doneMarker.getStartOffset() == builder.rawTokenTypeStart(tokenIdx) && doneMarker.getTokenType() == TokenType.ERROR_ELEMENT) { parenList.add( Pair.create(((PsiBuilder.Marker) doneMarker).precede(), (PsiBuilder.Marker) null)); } } while (true) { final IElementType tokenType = builder.getTokenType(); if (checkParens && (tokenType == state.braces[0].getLeftBraceType() || tokenType == state.braces[0].getRightBraceType() && !parenList.isEmpty())) { if (marker != null) { marker.done(chunkType); siblingList.addFirst(Pair.create(marker, 1)); marker = null; tokenCount = 0; } if (tokenType == state.braces[0].getLeftBraceType()) { final Pair<PsiBuilder.Marker, Integer> prev = siblingList.peek(); parenList.addFirst(Pair.create(builder.mark(), prev == null ? null : prev.first)); } checkSiblingsRunnable.run(); builder.advanceLexer(); if (tokenType == state.braces[0].getRightBraceType()) { final Pair<PsiBuilder.Marker, PsiBuilder.Marker> pair = parenList.removeFirst(); pair.first.done(chunkType); // drop all markers inside parens while (!siblingList.isEmpty() && siblingList.getFirst().first != pair.second) { siblingList.removeFirst(); } siblingList.addFirst(Pair.create(pair.first, 1)); checkSiblingsRunnable.run(); } } else { if (marker == null) { marker = builder.mark(); marker.setCustomEdgeTokenBinders(WhitespacesBinders.GREEDY_LEFT_BINDER, null); } final boolean result = (!parenList.isEmpty() || eatMoreCondition.parse(builder, level + 1)) && parser.parse(builder, level + 1); if (result) { tokenCount++; totalCount++; } if (!result) { break; } } if (tokenCount >= MAX_CHILDREN_IN_TREE && marker != null) { marker.done(chunkType); siblingList.addFirst(Pair.create(marker, 1)); checkSiblingsRunnable.run(); marker = null; tokenCount = 0; } } if (marker != null) { marker.drop(); } for (Pair<PsiBuilder.Marker, PsiBuilder.Marker> pair : parenList) { pair.first.drop(); } return totalCount != 0; } private static class DummyBlockElementType extends IElementType implements ICompositeElementType { DummyBlockElementType() { super("DUMMY_BLOCK", Language.ANY); } @NotNull @Override public ASTNode createCompositeNode() { return new DummyBlock(); } } public static class DummyBlock extends CompositePsiElement { DummyBlock() { super(DUMMY_BLOCK); } @NotNull @Override public PsiReference[] getReferences() { return PsiReference.EMPTY_ARRAY; } @NotNull @Override public Language getLanguage() { return getParent().getLanguage(); } } private static class MyList<E> extends ArrayList<E> { MyList(int initialCapacity) { super(initialCapacity); } protected void setSize(int fromIndex) { removeRange(fromIndex, size()); } @Override public boolean add(E e) { int size = size(); if (size >= MAX_VARIANTS_SIZE) { removeRange(MAX_VARIANTS_SIZE / 4, size - MAX_VARIANTS_SIZE / 4); } return super.add(e); } } }
/** @author peter */ @SuppressWarnings("UseOfSystemOutOrSystemErr") public abstract class UsefulTestCase extends TestCase { public static final boolean IS_UNDER_TEAMCITY = System.getenv("TEAMCITY_VERSION") != null; public static final String IDEA_MARKER_CLASS = "com.intellij.openapi.components.impl.stores.IdeaProjectStoreImpl"; public static final String TEMP_DIR_MARKER = "unitTest_"; protected static boolean OVERWRITE_TESTDATA = false; private static final String DEFAULT_SETTINGS_EXTERNALIZED; private static final Random RNG = new SecureRandom(); private static final String ORIGINAL_TEMP_DIR = FileUtil.getTempDirectory(); protected final Disposable myTestRootDisposable = new Disposable() { @Override public void dispose() {} @Override public String toString() { String testName = getTestName(false); return UsefulTestCase.this.getClass() + (StringUtil.isEmpty(testName) ? "" : ".test" + testName); } }; protected static String ourPathToKeep = null; private CodeStyleSettings myOldCodeStyleSettings; private String myTempDir; protected static final Key<String> CREATION_PLACE = Key.create("CREATION_PLACE"); static { // Radar #5755208: Command line Java applications need a way to launch without a Dock icon. System.setProperty("apple.awt.UIElement", "true"); try { CodeInsightSettings defaultSettings = new CodeInsightSettings(); Element oldS = new Element("temp"); defaultSettings.writeExternal(oldS); DEFAULT_SETTINGS_EXTERNALIZED = JDOMUtil.writeElement(oldS, "\n"); } catch (Exception e) { throw new RuntimeException(e); } } protected boolean shouldContainTempFiles() { return true; } @Override protected void setUp() throws Exception { super.setUp(); if (shouldContainTempFiles()) { String testName = getTestName(true); if (StringUtil.isEmptyOrSpaces(testName)) testName = ""; testName = new File(testName).getName(); // in case the test name contains file separators myTempDir = FileUtil.toSystemDependentName( ORIGINAL_TEMP_DIR + "/" + TEMP_DIR_MARKER + testName + "_" + RNG.nextInt(1000)); FileUtil.resetCanonicalTempPathCache(myTempDir); } //noinspection AssignmentToStaticFieldFromInstanceMethod DocumentImpl.CHECK_DOCUMENT_CONSISTENCY = !isPerformanceTest(); } @Override protected void tearDown() throws Exception { try { Disposer.dispose(myTestRootDisposable); cleanupSwingDataStructures(); cleanupDeleteOnExitHookList(); } finally { if (shouldContainTempFiles()) { FileUtil.resetCanonicalTempPathCache(ORIGINAL_TEMP_DIR); if (ourPathToKeep != null && FileUtil.isAncestor(myTempDir, ourPathToKeep, false)) { File[] files = new File(myTempDir).listFiles(); if (files != null) { for (File file : files) { if (!FileUtil.pathsEqual(file.getPath(), ourPathToKeep)) { FileUtil.delete(file); } } } } else { FileUtil.delete(new File(myTempDir)); } } } UIUtil.removeLeakingAppleListeners(); super.tearDown(); } private static final Set<String> DELETE_ON_EXIT_HOOK_DOT_FILES; private static final Class DELETE_ON_EXIT_HOOK_CLASS; static { Class<?> aClass = null; Set<String> files = null; try { aClass = Class.forName("java.io.DeleteOnExitHook"); files = ReflectionUtil.getField(aClass, null, Set.class, "files"); } catch (Exception ignored) { } DELETE_ON_EXIT_HOOK_CLASS = aClass; DELETE_ON_EXIT_HOOK_DOT_FILES = files; } public static void cleanupDeleteOnExitHookList() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { // try to reduce file set retained by java.io.DeleteOnExitHook List<String> list; synchronized (DELETE_ON_EXIT_HOOK_CLASS) { if (DELETE_ON_EXIT_HOOK_DOT_FILES.isEmpty()) return; list = new ArrayList<String>(DELETE_ON_EXIT_HOOK_DOT_FILES); } for (int i = list.size() - 1; i >= 0; i--) { String path = list.get(i); if (FileSystemUtil.getAttributes(path) == null || new File(path).delete()) { synchronized (DELETE_ON_EXIT_HOOK_CLASS) { DELETE_ON_EXIT_HOOK_DOT_FILES.remove(path); } } } } private static void cleanupSwingDataStructures() throws Exception { Class<?> aClass = Class.forName("javax.swing.KeyboardManager"); Method get = aClass.getMethod("getCurrentManager"); get.setAccessible(true); Object manager = get.invoke(null); { Field mapF = aClass.getDeclaredField("componentKeyStrokeMap"); mapF.setAccessible(true); Object map = mapF.get(manager); ((Map) map).clear(); } { Field mapF = aClass.getDeclaredField("containerMap"); mapF.setAccessible(true); Object map = mapF.get(manager); ((Map) map).clear(); } } protected CompositeException checkForSettingsDamage() throws Exception { Application app = ApplicationManager.getApplication(); if (isPerformanceTest() || app == null || app instanceof MockApplication) { return new CompositeException(); } CodeStyleSettings oldCodeStyleSettings = myOldCodeStyleSettings; myOldCodeStyleSettings = null; return doCheckForSettingsDamage(oldCodeStyleSettings, getCurrentCodeStyleSettings()); } public static CompositeException doCheckForSettingsDamage( @NotNull CodeStyleSettings oldCodeStyleSettings, @NotNull CodeStyleSettings currentCodeStyleSettings) throws Exception { CompositeException result = new CompositeException(); final CodeInsightSettings settings = CodeInsightSettings.getInstance(); try { Element newS = new Element("temp"); settings.writeExternal(newS); Assert.assertEquals( "Code insight settings damaged", DEFAULT_SETTINGS_EXTERNALIZED, JDOMUtil.writeElement(newS, "\n")); } catch (AssertionError error) { CodeInsightSettings clean = new CodeInsightSettings(); Element temp = new Element("temp"); clean.writeExternal(temp); settings.loadState(temp); result.add(error); } currentCodeStyleSettings.getIndentOptions(StdFileTypes.JAVA); try { checkSettingsEqual( oldCodeStyleSettings, currentCodeStyleSettings, "Code style settings damaged"); } catch (AssertionError e) { result.add(e); } finally { currentCodeStyleSettings.clearCodeStyleSettings(); } try { InplaceRefactoring.checkCleared(); } catch (AssertionError e) { result.add(e); } try { StartMarkAction.checkCleared(); } catch (AssertionError e) { result.add(e); } return result; } protected void storeSettings() { if (!isPerformanceTest() && ApplicationManager.getApplication() != null) { myOldCodeStyleSettings = getCurrentCodeStyleSettings().clone(); myOldCodeStyleSettings.getIndentOptions(StdFileTypes.JAVA); } } protected CodeStyleSettings getCurrentCodeStyleSettings() { if (CodeStyleSchemes.getInstance().getCurrentScheme() == null) return new CodeStyleSettings(); return CodeStyleSettingsManager.getInstance().getCurrentSettings(); } public Disposable getTestRootDisposable() { return myTestRootDisposable; } @Override protected void runTest() throws Throwable { final Throwable[] throwables = new Throwable[1]; Runnable runnable = new Runnable() { @Override public void run() { try { UsefulTestCase.super.runTest(); } catch (InvocationTargetException e) { e.fillInStackTrace(); throwables[0] = e.getTargetException(); } catch (IllegalAccessException e) { e.fillInStackTrace(); throwables[0] = e; } catch (Throwable e) { throwables[0] = e; } } }; invokeTestRunnable(runnable); if (throwables[0] != null) { throw throwables[0]; } } protected boolean shouldRunTest() { return PlatformTestUtil.canRunTest(getClass()); } public static void edt(Runnable r) { UIUtil.invokeAndWaitIfNeeded(r); } protected void invokeTestRunnable(@NotNull Runnable runnable) throws Exception { UIUtil.invokeAndWaitIfNeeded(runnable); // runnable.run(); } protected void defaultRunBare() throws Throwable { super.runBare(); } @Override public void runBare() throws Throwable { if (!shouldRunTest()) return; if (runInDispatchThread()) { final Throwable[] exception = {null}; UIUtil.invokeAndWaitIfNeeded( new Runnable() { @Override public void run() { try { defaultRunBare(); } catch (Throwable tearingDown) { if (exception[0] == null) exception[0] = tearingDown; } } }); if (exception[0] != null) throw exception[0]; } else { defaultRunBare(); } } protected boolean runInDispatchThread() { return true; } @NonNls public static String toString(Iterable<?> collection) { if (!collection.iterator().hasNext()) { return "<empty>"; } final StringBuilder builder = new StringBuilder(); for (final Object o : collection) { if (o instanceof THashSet) { builder.append(new TreeSet<Object>((THashSet) o)); } else { builder.append(o); } builder.append("\n"); } return builder.toString(); } public static <T> void assertOrderedEquals(T[] actual, T... expected) { assertOrderedEquals(Arrays.asList(actual), expected); } public static <T> void assertOrderedEquals(Iterable<T> actual, T... expected) { assertOrderedEquals(null, actual, expected); } public static void assertOrderedEquals(@NotNull byte[] actual, @NotNull byte[] expected) { assertEquals(actual.length, expected.length); for (int i = 0; i < actual.length; i++) { byte a = actual[i]; byte e = expected[i]; assertEquals("not equals at index: " + i, e, a); } } public static <T> void assertOrderedEquals( final String errorMsg, @NotNull Iterable<T> actual, @NotNull T... expected) { Assert.assertNotNull(actual); Assert.assertNotNull(expected); assertOrderedEquals(errorMsg, actual, Arrays.asList(expected)); } public static <T> void assertOrderedEquals( final Iterable<? extends T> actual, final Collection<? extends T> expected) { assertOrderedEquals(null, actual, expected); } public static <T> void assertOrderedEquals( final String erroMsg, final Iterable<? extends T> actual, final Collection<? extends T> expected) { ArrayList<T> list = new ArrayList<T>(); for (T t : actual) { list.add(t); } if (!list.equals(new ArrayList<T>(expected))) { String expectedString = toString(expected); String actualString = toString(actual); Assert.assertEquals(erroMsg, expectedString, actualString); Assert.fail( "Warning! 'toString' does not reflect the difference.\nExpected: " + expectedString + "\nActual: " + actualString); } } public static <T> void assertOrderedCollection(T[] collection, @NotNull Consumer<T>... checkers) { Assert.assertNotNull(collection); assertOrderedCollection(Arrays.asList(collection), checkers); } public static <T> void assertSameElements(T[] collection, T... expected) { assertSameElements(Arrays.asList(collection), expected); } public static <T> void assertSameElements(Collection<? extends T> collection, T... expected) { assertSameElements(collection, Arrays.asList(expected)); } public static <T> void assertSameElements( Collection<? extends T> collection, Collection<T> expected) { assertSameElements(null, collection, expected); } public static <T> void assertSameElements( String message, Collection<? extends T> collection, Collection<T> expected) { assertNotNull(collection); assertNotNull(expected); if (collection.size() != expected.size() || !new HashSet<T>(expected).equals(new HashSet<T>(collection))) { Assert.assertEquals(message, toString(expected, "\n"), toString(collection, "\n")); Assert.assertEquals(message, new HashSet<T>(expected), new HashSet<T>(collection)); } } public <T> void assertContainsOrdered(Collection<? extends T> collection, T... expected) { assertContainsOrdered(collection, Arrays.asList(expected)); } public <T> void assertContainsOrdered( Collection<? extends T> collection, Collection<T> expected) { ArrayList<T> copy = new ArrayList<T>(collection); copy.retainAll(expected); assertOrderedEquals(toString(collection), copy, expected); } public <T> void assertContainsElements(Collection<? extends T> collection, T... expected) { assertContainsElements(collection, Arrays.asList(expected)); } public <T> void assertContainsElements( Collection<? extends T> collection, Collection<T> expected) { ArrayList<T> copy = new ArrayList<T>(collection); copy.retainAll(expected); assertSameElements(toString(collection), copy, expected); } public static String toString(Object[] collection, String separator) { return toString(Arrays.asList(collection), separator); } public <T> void assertDoesntContain(Collection<? extends T> collection, T... notExpected) { assertDoesntContain(collection, Arrays.asList(notExpected)); } public <T> void assertDoesntContain( Collection<? extends T> collection, Collection<T> notExpected) { ArrayList<T> expected = new ArrayList<T>(collection); expected.removeAll(notExpected); assertSameElements(collection, expected); } public static String toString(Collection<?> collection, String separator) { List<String> list = ContainerUtil.map2List( collection, new Function<Object, String>() { @Override public String fun(final Object o) { return String.valueOf(o); } }); Collections.sort(list); StringBuilder builder = new StringBuilder(); boolean flag = false; for (final String o : list) { if (flag) { builder.append(separator); } builder.append(o); flag = true; } return builder.toString(); } public static <T> void assertOrderedCollection( Collection<? extends T> collection, Consumer<T>... checkers) { Assert.assertNotNull(collection); if (collection.size() != checkers.length) { Assert.fail(toString(collection)); } int i = 0; for (final T actual : collection) { try { checkers[i].consume(actual); } catch (AssertionFailedError e) { System.out.println(i + ": " + actual); throw e; } i++; } } public static <T> void assertUnorderedCollection(T[] collection, Consumer<T>... checkers) { assertUnorderedCollection(Arrays.asList(collection), checkers); } public static <T> void assertUnorderedCollection( Collection<? extends T> collection, Consumer<T>... checkers) { Assert.assertNotNull(collection); if (collection.size() != checkers.length) { Assert.fail(toString(collection)); } Set<Consumer<T>> checkerSet = new HashSet<Consumer<T>>(Arrays.asList(checkers)); int i = 0; Throwable lastError = null; for (final T actual : collection) { boolean flag = true; for (final Consumer<T> condition : checkerSet) { Throwable error = accepts(condition, actual); if (error == null) { checkerSet.remove(condition); flag = false; break; } else { lastError = error; } } if (flag) { lastError.printStackTrace(); Assert.fail("Incorrect element(" + i + "): " + actual); } i++; } } private static <T> Throwable accepts(final Consumer<T> condition, final T actual) { try { condition.consume(actual); return null; } catch (Throwable e) { return e; } } public static <T> T assertInstanceOf(Object o, Class<T> aClass) { Assert.assertNotNull("Expected instance of: " + aClass.getName() + " actual: " + null, o); Assert.assertTrue( "Expected instance of: " + aClass.getName() + " actual: " + o.getClass().getName(), aClass.isInstance(o)); @SuppressWarnings("unchecked") T t = (T) o; return t; } public static <T> T assertOneElement(Collection<T> collection) { Assert.assertNotNull(collection); Assert.assertEquals(toString(collection), 1, collection.size()); return collection.iterator().next(); } public static <T> T assertOneElement(T[] ts) { Assert.assertNotNull(ts); Assert.assertEquals(Arrays.asList(ts).toString(), 1, ts.length); return ts[0]; } public static <T> void assertOneOf(T value, T... values) { boolean found = false; for (T v : values) { if (value == v || value != null && value.equals(v)) { found = true; } } Assert.assertTrue(value + " should be equal to one of " + Arrays.toString(values), found); } public static void printThreadDump() { PerformanceWatcher.dumpThreadsToConsole("Thread dump:"); } public static void assertEmpty(final Object[] array) { assertOrderedEquals(array); } public static void assertNotEmpty(final Collection<?> collection) { if (collection == null) return; assertTrue(!collection.isEmpty()); } public static void assertEmpty(final Collection<?> collection) { assertEmpty(collection.toString(), collection); } public static void assertNullOrEmpty(final Collection<?> collection) { if (collection == null) return; assertEmpty(null, collection); } public static void assertEmpty(final String s) { assertTrue(s, StringUtil.isEmpty(s)); } public static <T> void assertEmpty(final String errorMsg, final Collection<T> collection) { assertOrderedEquals(errorMsg, collection); } public static void assertSize(int expectedSize, final Object[] array) { assertEquals(toString(Arrays.asList(array)), expectedSize, array.length); } public static void assertSize(int expectedSize, final Collection<?> c) { assertEquals(toString(c), expectedSize, c.size()); } protected <T extends Disposable> T disposeOnTearDown(final T disposable) { Disposer.register(myTestRootDisposable, disposable); return disposable; } public static void assertSameLines(String expected, String actual) { String expectedText = StringUtil.convertLineSeparators(expected.trim()); String actualText = StringUtil.convertLineSeparators(actual.trim()); Assert.assertEquals(expectedText, actualText); } public static void assertExists(File file) { assertTrue("File should exist " + file, file.exists()); } public static void assertDoesntExist(File file) { assertFalse("File should not exist " + file, file.exists()); } protected String getTestName(boolean lowercaseFirstLetter) { String name = getName(); return getTestName(name, lowercaseFirstLetter); } public static String getTestName(String name, boolean lowercaseFirstLetter) { if (name == null) { return ""; } name = StringUtil.trimStart(name, "test"); if (StringUtil.isEmpty(name)) { return ""; } return lowercaseFirstLetter(name, lowercaseFirstLetter); } public static String lowercaseFirstLetter(String name, boolean lowercaseFirstLetter) { if (lowercaseFirstLetter && !isAllUppercaseName(name)) { name = Character.toLowerCase(name.charAt(0)) + name.substring(1); } return name; } public static boolean isAllUppercaseName(String name) { int uppercaseChars = 0; for (int i = 0; i < name.length(); i++) { if (Character.isLowerCase(name.charAt(i))) { return false; } if (Character.isUpperCase(name.charAt(i))) { uppercaseChars++; } } return uppercaseChars >= 3; } protected String getTestDirectoryName() { final String testName = getTestName(true); return testName.replaceAll("_.*", ""); } protected static void assertSameLinesWithFile(String filePath, String actualText) { String fileText; try { if (OVERWRITE_TESTDATA) { VfsTestUtil.overwriteTestData(filePath, actualText); System.out.println("File " + filePath + " created."); } fileText = FileUtil.loadFile(new File(filePath), CharsetToolkit.UTF8); } catch (FileNotFoundException e) { VfsTestUtil.overwriteTestData(filePath, actualText); throw new AssertionFailedError("No output text found. File " + filePath + " created."); } catch (IOException e) { throw new RuntimeException(e); } String expected = StringUtil.convertLineSeparators(fileText.trim()); String actual = StringUtil.convertLineSeparators(actualText.trim()); if (!Comparing.equal(expected, actual)) { throw new FileComparisonFailure(null, expected, actual, filePath); } } public static void clearFields(final Object test) throws IllegalAccessException { Class aClass = test.getClass(); while (aClass != null) { clearDeclaredFields(test, aClass); aClass = aClass.getSuperclass(); } } public static void clearDeclaredFields(Object test, Class aClass) throws IllegalAccessException { if (aClass == null) return; for (final Field field : aClass.getDeclaredFields()) { @NonNls final String name = field.getDeclaringClass().getName(); if (!name.startsWith("junit.framework.") && !name.startsWith("com.intellij.testFramework.")) { final int modifiers = field.getModifiers(); if ((modifiers & Modifier.FINAL) == 0 && (modifiers & Modifier.STATIC) == 0 && !field.getType().isPrimitive()) { field.setAccessible(true); field.set(test, null); } } } } @SuppressWarnings("deprecation") protected static void checkSettingsEqual( JDOMExternalizable expected, JDOMExternalizable settings, String message) throws Exception { if (expected == null || settings == null) return; Element oldS = new Element("temp"); expected.writeExternal(oldS); Element newS = new Element("temp"); settings.writeExternal(newS); String newString = JDOMUtil.writeElement(newS, "\n"); String oldString = JDOMUtil.writeElement(oldS, "\n"); Assert.assertEquals(message, oldString, newString); } public boolean isPerformanceTest() { String name = getName(); return name != null && name.contains("Performance") || getClass().getName().contains("Performance"); } public static void doPostponedFormatting(final Project project) { DocumentUtil.writeInRunUndoTransparentAction( new Runnable() { @Override public void run() { PsiDocumentManager.getInstance(project).commitAllDocuments(); PostprocessReformattingAspect.getInstance(project).doPostponedFormatting(); } }); } protected static void checkAllTimersAreDisposed() { try { Class<?> aClass = Class.forName("javax.swing.TimerQueue"); Method inst = aClass.getDeclaredMethod("sharedInstance"); inst.setAccessible(true); Object queue = inst.invoke(null); Field field = aClass.getDeclaredField("firstTimer"); field.setAccessible(true); Object firstTimer = field.get(queue); if (firstTimer != null) { try { fail("Not disposed Timer: " + firstTimer.toString() + "; queue:" + queue); } finally { field.set(queue, null); } } } catch (Throwable e) { // Ignore } } /** * Checks that code block throw corresponding exception. * * @param exceptionCase Block annotated with some exception type * @throws Throwable */ protected void assertException(final AbstractExceptionCase exceptionCase) throws Throwable { assertException(exceptionCase, null); } /** * Checks that code block throw corresponding exception with expected error msg. If expected error * message is null it will not be checked. * * @param exceptionCase Block annotated with some exception type * @param expectedErrorMsg expected error messge * @throws Throwable */ protected void assertException( final AbstractExceptionCase exceptionCase, @Nullable final String expectedErrorMsg) throws Throwable { assertExceptionOccurred(true, exceptionCase, expectedErrorMsg); } /** * Checks that code block doesn't throw corresponding exception. * * @param exceptionCase Block annotated with some exception type * @throws Throwable */ protected void assertNoException(final AbstractExceptionCase exceptionCase) throws Throwable { assertExceptionOccurred(false, exceptionCase, null); } protected void assertNoThrowable(final Runnable closure) { String throwableName = null; try { closure.run(); } catch (Throwable thr) { throwableName = thr.getClass().getName(); } assertNull(throwableName); } private static void assertExceptionOccurred( boolean shouldOccur, AbstractExceptionCase exceptionCase, String expectedErrorMsg) throws Throwable { boolean wasThrown = false; try { exceptionCase.tryClosure(); } catch (Throwable e) { if (shouldOccur) { wasThrown = true; final String errorMessage = exceptionCase.getAssertionErrorMessage(); assertEquals(errorMessage, exceptionCase.getExpectedExceptionClass(), e.getClass()); if (expectedErrorMsg != null) { assertEquals("Compare error messages", expectedErrorMsg, e.getMessage()); } } else if (exceptionCase.getExpectedExceptionClass().equals(e.getClass())) { wasThrown = true; System.out.println(""); e.printStackTrace(System.out); fail("Exception isn't expected here. Exception message: " + e.getMessage()); } else { throw e; } } finally { if (shouldOccur && !wasThrown) { fail(exceptionCase.getAssertionErrorMessage()); } } } protected boolean annotatedWith(@NotNull Class annotationClass) { Class<?> aClass = getClass(); String methodName = "test" + getTestName(false); boolean methodChecked = false; while (aClass != null && aClass != Object.class) { if (aClass.getAnnotation(annotationClass) != null) return true; if (!methodChecked) { try { Method method = aClass.getDeclaredMethod(methodName); if (method.getAnnotation(annotationClass) != null) return true; methodChecked = true; } catch (NoSuchMethodException ignored) { } } aClass = aClass.getSuperclass(); } return false; } protected String getHomePath() { return PathManager.getHomePath().replace(File.separatorChar, '/'); } protected static boolean isInHeadlessEnvironment() { return GraphicsEnvironment.isHeadless(); } public static void refreshRecursively(@NotNull VirtualFile file) { VfsUtilCore.visitChildrenRecursively( file, new VirtualFileVisitor() { @Override public boolean visitFile(@NotNull VirtualFile file) { file.getChildren(); return true; } }); file.refresh(false, true); } @NotNull public static Test filteredSuite(@RegExp String regexp, @NotNull Test test) { final Pattern pattern = Pattern.compile(regexp); final TestSuite testSuite = new TestSuite(); new Processor<Test>() { @Override public boolean process(Test test) { if (test instanceof TestSuite) { for (int i = 0, len = ((TestSuite) test).testCount(); i < len; i++) { process(((TestSuite) test).testAt(i)); } } else if (pattern.matcher(test.toString()).find()) { testSuite.addTest(test); } return false; } }.process(test); return testSuite; } }
public abstract class PsiDocumentManagerBase extends PsiDocumentManager implements ProjectComponent, DocumentListener { protected static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.PsiDocumentManagerImpl"); protected static final Key<PsiFile> HARD_REF_TO_PSI = new Key<PsiFile>("HARD_REFERENCE_TO_PSI"); protected static final Key<List<Runnable>> ACTION_AFTER_COMMIT = Key.create("ACTION_AFTER_COMMIT"); protected final Project myProject; protected final PsiManager myPsiManager; protected final DocumentCommitProcessor myDocumentCommitProcessor; protected final Set<Document> myUncommittedDocuments = Collections.synchronizedSet(new HashSet<Document>()); protected volatile boolean myIsCommitInProgress; protected final PsiToDocumentSynchronizer mySynchronizer; protected final List<Listener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); protected final SmartPointerManagerImpl mySmartPointerManager; public PsiDocumentManagerBase( @NotNull final Project project, @NotNull PsiManager psiManager, @NotNull SmartPointerManager smartPointerManager, @NotNull MessageBus bus, @NonNls @NotNull final DocumentCommitProcessor documentCommitProcessor) { myProject = project; myPsiManager = psiManager; myDocumentCommitProcessor = documentCommitProcessor; mySmartPointerManager = (SmartPointerManagerImpl) smartPointerManager; mySynchronizer = new PsiToDocumentSynchronizer(this, bus); myPsiManager.addPsiTreeChangeListener(mySynchronizer); } @Override public void projectOpened() {} @Override public void projectClosed() {} @Override @NotNull public String getComponentName() { return "PsiDocumentManager"; } @Override public void initComponent() {} @Override public void disposeComponent() {} @Override @Nullable public PsiFile getPsiFile(@NotNull Document document) { final PsiFile userData = document.getUserData(HARD_REF_TO_PSI); if (userData != null) return userData; PsiFile psiFile = getCachedPsiFile(document); if (psiFile != null) return psiFile; final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile == null || !virtualFile.isValid()) return null; psiFile = getPsiFile(virtualFile); if (psiFile == null) return null; fireFileCreated(document, psiFile); return psiFile; } public static void cachePsi(@NotNull Document document, @NotNull PsiFile file) { document.putUserData(HARD_REF_TO_PSI, file); } @Override public PsiFile getCachedPsiFile(@NotNull Document document) { final PsiFile userData = document.getUserData(HARD_REF_TO_PSI); if (userData != null) return userData; final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile == null || !virtualFile.isValid()) return null; return getCachedPsiFile(virtualFile); } @Nullable public FileViewProvider getCachedViewProvider(@NotNull Document document) { final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile == null || !virtualFile.isValid()) return null; return ((PsiManagerEx) myPsiManager).getFileManager().findCachedViewProvider(virtualFile); } @Nullable protected PsiFile getCachedPsiFile(VirtualFile virtualFile) { return ((PsiManagerEx) myPsiManager).getFileManager().getCachedPsiFile(virtualFile); } @Nullable protected PsiFile getPsiFile(VirtualFile virtualFile) { return ((PsiManagerEx) myPsiManager).getFileManager().findFile(virtualFile); } @Nullable @Override public Document getDocument(@NotNull PsiFile file) { if (file instanceof PsiBinaryFile) return null; Document document = getCachedDocument(file); if (document != null) { if (!file.getViewProvider().isPhysical() && document.getUserData(HARD_REF_TO_PSI) == null) { cachePsi(document, file); } return document; } if (!file.getViewProvider().isEventSystemEnabled()) return null; document = FileDocumentManager.getInstance().getDocument(file.getViewProvider().getVirtualFile()); if (document != null) { if (document.getTextLength() != file.getTextLength()) { throw new AssertionError( "Modified PSI with no document: " + file + "; physical=" + file.getViewProvider().isPhysical()); } if (!file.getViewProvider().isPhysical()) { cachePsi(document, file); } } return document; } @Override public Document getCachedDocument(@NotNull PsiFile file) { if (!file.isPhysical()) return null; VirtualFile vFile = file.getViewProvider().getVirtualFile(); return FileDocumentManager.getInstance().getCachedDocument(vFile); } @Override public void commitAllDocuments() { ApplicationManager.getApplication().assertIsDispatchThread(); if (myUncommittedDocuments.isEmpty()) return; final Document[] documents = getUncommittedDocuments(); for (Document document : documents) { commitDocument(document); } LOG.assertTrue(!hasUncommitedDocuments(), myUncommittedDocuments); } @Override public void performForCommittedDocument( @NotNull final Document doc, @NotNull final Runnable action) { final Document document = doc instanceof DocumentWindow ? ((DocumentWindow) doc).getDelegate() : doc; if (isCommitted(document)) { action.run(); } else { addRunOnCommit(document, action); } } private final Map<Object, Runnable> actionsWhenAllDocumentsAreCommitted = new LinkedHashMap<Object, Runnable>(); // accessed from EDT only private static final Object PERFORM_ALWAYS_KEY = new Object() { @Override @NonNls public String toString() { return "PERFORM_ALWAYS"; } }; /** * Cancel previously registered action and schedules (new) action to be executed when all * documents are committed. * * @param key the (unique) id of the action. * @param action The action to be executed after automatic commit. This action will overwrite any * action which was registered under this key earlier. The action will be executed in EDT. * @return true if action has been run immediately, or false if action was scheduled for execution * later. */ public boolean cancelAndRunWhenAllCommitted( @NonNls @NotNull Object key, @NotNull final Runnable action) { ApplicationManager.getApplication().assertIsDispatchThread(); if (myProject.isDisposed()) { action.run(); return true; } if (myUncommittedDocuments.isEmpty()) { action.run(); assert actionsWhenAllDocumentsAreCommitted.isEmpty() : actionsWhenAllDocumentsAreCommitted; return true; } actionsWhenAllDocumentsAreCommitted.put(key, action); return false; } public static void addRunOnCommit(@NotNull Document document, @NotNull Runnable action) { synchronized (ACTION_AFTER_COMMIT) { List<Runnable> list = document.getUserData(ACTION_AFTER_COMMIT); if (list == null) { document.putUserData(ACTION_AFTER_COMMIT, list = new SmartList<Runnable>()); } list.add(action); } } @Override public void commitDocument(@NotNull final Document doc) { final Document document = doc instanceof DocumentWindow ? ((DocumentWindow) doc).getDelegate() : doc; if (!isCommitted(document)) { doCommit(document, null); } } public boolean finishCommit( @NotNull final Document document, @NotNull final List<Processor<Document>> finishProcessors, final boolean synchronously, @NotNull final Object reason) { assert !myProject.isDisposed() : "Already disposed"; final boolean[] ok = {true}; ApplicationManager.getApplication() .runWriteAction( new CommitToPsiFileAction(document, myProject) { @Override public void run() { ok[0] = finishCommitInWriteAction(document, finishProcessors, synchronously); } }); if (ok[0]) { // otherwise changes maybe not synced to the document yet, and injectors will crash if (!mySynchronizer.isDocumentAffectedByTransactions(document)) { final InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(myProject); if (injectedLanguageManager != null) injectedLanguageManager.startRunInjectors(document, synchronously); } // run after commit actions outside write action runAfterCommitActions(document); if (DebugUtil.DO_EXPENSIVE_CHECKS) { checkAllElementsValid(document, reason); } } return ok[0]; } protected boolean finishCommitInWriteAction( @NotNull final Document document, @NotNull final List<Processor<Document>> finishProcessors, final boolean synchronously) { if (myProject.isDisposed()) return false; assert !(document instanceof DocumentWindow); myIsCommitInProgress = true; boolean success = true; try { final FileViewProvider viewProvider = getCachedViewProvider(document); if (viewProvider != null) { for (Processor<Document> finishRunnable : finishProcessors) { success = finishRunnable.process(document); if (synchronously) { assert success; } if (!success) { break; } } viewProvider.contentsSynchronized(); } } finally { myDocumentCommitProcessor.log( "in PDI.finishDoc: ", null, synchronously, success, myUncommittedDocuments); if (success) { myUncommittedDocuments.remove(document); myDocumentCommitProcessor.log( "in PDI.finishDoc: removed doc", null, synchronously, success, myUncommittedDocuments); } myIsCommitInProgress = false; myDocumentCommitProcessor.log( "in PDI.finishDoc: exit", null, synchronously, success, myUncommittedDocuments); } return success; } private void checkAllElementsValid(@NotNull Document document, @NotNull final Object reason) { final PsiFile psiFile = getCachedPsiFile(document); if (psiFile != null) { psiFile.accept( new PsiRecursiveElementWalkingVisitor() { @Override public void visitElement(PsiElement element) { if (!element.isValid()) { throw new AssertionError( "Commit to '" + psiFile.getVirtualFile() + "' has led to invalid element: " + element + "; Reason: '" + reason + "'"); } } }); } } protected void doCommit(@NotNull final Document document, final PsiFile excludeFile) { assert !myIsCommitInProgress : "Do not call commitDocument() from inside PSI change listener"; ApplicationManager.getApplication() .runWriteAction( new Runnable() { @Override public void run() { // otherwise there are many clients calling commitAllDocs() on PSI childrenChanged() if (getSynchronizer().isDocumentAffectedByTransactions(document) && excludeFile == null) return; myIsCommitInProgress = true; try { myDocumentCommitProcessor.commitSynchronously(document, myProject, excludeFile); } finally { myIsCommitInProgress = false; } assert !myUncommittedDocuments.contains(document) : "Document :" + document; } }); } public void commitOtherFilesAssociatedWithDocument( final Document document, final PsiFile psiFile) {} @Override public <T> T commitAndRunReadAction(@NotNull final Computable<T> computation) { final Ref<T> ref = Ref.create(null); commitAndRunReadAction( new Runnable() { @Override public void run() { ref.set(computation.compute()); } }); return ref.get(); } @Override public void commitAndRunReadAction(@NotNull final Runnable runnable) { final Application application = ApplicationManager.getApplication(); if (SwingUtilities.isEventDispatchThread()) { commitAllDocuments(); runnable.run(); } else { LOG.assertTrue( !ApplicationManager.getApplication().isReadAccessAllowed(), "Don't call commitAndRunReadAction inside ReadAction, it will cause a deadlock otherwise."); final Semaphore s1 = new Semaphore(); final Semaphore s2 = new Semaphore(); final boolean[] committed = {false}; application.runReadAction( new Runnable() { @Override public void run() { if (myUncommittedDocuments.isEmpty()) { runnable.run(); committed[0] = true; } else { s1.down(); s2.down(); final Runnable commitRunnable = new Runnable() { @Override public void run() { commitAllDocuments(); s1.up(); s2.waitFor(); } }; final ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator(); if (progressIndicator == null) { ApplicationManager.getApplication().invokeLater(commitRunnable); } else { ApplicationManager.getApplication() .invokeLater(commitRunnable, progressIndicator.getModalityState()); } } } }); if (!committed[0]) { s1.waitFor(); application.runReadAction( new Runnable() { @Override public void run() { s2.up(); runnable.run(); } }); } } } /** * Schedules action to be executed when all documents are committed. * * @return true if action has been run immediately, or false if action was scheduled for execution * later. */ @Override public boolean performWhenAllCommitted(@NotNull final Runnable action) { ApplicationManager.getApplication().assertIsDispatchThread(); assert !myProject.isDisposed() : "Already disposed: " + myProject; if (myUncommittedDocuments.isEmpty()) { action.run(); return true; } CompositeRunnable actions = (CompositeRunnable) actionsWhenAllDocumentsAreCommitted.get(PERFORM_ALWAYS_KEY); if (actions == null) { actions = new CompositeRunnable(); actionsWhenAllDocumentsAreCommitted.put(PERFORM_ALWAYS_KEY, actions); } actions.add(action); myDocumentCommitProcessor.log( "PDI: added performWhenAllCommitted", null, false, action, myUncommittedDocuments); return false; } private static class CompositeRunnable extends ArrayList<Runnable> implements Runnable { @Override public void run() { for (Runnable runnable : this) { runnable.run(); } } } private void runAfterCommitActions(@NotNull Document document) { ApplicationManager.getApplication().assertIsDispatchThread(); List<Runnable> list; synchronized (ACTION_AFTER_COMMIT) { list = document.getUserData(ACTION_AFTER_COMMIT); if (list != null) { list = new ArrayList<Runnable>(list); document.putUserData(ACTION_AFTER_COMMIT, null); } } if (list != null) { for (final Runnable runnable : list) { runnable.run(); } } if (!hasUncommitedDocuments() && !actionsWhenAllDocumentsAreCommitted.isEmpty()) { List<Object> keys = new ArrayList<Object>(actionsWhenAllDocumentsAreCommitted.keySet()); for (Object key : keys) { Runnable action = actionsWhenAllDocumentsAreCommitted.remove(key); myDocumentCommitProcessor.log("Running after commit runnable: ", null, false, key, action); action.run(); } } } @Override public void addListener(@NotNull Listener listener) { myListeners.add(listener); } @Override public void removeListener(@NotNull Listener listener) { myListeners.remove(listener); } @Override public boolean isDocumentBlockedByPsi(@NotNull Document doc) { return false; } @Override public void doPostponedOperationsAndUnblockDocument(@NotNull Document doc) {} protected void fireDocumentCreated(@NotNull Document document, PsiFile file) { for (Listener listener : myListeners) { listener.documentCreated(document, file); } } private void fireFileCreated(Document document, PsiFile file) { for (Listener listener : myListeners) { listener.fileCreated(file, document); } } @Override @NotNull public Document[] getUncommittedDocuments() { ApplicationManager.getApplication().assertIsDispatchThread(); return myUncommittedDocuments.toArray(new Document[myUncommittedDocuments.size()]); } public Collection<Document> getUncommittedDocumentsUnsafe() { return myUncommittedDocuments; } @Override public boolean isUncommited(@NotNull Document document) { return !isCommitted(document); } @Override public boolean isCommitted(@NotNull Document document) { if (getSynchronizer().isInSynchronization(document)) return true; return !((DocumentEx) document).isInEventsHandling() && !myUncommittedDocuments.contains(document); } @Override public boolean hasUncommitedDocuments() { return !myIsCommitInProgress && !myUncommittedDocuments.isEmpty(); } @Override public void beforeDocumentChange(DocumentEvent event) { final Document document = event.getDocument(); final FileViewProvider viewProvider = getCachedViewProvider(document); if (viewProvider == null) return; if (!isRelevant(viewProvider)) return; VirtualFile virtualFile = viewProvider.getVirtualFile(); if (virtualFile.getFileType().isBinary()) return; final List<PsiFile> files = viewProvider.getAllFiles(); PsiFile psiCause = null; for (PsiFile file : files) { mySmartPointerManager.fastenBelts(file, event.getOffset(), null); if (TextBlock.get(file).isLocked()) { psiCause = file; } } if (psiCause == null) { beforeDocumentChangeOnUnlockedDocument(viewProvider); } ((SingleRootFileViewProvider) viewProvider).beforeDocumentChanged(psiCause); } protected void beforeDocumentChangeOnUnlockedDocument( @NotNull final FileViewProvider viewProvider) {} @Override public void documentChanged(DocumentEvent event) { final Document document = event.getDocument(); final FileViewProvider viewProvider = getCachedViewProvider(document); if (viewProvider == null) return; if (!isRelevant(viewProvider)) return; ApplicationManager.getApplication().assertWriteAccessAllowed(); final List<PsiFile> files = viewProvider.getAllFiles(); boolean commitNecessary = true; for (PsiFile file : files) { mySmartPointerManager.unfastenBelts(file, event.getOffset()); final TextBlock textBlock = TextBlock.get(file); if (textBlock.isLocked()) { commitNecessary = false; continue; } textBlock.documentChanged(event); assert file instanceof PsiFileImpl || "mock.file".equals(file.getName()) && ApplicationManager.getApplication().isUnitTestMode() : event + "; file=" + file + "; allFiles=" + files + "; viewProvider=" + viewProvider; } boolean forceCommit = ApplicationManager.getApplication().hasWriteAction(ExternalChangeAction.class) && (SystemProperties.getBooleanProperty("idea.force.commit.on.external.change", false) || ApplicationManager.getApplication().isHeadlessEnvironment()); // Consider that it's worth to perform complete re-parse instead of merge if the whole document // text is replaced and // current document lines number is roughly above 5000. This makes sense in situations when // external change is performed // for the huge file (that causes the whole document to be reloaded and 'merge' way takes a // while to complete). if (event.isWholeTextReplaced() && document.getTextLength() > 100000) { document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE); } if (commitNecessary) { myUncommittedDocuments.add(document); if (forceCommit) { commitDocument(document); } else if (!((DocumentEx) document).isInBulkUpdate()) { myDocumentCommitProcessor.commitAsynchronously(myProject, document, event); } } } private boolean isRelevant(@NotNull FileViewProvider viewProvider) { VirtualFile virtualFile = viewProvider.getVirtualFile(); return !virtualFile.getFileType().isBinary() && viewProvider.getManager() == myPsiManager && !myPsiManager.getProject().isDisposed(); } public static boolean checkConsistency(PsiFile psiFile, Document document) { // todo hack if (psiFile.getVirtualFile() == null) return true; CharSequence editorText = document.getCharsSequence(); int documentLength = document.getTextLength(); if (psiFile.textMatches(editorText)) { LOG.assertTrue(psiFile.getTextLength() == documentLength); return true; } char[] fileText = psiFile.textToCharArray(); @SuppressWarnings({"NonConstantStringShouldBeStringBuffer"}) @NonNls String error = "File '" + psiFile.getName() + "' text mismatch after reparse. " + "File length=" + fileText.length + "; Doc length=" + documentLength + "\n"; int i = 0; for (; i < documentLength; i++) { if (i >= fileText.length) { error += "editorText.length > psiText.length i=" + i + "\n"; break; } if (i >= editorText.length()) { error += "editorText.length > psiText.length i=" + i + "\n"; break; } if (editorText.charAt(i) != fileText[i]) { error += "first unequal char i=" + i + "\n"; break; } } // error += "*********************************************" + "\n"; // if (i <= 500){ // error += "Equal part:" + editorText.subSequence(0, i) + "\n"; // } // else{ // error += "Equal part start:\n" + editorText.subSequence(0, 200) + "\n"; // error += "................................................" + "\n"; // error += "................................................" + "\n"; // error += "................................................" + "\n"; // error += "Equal part end:\n" + editorText.subSequence(i - 200, i) + "\n"; // } error += "*********************************************" + "\n"; error += "Editor Text tail:(" + (documentLength - i) + ")\n"; // + editorText.subSequence(i, Math.min(i + 300, documentLength)) + "\n"; error += "*********************************************" + "\n"; error += "Psi Text tail:(" + (fileText.length - i) + ")\n"; error += "*********************************************" + "\n"; if (document instanceof DocumentWindow) { error += "doc: '" + document.getText() + "'\n"; error += "psi: '" + psiFile.getText() + "'\n"; error += "ast: '" + psiFile.getNode().getText() + "'\n"; error += psiFile.getLanguage() + "\n"; PsiElement context = InjectedLanguageManager.getInstance(psiFile.getProject()).getInjectionHost(psiFile); if (context != null) { error += "context: " + context + "; text: '" + context.getText() + "'\n"; error += "context file: " + context.getContainingFile() + "\n"; } error += "document window ranges: " + Arrays.asList(((DocumentWindow) document).getHostRanges()) + "\n"; } LOG.error(error); // document.replaceString(0, documentLength, psiFile.getText()); return false; } @TestOnly public void clearUncommittedDocuments() { myUncommittedDocuments.clear(); mySynchronizer.cleanupForNextTest(); } public PsiToDocumentSynchronizer getSynchronizer() { return mySynchronizer; } }
public class MavenDomUtil { private static final Key<Pair<Long, Set<VirtualFile>>> FILTERED_RESOURCES_ROOTS_KEY = Key.create("MavenDomUtil.FILTERED_RESOURCES_ROOTS"); // see http://maven.apache.org/settings.html private static final Set<String> SUBTAGS_IN_SETTINGS_FILE = ContainerUtil.newHashSet( "localRepository", "interactiveMode", "usePluginRegistry", "offline", "pluginGroups", "servers", "mirrors", "proxies", "profiles", "activeProfiles"); public static boolean isMavenFile(PsiFile file) { return isProjectFile(file) || isProfilesFile(file) || isSettingsFile(file); } public static boolean isProjectFile(PsiFile file) { if (!(file instanceof XmlFile)) return false; String name = file.getName(); return name.equals(MavenConstants.POM_XML) || name.endsWith(".pom") || name.equals(MavenConstants.SUPER_POM_XML); } public static boolean isProfilesFile(PsiFile file) { if (!(file instanceof XmlFile)) return false; return MavenConstants.PROFILES_XML.equals(file.getName()); } public static boolean isSettingsFile(PsiFile file) { if (!(file instanceof XmlFile)) return false; XmlTag rootTag = ((XmlFile) file).getRootTag(); if (rootTag == null || !"settings".equals(rootTag.getName())) return false; String xmlns = rootTag.getAttributeValue("xmlns"); if (xmlns != null) { return xmlns.contains("maven"); } boolean hasTag = false; for (PsiElement e = rootTag.getFirstChild(); e != null; e = e.getNextSibling()) { if (e instanceof XmlTag) { if (SUBTAGS_IN_SETTINGS_FILE.contains(((XmlTag) e).getName())) return true; hasTag = true; } } return !hasTag; } public static boolean isMavenFile(PsiElement element) { return isMavenFile(element.getContainingFile()); } @Nullable public static Module findContainingMavenizedModule(@NotNull PsiFile psiFile) { VirtualFile file = psiFile.getVirtualFile(); if (file == null) return null; Project project = psiFile.getProject(); MavenProjectsManager manager = MavenProjectsManager.getInstance(project); if (!manager.isMavenizedProject()) return null; ProjectFileIndex index = ProjectRootManager.getInstance(project).getFileIndex(); Module module = index.getModuleForFile(file); if (module == null || !manager.isMavenizedModule(module)) return null; return module; } public static boolean isMavenProperty(PsiElement target) { XmlTag tag = PsiTreeUtil.getParentOfType(target, XmlTag.class, false); if (tag == null) return false; return DomUtil.findDomElement(tag, MavenDomProperties.class) != null; } public static String calcRelativePath(VirtualFile parent, VirtualFile child) { String result = FileUtil.getRelativePath(parent.getPath(), child.getPath(), '/'); if (result == null) { MavenLog.LOG.warn( "cannot calculate relative path for\nparent: " + parent + "\nchild: " + child); result = child.getPath(); } return FileUtil.toSystemIndependentName(result); } public static MavenDomParent updateMavenParent( MavenDomProjectModel mavenModel, MavenProject parentProject) { MavenDomParent result = mavenModel.getMavenParent(); VirtualFile pomFile = DomUtil.getFile(mavenModel).getVirtualFile(); Project project = mavenModel.getXmlElement().getProject(); MavenId parentId = parentProject.getMavenId(); result.getGroupId().setStringValue(parentId.getGroupId()); result.getArtifactId().setStringValue(parentId.getArtifactId()); result.getVersion().setStringValue(parentId.getVersion()); if (!Comparing.equal(pomFile.getParent().getParent(), parentProject.getDirectoryFile())) { result .getRelativePath() .setValue(PsiManager.getInstance(project).findFile(parentProject.getFile())); } return result; } public static <T> T getImmediateParent(ConvertContext context, Class<T> clazz) { DomElement parentElement = context.getInvocationElement().getParent(); return clazz.isInstance(parentElement) ? (T) parentElement : null; } @Nullable public static VirtualFile getVirtualFile(@NotNull DomElement element) { PsiFile psiFile = DomUtil.getFile(element); return getVirtualFile(psiFile); } @Nullable public static VirtualFile getVirtualFile(@NotNull PsiElement element) { PsiFile psiFile = element.getContainingFile(); return getVirtualFile(psiFile); } @Nullable private static VirtualFile getVirtualFile(PsiFile psiFile) { if (psiFile == null) return null; psiFile = psiFile.getOriginalFile(); return psiFile.getVirtualFile(); } @Nullable public static MavenProject findProject(@NotNull MavenDomProjectModel projectDom) { XmlElement element = projectDom.getXmlElement(); if (element == null) return null; VirtualFile file = getVirtualFile(element); if (file == null) return null; MavenProjectsManager manager = MavenProjectsManager.getInstance(element.getProject()); return manager.findProject(file); } @Nullable public static MavenProject findContainingProject(@NotNull DomElement element) { PsiElement psi = element.getXmlElement(); return psi == null ? null : findContainingProject(psi); } @Nullable public static MavenProject findContainingProject(@NotNull PsiElement element) { VirtualFile file = getVirtualFile(element); if (file == null) return null; MavenProjectsManager manager = MavenProjectsManager.getInstance(element.getProject()); return manager.findContainingProject(file); } @Nullable public static MavenDomProjectModel getMavenDomProjectModel( @NotNull Project project, @NotNull VirtualFile file) { return getMavenDomModel(project, file, MavenDomProjectModel.class); } @Nullable public static MavenDomProfiles getMavenDomProfilesModel( @NotNull Project project, @NotNull VirtualFile file) { MavenDomProfilesModel model = getMavenDomModel(project, file, MavenDomProfilesModel.class); if (model != null) return model.getProfiles(); return getMavenDomModel(project, file, MavenDomProfiles.class); // try old-style model } @Nullable public static <T extends MavenDomElement> T getMavenDomModel( @NotNull Project project, @NotNull VirtualFile file, @NotNull Class<T> clazz) { if (!file.isValid()) return null; PsiFile psiFile = PsiManager.getInstance(project).findFile(file); if (psiFile == null) return null; return getMavenDomModel(psiFile, clazz); } @Nullable public static <T extends MavenDomElement> T getMavenDomModel( @NotNull PsiFile file, @NotNull Class<T> clazz) { DomFileElement<T> fileElement = getMavenDomFile(file, clazz); return fileElement == null ? null : fileElement.getRootElement(); } @Nullable private static <T extends MavenDomElement> DomFileElement<T> getMavenDomFile( @NotNull PsiFile file, @NotNull Class<T> clazz) { if (!(file instanceof XmlFile)) return null; return DomManager.getDomManager(file.getProject()).getFileElement((XmlFile) file, clazz); } @Nullable public static XmlTag findTag(@NotNull DomElement domElement, @NotNull String path) { List<String> elements = StringUtil.split(path, "."); if (elements.isEmpty()) return null; Pair<String, Integer> nameAndIndex = translateTagName(elements.get(0)); String name = nameAndIndex.first; Integer index = nameAndIndex.second; XmlTag result = domElement.getXmlTag(); if (result == null || !name.equals(result.getName())) return null; result = getIndexedTag(result, index); for (String each : elements.subList(1, elements.size())) { nameAndIndex = translateTagName(each); name = nameAndIndex.first; index = nameAndIndex.second; result = result.findFirstSubTag(name); if (result == null) return null; result = getIndexedTag(result, index); } return result; } private static final Pattern XML_TAG_NAME_PATTERN = Pattern.compile("(\\S*)\\[(\\d*)\\]\\z"); private static Pair<String, Integer> translateTagName(String text) { String tagName = text.trim(); Integer index = null; Matcher matcher = XML_TAG_NAME_PATTERN.matcher(tagName); if (matcher.find()) { tagName = matcher.group(1); try { index = Integer.parseInt(matcher.group(2)); } catch (NumberFormatException e) { return null; } } return Pair.create(tagName, index); } private static XmlTag getIndexedTag(XmlTag parent, Integer index) { if (index == null) return parent; XmlTag[] children = parent.getSubTags(); if (index < 0 || index >= children.length) return null; return children[index]; } @Nullable public static PropertiesFile getPropertiesFile( @NotNull Project project, @NotNull VirtualFile file) { PsiFile psiFile = PsiManager.getInstance(project).findFile(file); if (!(psiFile instanceof PropertiesFile)) return null; return (PropertiesFile) psiFile; } @Nullable public static IProperty findProperty( @NotNull Project project, @NotNull VirtualFile file, @NotNull String propName) { PropertiesFile propertiesFile = getPropertiesFile(project, file); return propertiesFile == null ? null : propertiesFile.findPropertyByKey(propName); } @Nullable public static PsiElement findPropertyValue( @NotNull Project project, @NotNull VirtualFile file, @NotNull String propName) { IProperty prop = findProperty(project, file, propName); return prop == null ? null : prop.getPsiElement().getFirstChild().getNextSibling().getNextSibling(); } private static Set<VirtualFile> getFilteredResourcesRoots(@NotNull MavenProject mavenProject) { Pair<Long, Set<VirtualFile>> cachedValue = mavenProject.getCachedValue(FILTERED_RESOURCES_ROOTS_KEY); if (cachedValue == null || cachedValue.first != VirtualFileManager.getInstance().getModificationCount()) { Set<VirtualFile> set = null; for (MavenResource resource : ContainerUtil.concat(mavenProject.getResources(), mavenProject.getTestResources())) { if (!resource.isFiltered()) continue; VirtualFile resourceDir = LocalFileSystem.getInstance().findFileByPath(resource.getDirectory()); if (resourceDir == null) continue; if (set == null) { set = new HashSet<VirtualFile>(); } set.add(resourceDir); } if (set == null) { set = Collections.emptySet(); } cachedValue = Pair.create(VirtualFileManager.getInstance().getModificationCount(), set); mavenProject.putCachedValue(FILTERED_RESOURCES_ROOTS_KEY, cachedValue); } return cachedValue.second; } public static boolean isFilteredResourceFile(PsiElement element) { PsiFile psiFile = element.getContainingFile(); VirtualFile file = getVirtualFile(psiFile); if (file == null) return false; MavenProjectsManager manager = MavenProjectsManager.getInstance(psiFile.getProject()); MavenProject mavenProject = manager.findContainingProject(file); if (mavenProject == null) return false; Set<VirtualFile> filteredRoots = getFilteredResourcesRoots(mavenProject); if (!filteredRoots.isEmpty()) { for (VirtualFile f = file.getParent(); f != null; f = f.getParent()) { if (filteredRoots.contains(f)) { return true; } } } return false; } public static List<DomFileElement<MavenDomProjectModel>> collectProjectModels(Project p) { return DomService.getInstance() .getFileElements(MavenDomProjectModel.class, p, GlobalSearchScope.projectScope(p)); } public static MavenId describe(PsiFile psiFile) { MavenDomProjectModel model = getMavenDomModel(psiFile, MavenDomProjectModel.class); String groupId = model.getGroupId().getStringValue(); String artifactId = model.getArtifactId().getStringValue(); String version = model.getVersion().getStringValue(); if (groupId == null) { groupId = model.getMavenParent().getGroupId().getStringValue(); } if (version == null) { version = model.getMavenParent().getVersion().getStringValue(); } return new MavenId(groupId, artifactId, version); } @NotNull public static MavenDomDependency createDomDependency( MavenDomProjectModel model, @Nullable Editor editor, @NotNull final MavenId id) { return createDomDependency(model.getDependencies(), editor, id); } @NotNull public static MavenDomDependency createDomDependency( MavenDomDependencies dependencies, @Nullable Editor editor, @NotNull final MavenId id) { MavenDomDependency dep = createDomDependency(dependencies, editor); dep.getGroupId().setStringValue(id.getGroupId()); dep.getArtifactId().setStringValue(id.getArtifactId()); dep.getVersion().setStringValue(id.getVersion()); return dep; } @NotNull public static MavenDomDependency createDomDependency( @NotNull MavenDomProjectModel model, @Nullable Editor editor) { return createDomDependency(model.getDependencies(), editor); } @NotNull public static MavenDomDependency createDomDependency( @NotNull MavenDomDependencies dependencies, @Nullable Editor editor) { int index = getCollectionIndex(dependencies, editor); if (index >= 0) { DomCollectionChildDescription childDescription = dependencies.getGenericInfo().getCollectionChildDescription("dependency"); if (childDescription != null) { DomElement element = childDescription.addValue(dependencies, index); if (element instanceof MavenDomDependency) { return (MavenDomDependency) element; } } } return dependencies.addDependency(); } public static int getCollectionIndex( @NotNull final MavenDomDependencies dependencies, @Nullable final Editor editor) { if (editor != null) { int offset = editor.getCaretModel().getOffset(); List<MavenDomDependency> dependencyList = dependencies.getDependencies(); for (int i = 0; i < dependencyList.size(); i++) { MavenDomDependency dependency = dependencyList.get(i); XmlElement xmlElement = dependency.getXmlElement(); if (xmlElement != null && xmlElement.getTextRange().getStartOffset() >= offset) { return i; } } } return -1; } }
/** @author peter */ @Deprecated // TODO [VISTALL] rewrit it using ModuleExtensions public abstract class MvcFramework { protected static final ExtensionPointName<MvcFramework> EP_NAME = ExtensionPointName.create("org.intellij.groovy.mvc.framework"); private static final Logger LOG = Logger.getInstance("#org.jetbrains.plugins.groovy.mvc.MvcFramework"); public static final Key<Boolean> CREATE_APP_STRUCTURE = Key.create("CREATE_MVC_APP_STRUCTURE"); public static final Key<Boolean> UPGRADE = Key.create("UPGRADE"); @NonNls public static final String GROOVY_STARTER_CONF = "/conf/groovy-starter.conf"; @NonNls public static final String XMX_JVM_PARAMETER = "-Xmx"; public abstract boolean hasSupport(@NotNull Module module); public boolean isAuxModule(@NotNull Module module) { return isCommonPluginsModule(module) || isGlobalPluginModule(module); } public boolean hasFrameworkJar(@NotNull Module module) { GlobalSearchScope scope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false); return JavaPsiFacade.getInstance(module.getProject()).findClass(getSomeFrameworkClass(), scope) != null; } public boolean isCommonPluginsModule(@NotNull Module module) { return module.getName().endsWith(getCommonPluginSuffix()); } public List<Module> reorderModulesForMvcView(List<Module> modules) { return modules; } public abstract String getApplicationDirectoryName(); public void syncSdkAndLibrariesInPluginsModule(@NotNull Module module) { final Module pluginsModule = findCommonPluginsModule(module); if (pluginsModule != null) { MvcModuleStructureUtil.syncAuxModuleSdk(module, pluginsModule, this); } } public boolean isInteractiveConsoleSupported(@NotNull Module module) { return false; } public void runInteractiveConsole(@NotNull Module module) { throw new UnsupportedOperationException(); } public abstract void upgradeFramework(@NotNull Module module); public void createApplicationIfNeeded(@NotNull final Module module) { if (findAppRoot(module) == null && module.getUserData(CREATE_APP_STRUCTURE) == Boolean.TRUE) { while (ModuleUtilCore.getSdk(module, JavaModuleExtensionImpl.class) == null) { if (Messages.showYesNoDialog( module.getProject(), "Cannot generate " + getDisplayName() + " project structure because JDK is not specified for module \"" + module.getName() + "\".\n" + getDisplayName() + " project will not be created if you don't specify JDK.\nDo you want to specify JDK?", "Error", Messages.getErrorIcon()) == 1) { return; } ProjectSettingsService.getInstance(module.getProject()) .showModuleConfigurationDialog(module.getName(), ClasspathEditor.NAME); } module.putUserData(CREATE_APP_STRUCTURE, null); final GeneralCommandLine commandLine = getCreationCommandLine(module); if (commandLine == null) return; MvcConsole.executeProcess( module, commandLine, new Runnable() { public void run() { VirtualFile root = findAppRoot(module); if (root == null) return; PsiDirectory psiDir = PsiManager.getInstance(module.getProject()).findDirectory(root); IdeView ide = LangDataKeys.IDE_VIEW.getData(DataManager.getInstance().getDataContext()); if (ide != null) ide.selectElement(psiDir); // also here comes fileCreated(application.properties) which manages roots and run // configuration } }, true); } } @Nullable protected GeneralCommandLine getCreationCommandLine(Module module) { String message = "Create default " + getDisplayName() + " directory structure in module '" + module.getName() + "'?"; final int result = Messages.showDialog( module.getProject(), message, "Create " + getDisplayName() + " application", new String[] {"Run 'create-&app'", "Run 'create-&plugin'", "&Cancel"}, 0, getIcon()); if (result < 0 || result > 1) { return null; } return createCommandAndShowErrors( null, module, true, new MvcCommand(result == 0 ? "create-app" : "create-plugin")); } public abstract void updateProjectStructure(@NotNull final Module module); public abstract void ensureRunConfigurationExists(@NotNull Module module); @Nullable public VirtualFile findAppRoot(@Nullable Module module) { if (module == null) return null; String appDirName = getApplicationDirectoryName(); for (VirtualFile root : ModuleRootManager.getInstance(module).getContentRoots()) { if (root.findChild(appDirName) != null) return root; } return null; } @Nullable public VirtualFile findAppRoot(@Nullable PsiElement element) { VirtualFile appDirectory = findAppDirectory(element); return appDirectory == null ? null : appDirectory.getParent(); } @Nullable public VirtualFile findAppDirectory(@Nullable Module module) { if (module == null) return null; String appDirName = getApplicationDirectoryName(); for (VirtualFile root : ModuleRootManager.getInstance(module).getContentRoots()) { VirtualFile res = root.findChild(appDirName); if (res != null) return res; } return null; } @Nullable public VirtualFile findAppDirectory(@Nullable PsiElement element) { if (element == null) return null; PsiFile containingFile = element.getContainingFile().getOriginalFile(); VirtualFile file = containingFile.getVirtualFile(); if (file == null) return null; ProjectFileIndex index = ProjectRootManager.getInstance(containingFile.getProject()).getFileIndex(); VirtualFile root = index.getContentRootForFile(file); if (root == null) return null; return root.findChild(getApplicationDirectoryName()); } @Nullable public abstract VirtualFile getSdkRoot(@Nullable Module module); public abstract String getUserLibraryName(); protected List<File> getImplicitClasspathRoots(@NotNull Module module) { final List<File> toExclude = new ArrayList<File>(); VirtualFile sdkRoot = getSdkRoot(module); if (sdkRoot != null) toExclude.add(VfsUtil.virtualToIoFile(sdkRoot)); ContainerUtil.addIfNotNull(getCommonPluginsDir(module), toExclude); final VirtualFile appRoot = findAppRoot(module); if (appRoot != null) { VirtualFile pluginDir = appRoot.findChild(MvcModuleStructureUtil.PLUGINS_DIRECTORY); if (pluginDir != null) toExclude.add(VfsUtil.virtualToIoFile(pluginDir)); VirtualFile libDir = appRoot.findChild("lib"); if (libDir != null) toExclude.add(VfsUtil.virtualToIoFile(libDir)); } final Library library = MvcModuleStructureUtil.findUserLibrary(module, getUserLibraryName()); if (library != null) { for (VirtualFile file : library.getFiles(OrderRootType.CLASSES)) { toExclude.add(VfsUtil.virtualToIoFile(PathUtil.getLocalFile(file))); } } return toExclude; } private PathsList removeFrameworkStuff(Module module, List<VirtualFile> rootFiles) { final List<File> toExclude = getImplicitClasspathRoots(module); if (LOG.isDebugEnabled()) { LOG.debug("Before removing framework stuff: " + rootFiles); LOG.debug("Implicit roots:" + toExclude); } PathsList scriptClassPath = new PathsList(); eachRoot: for (VirtualFile file : rootFiles) { for (final File excluded : toExclude) { if (VfsUtil.isAncestor(excluded, VfsUtil.virtualToIoFile(file), false)) { continue eachRoot; } } scriptClassPath.add(file); } return scriptClassPath; } public PathsList getApplicationClassPath(Module module) { final List<VirtualFile> classPath = OrderEnumerator.orderEntries(module) .recursively() .withoutSdk() .getPathsList() .getVirtualFiles(); retainOnlyJarsAndDirectories(classPath); removeModuleOutput(module, classPath); final Module pluginsModule = findCommonPluginsModule(module); if (pluginsModule != null) { removeModuleOutput(pluginsModule, classPath); } return removeFrameworkStuff(module, classPath); } public abstract boolean updatesWholeProject(); private static void retainOnlyJarsAndDirectories(List<VirtualFile> woSdk) { for (Iterator<VirtualFile> iterator = woSdk.iterator(); iterator.hasNext(); ) { VirtualFile file = iterator.next(); final VirtualFile local = ArchiveVfsUtil.getVirtualFileForJar(file); final boolean dir = file.isDirectory(); final String name = file.getName(); if (LOG.isDebugEnabled()) { LOG.debug( "Considering: " + file.getPath() + "; local=" + local + "; dir=" + dir + "; name=" + name); } if (dir || local != null) { continue; } if (name.endsWith(".jar")) { continue; } LOG.debug("Removing"); iterator.remove(); } } private static void removeModuleOutput(Module module, List<VirtualFile> from) { CompilerPathsManager compilerPathsManager = CompilerPathsManager.getInstance(module.getProject()); for (ContentFolderType contentFolderType : ContentFolderType.ALL_SOURCE_ROOTS) { from.remove(compilerPathsManager.getCompilerOutput(module, contentFolderType)); } } public abstract JavaParameters createJavaParameters( @NotNull Module module, boolean forCreation, boolean forTests, boolean classpathFromDependencies, @Nullable String jvmParams, @NotNull MvcCommand command) throws ExecutionException; protected static void ensureRunConfigurationExists( Module module, ConfigurationType configurationType, String name) { final RunManagerEx runManager = RunManagerEx.getInstanceEx(module.getProject()); for (final RunConfiguration runConfiguration : runManager.getConfigurations(configurationType)) { if (runConfiguration instanceof MvcRunConfiguration && ((MvcRunConfiguration) runConfiguration).getModule() == module) { return; } } final ConfigurationFactory factory = configurationType.getConfigurationFactories()[0]; final RunnerAndConfigurationSettings runSettings = runManager.createRunConfiguration(name, factory); final MvcRunConfiguration configuration = (MvcRunConfiguration) runSettings.getConfiguration(); configuration.setModule(module); runManager.addConfiguration(runSettings, false); runManager.setActiveConfiguration(runSettings); RunManagerEx.disableTasks( module.getProject(), configuration, CompileStepBeforeRun.ID, CompileStepBeforeRunNoErrorCheck.ID); } public abstract String getFrameworkName(); public String getDisplayName() { return getFrameworkName(); } public abstract Icon getIcon(); // 16*16 public abstract Icon getToolWindowIcon(); // 13*13 public abstract String getSdkHomePropertyName(); @Nullable public GeneralCommandLine createCommandAndShowErrors( @NotNull Module module, @NotNull String command, String... args) { return createCommandAndShowErrors(null, module, new MvcCommand(command, args)); } @Nullable public GeneralCommandLine createCommandAndShowErrors( @NotNull Module module, @NotNull MvcCommand command) { return createCommandAndShowErrors(null, module, command); } @Nullable public GeneralCommandLine createCommandAndShowErrors( @Nullable String vmOptions, @NotNull Module module, @NotNull MvcCommand command) { return createCommandAndShowErrors(vmOptions, module, false, command); } @Nullable public GeneralCommandLine createCommandAndShowErrors( @Nullable String vmOptions, @NotNull Module module, final boolean forCreation, @NotNull MvcCommand command) { try { return createCommand(module, vmOptions, forCreation, command); } catch (ExecutionException e) { Messages.showErrorDialog(e.getMessage(), "Failed to run grails command: " + command); return null; } } @NotNull public GeneralCommandLine createCommand( @NotNull Module module, @Nullable String jvmParams, boolean forCreation, @NotNull MvcCommand command) throws ExecutionException { final JavaParameters params = createJavaParameters(module, forCreation, false, true, jvmParams, command); addJavaHome(params, module); final GeneralCommandLine commandLine = createCommandLine(params); final VirtualFile griffonHome = getSdkRoot(module); if (griffonHome != null) { commandLine .getEnvironment() .put(getSdkHomePropertyName(), FileUtil.toSystemDependentName(griffonHome.getPath())); } final VirtualFile root = findAppRoot(module); final File ioRoot = root != null ? VfsUtilCore.virtualToIoFile(root) : new File(module.getModuleDirPath()); commandLine.setWorkDirectory(forCreation ? ioRoot.getParentFile() : ioRoot); return commandLine; } public static void addJavaHome(@NotNull JavaParameters params, @NotNull Module module) { final Sdk sdk = ModuleUtilCore.getSdk(module, JavaModuleExtensionImpl.class); if (sdk != null && sdk.getSdkType() instanceof JavaSdkType) { String path = StringUtil.trimEnd(sdk.getHomePath(), File.separator); if (StringUtil.isNotEmpty(path)) { Map<String, String> env = params.getEnv(); if (env == null) { env = new HashMap<String, String>(); params.setEnv(env); } env.put("JAVA_HOME", FileUtil.toSystemDependentName(path)); } } } public static GeneralCommandLine createCommandLine(@NotNull JavaParameters params) throws CantRunException { return CommandLineBuilder.createFromJavaParameters(params); } private void extractPlugins( Project project, @Nullable VirtualFile pluginRoot, Map<String, VirtualFile> res) { if (pluginRoot != null) { VirtualFile[] children = pluginRoot.getChildren(); if (children != null) { for (VirtualFile child : children) { String pluginName = getInstalledPluginNameByPath(project, child); if (pluginName != null) { res.put(pluginName, child); } } } } } public Collection<VirtualFile> getAllPluginRoots(@NotNull Module module, boolean refresh) { return getCommonPluginRoots(module, refresh); } public void collectCommonPluginRoots( Map<String, VirtualFile> result, @NotNull Module module, boolean refresh) { if (isCommonPluginsModule(module)) { for (VirtualFile root : ModuleRootManager.getInstance(module).getContentRoots()) { String pluginName = getInstalledPluginNameByPath(module.getProject(), root); if (pluginName != null) { result.put(pluginName, root); } } } else { VirtualFile root = findAppRoot(module); if (root == null) return; extractPlugins( module.getProject(), root.findChild(MvcModuleStructureUtil.PLUGINS_DIRECTORY), result); extractPlugins( module.getProject(), MvcModuleStructureUtil.findFile(getCommonPluginsDir(module), refresh), result); extractPlugins( module.getProject(), MvcModuleStructureUtil.findFile(getGlobalPluginsDir(module), refresh), result); } } public Collection<VirtualFile> getCommonPluginRoots(@NotNull Module module, boolean refresh) { Map<String, VirtualFile> result = new HashMap<String, VirtualFile>(); collectCommonPluginRoots(result, module, refresh); return result.values(); } @Nullable public Module findCommonPluginsModule(@NotNull Module module) { return ModuleManager.getInstance(module.getProject()) .findModuleByName(getCommonPluginsModuleName(module)); } public boolean isGlobalPluginModule(@NotNull Module module) { return module.getName().startsWith(getGlobalPluginsModuleName()); } @Nullable public File getSdkWorkDir(@NotNull Module module) { return getDefaultSdkWorkDir(module); } @Nullable public abstract File getDefaultSdkWorkDir(@NotNull Module module); @Nullable public File getGlobalPluginsDir(@NotNull Module module) { final File sdkWorkDir = getSdkWorkDir(module); return sdkWorkDir == null ? null : new File(sdkWorkDir, "global-plugins"); } @Nullable public File getCommonPluginsDir(@NotNull Module module) { final File grailsWorkDir = getSdkWorkDir(module); if (grailsWorkDir == null) return null; final String applicationName = getApplicationName(module); if (applicationName == null) return null; return new File(grailsWorkDir, "projects/" + applicationName + "/plugins"); } public String getApplicationName(Module module) { final VirtualFile root = findAppRoot(module); if (root == null) return null; return root.getName(); } protected abstract String getCommonPluginSuffix(); public abstract String getGlobalPluginsModuleName(); public String getCommonPluginsModuleName(Module module) { return module.getName() + getCommonPluginSuffix(); } public abstract boolean isSDKLibrary(Library library); public abstract MvcProjectStructure createProjectStructure( @NotNull Module module, boolean auxModule); public abstract LibraryKind getLibraryKind(); public abstract String getSomeFrameworkClass(); public static void addAvailableSystemScripts( final Collection<String> result, @NotNull Module module) { VirtualFile scriptRoot = null; GlobalSearchScope searchScope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false); for (PsiClass aClass : JavaPsiFacade.getInstance(module.getProject()).findClasses("CreateApp_", searchScope)) { PsiClass superClass = aClass.getSuperClass(); if (superClass != null && GroovyCommonClassNames.GROOVY_LANG_SCRIPT.equals(superClass.getQualifiedName())) { PsiFile psiFile = aClass.getContainingFile(); if (psiFile != null) { VirtualFile file = psiFile.getVirtualFile(); if (file != null && file.getFileSystem() instanceof ArchiveFileSystem) { VirtualFile parent = file.getParent(); if (parent != null && parent.findChild("Console.class") != null) { scriptRoot = parent; break; } } } } } if (scriptRoot == null) return; Pattern scriptPattern = Pattern.compile("([A-Za-z0-9]+)_?\\.class"); for (VirtualFile file : scriptRoot.getChildren()) { Matcher matcher = scriptPattern.matcher(file.getName()); if (matcher.matches()) { result.add(GroovyNamesUtil.camelToSnake(matcher.group(1))); } } } public abstract boolean isToReformatOnCreation(VirtualFile file); public static void addAvailableScripts( final Collection<String> result, @Nullable final VirtualFile root) { if (root == null || !root.isDirectory()) { return; } final VirtualFile scripts = root.findChild("scripts"); if (scripts == null || !scripts.isDirectory()) { return; } for (VirtualFile child : scripts.getChildren()) { if (isScriptFile(child)) { result.add(GroovyNamesUtil.camelToSnake(child.getNameWithoutExtension())); } } } @Nullable public static MvcFramework findCommonPluginModuleFramework(Module module) { for (MvcFramework framework : EP_NAME.getExtensions()) { if (framework.isCommonPluginsModule(module)) { return framework; } } return null; } public static boolean isScriptFileName(String fileName) { return fileName.endsWith(GroovyFileType.DEFAULT_EXTENSION) && fileName.charAt(0) != '_'; } private static boolean isScriptFile(VirtualFile virtualFile) { return !virtualFile.isDirectory() && isScriptFileName(virtualFile.getName()); } @Nullable public String getInstalledPluginNameByPath(Project project, @NotNull VirtualFile pluginPath) { VirtualFile pluginXml = pluginPath.findChild("plugin.xml"); if (pluginXml == null) return null; PsiFile pluginXmlPsi = PsiManager.getInstance(project).findFile(pluginXml); if (!(pluginXmlPsi instanceof XmlFile)) return null; XmlTag rootTag = ((XmlFile) pluginXmlPsi).getRootTag(); if (rootTag == null || !"plugin".equals(rootTag.getName())) return null; XmlAttribute attrName = rootTag.getAttribute("name"); if (attrName == null) return null; String res = attrName.getValue(); if (res == null) return null; res = res.trim(); if (res.length() == 0) return null; return res; } @Nullable public static MvcFramework getInstance(@Nullable final Module module) { if (module == null) { return null; } final Project project = module.getProject(); return CachedValuesManager.getManager(project) .getCachedValue( module, new CachedValueProvider<MvcFramework>() { @Override public Result<MvcFramework> compute() { final ModificationTracker tracker = MvcModuleStructureSynchronizer.getInstance(project) .getFileAndRootsModificationTracker(); for (final MvcFramework framework : EP_NAME.getExtensions()) { if (framework.hasSupport(module)) { return Result.create(framework, tracker); } } return Result.create(null, tracker); } }); } @Nullable public static MvcFramework getInstanceBySdk(@NotNull Module module) { for (final MvcFramework framework : EP_NAME.getExtensions()) { if (framework.getSdkRoot(module) != null) { return framework; } } return null; } }
public final class LoadTextUtil { @Nls private static final String AUTO_DETECTED_FROM_BOM = "auto-detected from BOM"; private LoadTextUtil() {} @NotNull private static Pair<CharSequence, String> convertLineSeparators(@NotNull CharBuffer buffer) { int dst = 0; char prev = ' '; int crCount = 0; int lfCount = 0; int crlfCount = 0; final int length = buffer.length(); final char[] bufferArray = CharArrayUtil.fromSequenceWithoutCopying(buffer); for (int src = 0; src < length; src++) { char c = bufferArray != null ? bufferArray[src] : buffer.charAt(src); switch (c) { case '\r': if (bufferArray != null) bufferArray[dst++] = '\n'; else buffer.put(dst++, '\n'); crCount++; break; case '\n': if (prev == '\r') { crCount--; crlfCount++; } else { if (bufferArray != null) bufferArray[dst++] = '\n'; else buffer.put(dst++, '\n'); lfCount++; } break; default: if (bufferArray != null) bufferArray[dst++] = c; else buffer.put(dst++, c); break; } prev = c; } String detectedLineSeparator = null; if (crlfCount > crCount && crlfCount > lfCount) { detectedLineSeparator = "\r\n"; } else if (crCount > lfCount) { detectedLineSeparator = "\r"; } else if (lfCount > 0) { detectedLineSeparator = "\n"; } CharSequence result; if (buffer.length() == dst) { result = buffer; } else { // in Mac JDK CharBuffer.subSequence() signature differs from Oracle's // more than that, the signature has changed between jd6 and jdk7, // so use more generic CharSequence.subSequence() just in case @SuppressWarnings("UnnecessaryLocalVariable") CharSequence seq = buffer; result = seq.subSequence(0, dst); } return Pair.create(result, detectedLineSeparator); } @NotNull public static Charset detectCharset( @NotNull VirtualFile virtualFile, @NotNull byte[] content, @NotNull FileType fileType) { Charset charset = null; Trinity<Charset, CharsetToolkit.GuessedEncoding, byte[]> guessed = guessFromContent(virtualFile, content, content.length); if (guessed != null && guessed.first != null) { charset = guessed.first; } else { String charsetName = fileType.getCharset(virtualFile, content); if (charsetName == null) { Charset specifiedExplicitly = EncodingRegistry.getInstance().getEncoding(virtualFile, true); if (specifiedExplicitly != null) { charset = specifiedExplicitly; } } else { charset = CharsetToolkit.forName(charsetName); } } if (charset == null) { charset = EncodingRegistry.getInstance().getDefaultCharset(); } if (fileType.getName().equals("Properties") && EncodingRegistry.getInstance().isNative2Ascii(virtualFile)) { charset = Native2AsciiCharset.wrap(charset); } virtualFile.setCharset(charset); return charset; } @NotNull public static Charset detectCharsetAndSetBOM( @NotNull VirtualFile virtualFile, @NotNull byte[] content) { return doDetectCharsetAndSetBOM(virtualFile, content, true).getFirst(); } @NotNull private static Pair.NonNull<Charset, byte[]> doDetectCharsetAndSetBOM( @NotNull VirtualFile virtualFile, @NotNull byte[] content, boolean saveBOM) { return doDetectCharsetAndSetBOM(virtualFile, content, saveBOM, virtualFile.getFileType()); } @NotNull private static Pair.NonNull<Charset, byte[]> doDetectCharsetAndSetBOM( @NotNull VirtualFile virtualFile, @NotNull byte[] content, boolean saveBOM, @NotNull FileType fileType) { @NotNull Charset charset = virtualFile.isCharsetSet() ? virtualFile.getCharset() : detectCharset(virtualFile, content, fileType); Pair.NonNull<Charset, byte[]> bomAndCharset = getCharsetAndBOM(content, charset); final byte[] bom = bomAndCharset.second; if (saveBOM && bom.length != 0) { virtualFile.setBOM(bom); setCharsetWasDetectedFromBytes(virtualFile, AUTO_DETECTED_FROM_BOM); } return bomAndCharset; } private static final boolean GUESS_UTF = Boolean.parseBoolean(System.getProperty("idea.guess.utf.encoding", "true")); @Nullable( "null means no luck, otherwise it's tuple(guessed encoding, hint about content if was unable to guess, BOM)") public static Trinity<Charset, CharsetToolkit.GuessedEncoding, byte[]> guessFromContent( @NotNull VirtualFile virtualFile, @NotNull byte[] content, int length) { Charset defaultCharset = ObjectUtils.notNull( EncodingManager.getInstance().getEncoding(virtualFile, true), CharsetToolkit.getDefaultSystemCharset()); CharsetToolkit toolkit = GUESS_UTF ? new CharsetToolkit(content, defaultCharset) : null; String detectedFromBytes = null; try { if (GUESS_UTF) { toolkit.setEnforce8Bit(true); Charset charset = toolkit.guessFromBOM(); if (charset != null) { detectedFromBytes = AUTO_DETECTED_FROM_BOM; byte[] bom = ObjectUtils.notNull(CharsetToolkit.getMandatoryBom(charset), CharsetToolkit.UTF8_BOM); return Trinity.create(charset, null, bom); } CharsetToolkit.GuessedEncoding guessed = toolkit.guessFromContent(length); if (guessed == CharsetToolkit.GuessedEncoding.VALID_UTF8) { detectedFromBytes = "auto-detected from bytes"; return Trinity.create( CharsetToolkit.UTF8_CHARSET, guessed, null); // UTF detected, ignore all directives } if (guessed == CharsetToolkit.GuessedEncoding.SEVEN_BIT) { return Trinity.create(null, guessed, null); } } return null; } finally { setCharsetWasDetectedFromBytes(virtualFile, detectedFromBytes); } } @NotNull private static Pair.NonNull<Charset, byte[]> getCharsetAndBOM( @NotNull byte[] content, @NotNull Charset charset) { if (charset.name().contains(CharsetToolkit.UTF8) && CharsetToolkit.hasUTF8Bom(content)) { return Pair.createNonNull(charset, CharsetToolkit.UTF8_BOM); } try { Charset fromBOM = CharsetToolkit.guessFromBOM(content); if (fromBOM != null) { return Pair.createNonNull( fromBOM, ObjectUtils.notNull( CharsetToolkit.getMandatoryBom(fromBOM), ArrayUtil.EMPTY_BYTE_ARRAY)); } } catch (UnsupportedCharsetException ignore) { } return Pair.createNonNull(charset, ArrayUtil.EMPTY_BYTE_ARRAY); } public static void changeLineSeparators( @Nullable Project project, @NotNull VirtualFile file, @NotNull String newSeparator, @NotNull Object requestor) throws IOException { CharSequence currentText = getTextByBinaryPresentation(file.contentsToByteArray(), file, true, false); String currentSeparator = detectLineSeparator(file, false); if (newSeparator.equals(currentSeparator)) { return; } String newText = StringUtil.convertLineSeparators(currentText.toString(), newSeparator); file.setDetectedLineSeparator(newSeparator); write(project, file, requestor, newText, -1); } /** * Overwrites file with text and sets modification stamp and time stamp to the specified values. * * <p>Normally you should not use this method. * * @param requestor any object to control who called this method. Note that it is considered to be * an external change if <code>requestor</code> is <code>null</code>. See {@link * com.intellij.openapi.vfs.VirtualFileEvent#getRequestor} * @param newModificationStamp new modification stamp or -1 if no special value should be * set @return <code>Writer</code> * @throws java.io.IOException if an I/O error occurs * @see VirtualFile#getModificationStamp() */ public static void write( @Nullable Project project, @NotNull VirtualFile virtualFile, @NotNull Object requestor, @NotNull String text, long newModificationStamp) throws IOException { Charset existing = virtualFile.getCharset(); Pair.NonNull<Charset, byte[]> chosen = charsetForWriting(project, virtualFile, text, existing); Charset charset = chosen.first; byte[] buffer = chosen.second; if (!charset.equals(existing)) { virtualFile.setCharset(charset); } setDetectedFromBytesFlagBack(virtualFile, buffer); OutputStream outputStream = virtualFile.getOutputStream(requestor, newModificationStamp, -1); try { outputStream.write(buffer); } finally { outputStream.close(); } } @NotNull private static Pair.NonNull<Charset, byte[]> charsetForWriting( @Nullable Project project, @NotNull VirtualFile virtualFile, @NotNull String text, @NotNull Charset existing) { Charset specified = extractCharsetFromFileContent(project, virtualFile, text); Pair.NonNull<Charset, byte[]> chosen = chooseMostlyHarmlessCharset(existing, specified, text); Charset charset = chosen.first; // in case of "UTF-16", OutputStreamWriter sometimes adds BOM on it's own. // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6800103 byte[] bom = virtualFile.getBOM(); Charset fromBom = bom == null ? null : CharsetToolkit.guessFromBOM(bom); if (fromBom != null && !fromBom.equals(charset)) { chosen = Pair.createNonNull(fromBom, toBytes(text, fromBom)); } return chosen; } public static void setDetectedFromBytesFlagBack( @NotNull VirtualFile virtualFile, @NotNull byte[] content) { if (virtualFile.getBOM() == null) { guessFromContent(virtualFile, content, content.length); } else { // prevent file to be reloaded in other encoding after save with BOM setCharsetWasDetectedFromBytes(virtualFile, AUTO_DETECTED_FROM_BOM); } } @NotNull public static Pair.NonNull<Charset, byte[]> chooseMostlyHarmlessCharset( @NotNull Charset existing, @NotNull Charset specified, @NotNull String text) { try { if (specified.equals(existing)) { return Pair.createNonNull(specified, toBytes(text, existing)); } byte[] out = isSupported(specified, text); if (out != null) { return Pair.createNonNull( specified, out); // if explicitly specified encoding is safe, return it } out = isSupported(existing, text); if (out != null) { return Pair.createNonNull(existing, out); // otherwise stick to the old encoding if it's ok } return Pair.createNonNull( specified, toBytes(text, specified)); // if both are bad there is no difference } catch (RuntimeException e) { return Pair.createNonNull( Charset.defaultCharset(), toBytes(text, null)); // if both are bad and there is no hope, use the default charset } } @NotNull private static byte[] toBytes(@NotNull String text, @Nullable Charset charset) throws RuntimeException { //noinspection SSBasedInspection return charset == null ? text.getBytes() : text.getBytes(charset); } @Nullable("null means not supported, otherwise it is converted byte stream") private static byte[] isSupported(@NotNull Charset charset, @NotNull String str) { try { if (!charset.canEncode()) return null; byte[] bytes = str.getBytes(charset); if (!str.equals(new String(bytes, charset))) { return null; } return bytes; } catch (Exception e) { return null; // wow, some charsets throw NPE inside .getBytes() when unable to encode // (JIS_X0212-1990) } } @NotNull public static Charset extractCharsetFromFileContent( @Nullable Project project, @NotNull VirtualFile virtualFile, @NotNull CharSequence text) { return ObjectUtils.notNull( charsetFromContentOrNull(project, virtualFile, text), virtualFile.getCharset()); } @Nullable("returns null if cannot determine from content") public static Charset charsetFromContentOrNull( @Nullable Project project, @NotNull VirtualFile virtualFile, @NotNull CharSequence text) { return CharsetUtil.extractCharsetFromFileContent( project, virtualFile, virtualFile.getFileType(), text); } private static boolean ourDecompileProgressStarted = false; @NotNull public static CharSequence loadText(@NotNull final VirtualFile file) { if (file instanceof LightVirtualFile) { return ((LightVirtualFile) file).getContent(); } if (file.isDirectory()) { throw new AssertionError("'" + file.getPresentableUrl() + "' is a directory"); } FileType fileType = file.getFileType(); if (fileType.isBinary()) { final BinaryFileDecompiler decompiler = BinaryFileTypeDecompilers.INSTANCE.forFileType(fileType); if (decompiler != null) { CharSequence text; Application app = ApplicationManager.getApplication(); if (app != null && app.isDispatchThread() && !app.isWriteAccessAllowed() && !ourDecompileProgressStarted) { final Ref<CharSequence> result = Ref.create(ArrayUtil.EMPTY_CHAR_SEQUENCE); final Ref<Throwable> error = Ref.create(); ourDecompileProgressStarted = true; try { ProgressManager.getInstance() .run( new Task.Modal(null, "Decompiling " + file.getName(), true) { @Override public void run(@NotNull ProgressIndicator indicator) { indicator.setIndeterminate(true); try { result.set( ApplicationUtil.runWithCheckCanceled( new Callable<CharSequence>() { @Override public CharSequence call() { return decompiler.decompile(file); } }, indicator)); } catch (Throwable t) { error.set(t); } } }); } finally { ourDecompileProgressStarted = false; } ExceptionUtil.rethrowUnchecked(error.get()); text = result.get(); } else { text = decompiler.decompile(file); } StringUtil.assertValidSeparators(text); return text; } throw new IllegalArgumentException( "Attempt to load text for binary file which doesn't have a decompiler plugged in: " + file.getPresentableUrl() + ". File type: " + fileType.getName()); } try { byte[] bytes = file.contentsToByteArray(); return getTextByBinaryPresentation(bytes, file); } catch (IOException e) { return ArrayUtil.EMPTY_CHAR_SEQUENCE; } } @NotNull public static CharSequence getTextByBinaryPresentation( @NotNull final byte[] bytes, @NotNull VirtualFile virtualFile) { return getTextByBinaryPresentation(bytes, virtualFile, true, true); } @NotNull public static CharSequence getTextByBinaryPresentation( @NotNull byte[] bytes, @NotNull VirtualFile virtualFile, boolean saveDetectedSeparators, boolean saveBOM) { return getTextByBinaryPresentation( bytes, virtualFile, saveDetectedSeparators, saveBOM, virtualFile.getFileType()); } @NotNull public static CharSequence getTextByBinaryPresentation( @NotNull byte[] bytes, @NotNull VirtualFile virtualFile, boolean saveDetectedSeparators, boolean saveBOM, @NotNull FileType fileType) { Pair.NonNull<Charset, byte[]> pair = doDetectCharsetAndSetBOM(virtualFile, bytes, saveBOM, fileType); Charset charset = pair.getFirst(); byte[] bom = pair.getSecond(); int offset = bom.length; Pair<CharSequence, String> result = convertBytes(bytes, charset, offset); if (saveDetectedSeparators) { virtualFile.setDetectedLineSeparator(result.getSecond()); } return result.getFirst(); } /** * Get detected line separator, if the file never been loaded, is loaded if checkFile parameter is * specified. * * @param file the file to check * @param checkFile if the line separator was not detected before, try to detect it * @return the detected line separator or null */ @Nullable public static String detectLineSeparator(@NotNull VirtualFile file, boolean checkFile) { String lineSeparator = getDetectedLineSeparator(file); if (lineSeparator == null && checkFile) { try { getTextByBinaryPresentation(file.contentsToByteArray(), file); lineSeparator = getDetectedLineSeparator(file); } catch (IOException e) { // null will be returned } } return lineSeparator; } static String getDetectedLineSeparator(@NotNull VirtualFile file) { return file.getDetectedLineSeparator(); } @NotNull public static CharSequence getTextByBinaryPresentation( @NotNull byte[] bytes, @NotNull Charset charset) { Pair.NonNull<Charset, byte[]> pair = getCharsetAndBOM(bytes, charset); byte[] bom = pair.getSecond(); int offset = bom.length; final Pair<CharSequence, String> result = convertBytes(bytes, pair.first, offset); return result.getFirst(); } // do not need to think about BOM here. it is processed outside @NotNull private static Pair<CharSequence, String> convertBytes( @NotNull byte[] bytes, @NotNull Charset charset, final int startOffset) { ByteBuffer byteBuffer = ByteBuffer.wrap(bytes, startOffset, bytes.length - startOffset); CharBuffer charBuffer; try { charBuffer = charset.decode(byteBuffer); } catch (Exception e) { // esoteric charsets can throw any kind of exception charBuffer = CharBuffer.wrap(ArrayUtil.EMPTY_CHAR_ARRAY); } return convertLineSeparators(charBuffer); } private static final Key<String> CHARSET_WAS_DETECTED_FROM_BYTES = Key.create("CHARSET_WAS_DETECTED_FROM_BYTES"); @Nullable("null if was not detected, otherwise the reason it was") public static String wasCharsetDetectedFromBytes(@NotNull VirtualFile virtualFile) { return virtualFile.getUserData(CHARSET_WAS_DETECTED_FROM_BYTES); } public static void setCharsetWasDetectedFromBytes( @NotNull VirtualFile virtualFile, @Nullable("null if was not detected, otherwise the reason it was") String reason) { virtualFile.putUserData(CHARSET_WAS_DETECTED_FROM_BYTES, reason); } }
public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingPass implements DumbAware { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.GeneralHighlightingPass"); static final String PRESENTABLE_NAME = DaemonBundle.message("pass.syntax"); private static final Key<Boolean> HAS_ERROR_ELEMENT = Key.create("HAS_ERROR_ELEMENT"); private final int myStartOffset; private final int myEndOffset; private final boolean myUpdateAll; private final ProperTextRange myPriorityRange; private final Editor myEditor; private final List<HighlightInfo> myHighlights = new ArrayList<HighlightInfo>(); protected volatile boolean myHasErrorElement; private volatile boolean myErrorFound; private static final Comparator<HighlightVisitor> VISITOR_ORDER_COMPARATOR = new Comparator<HighlightVisitor>() { @Override public int compare(final HighlightVisitor o1, final HighlightVisitor o2) { return o1.order() - o2.order(); } }; private Runnable myApplyCommand; private final EditorColorsScheme myGlobalScheme; private boolean myFailFastOnAcquireReadAction = true; public GeneralHighlightingPass( @NotNull Project project, @NotNull PsiFile file, @NotNull Document document, int startOffset, int endOffset, boolean updateAll) { this( project, file, document, startOffset, endOffset, updateAll, new ProperTextRange(0, document.getTextLength()), null); } public GeneralHighlightingPass( @NotNull Project project, @NotNull PsiFile file, @NotNull Document document, int startOffset, int endOffset, boolean updateAll, @NotNull ProperTextRange priorityRange, @Nullable Editor editor) { super(project, document, PRESENTABLE_NAME, file, true); myStartOffset = startOffset; myEndOffset = endOffset; myUpdateAll = updateAll; myPriorityRange = priorityRange; myEditor = editor; LOG.assertTrue(file.isValid()); setId(Pass.UPDATE_ALL); myHasErrorElement = !isWholeFileHighlighting() && Boolean.TRUE.equals(myFile.getUserData(HAS_ERROR_ELEMENT)); FileStatusMap fileStatusMap = ((DaemonCodeAnalyzerImpl) DaemonCodeAnalyzer.getInstance(myProject)).getFileStatusMap(); myErrorFound = !isWholeFileHighlighting() && fileStatusMap.wasErrorFound(myDocument); myApplyCommand = new Runnable() { @Override public void run() { ProperTextRange range = new ProperTextRange(myStartOffset, myEndOffset); MarkupModel model = DocumentMarkupModel.forDocument(myDocument, myProject, true); UpdateHighlightersUtil.cleanFileLevelHighlights(myProject, Pass.UPDATE_ALL, myFile); final EditorColorsScheme colorsScheme = getColorsScheme(); UpdateHighlightersUtil.setHighlightersInRange( myProject, myDocument, range, colorsScheme, myHighlights, (MarkupModelEx) model, Pass.UPDATE_ALL); } }; // initial guess to show correct progress in the traffic light icon setProgressLimit(document.getTextLength() / 2); // approx number of PSI elements = file length/2 myGlobalScheme = EditorColorsManager.getInstance().getGlobalScheme(); } private static final Key<AtomicInteger> HIGHLIGHT_VISITOR_INSTANCE_COUNT = new Key<AtomicInteger>("HIGHLIGHT_VISITOR_INSTANCE_COUNT"); @NotNull private HighlightVisitor[] getHighlightVisitors() { int oldCount = incVisitorUsageCount(1); HighlightVisitor[] highlightVisitors = createHighlightVisitors(); if (oldCount != 0) { HighlightVisitor[] clones = new HighlightVisitor[highlightVisitors.length]; for (int i = 0; i < highlightVisitors.length; i++) { HighlightVisitor highlightVisitor = highlightVisitors[i]; clones[i] = highlightVisitor.clone(); } highlightVisitors = clones; } return highlightVisitors; } protected HighlightVisitor[] createHighlightVisitors() { return Extensions.getExtensions(HighlightVisitor.EP_HIGHLIGHT_VISITOR, myProject); } // returns old value private int incVisitorUsageCount(int delta) { AtomicInteger count = myProject.getUserData(HIGHLIGHT_VISITOR_INSTANCE_COUNT); if (count == null) { count = ((UserDataHolderEx) myProject) .putUserDataIfAbsent(HIGHLIGHT_VISITOR_INSTANCE_COUNT, new AtomicInteger(0)); } int old = count.getAndAdd(delta); assert old + delta >= 0 : old + ";" + delta; return old; } @Override protected void collectInformationWithProgress(final ProgressIndicator progress) { final Set<HighlightInfo> gotHighlights = new THashSet<HighlightInfo>(100); final Set<HighlightInfo> outsideResult = new THashSet<HighlightInfo>(100); DaemonCodeAnalyzer daemonCodeAnalyzer = DaemonCodeAnalyzer.getInstance(myProject); HighlightVisitor[] highlightVisitors = getHighlightVisitors(); final HighlightVisitor[] filteredVisitors = filterVisitors(highlightVisitors, myFile); final List<PsiElement> inside = new ArrayList<PsiElement>(); final List<PsiElement> outside = new ArrayList<PsiElement>(); try { Divider.divideInsideAndOutside( myFile, myStartOffset, myEndOffset, myPriorityRange, inside, outside, HighlightLevelUtil.AnalysisLevel.HIGHLIGHT, false); setProgressLimit((long) (inside.size() + outside.size())); final boolean forceHighlightParents = forceHighlightParents(); if (!isDumbMode()) { highlightTodos( myFile, myDocument.getCharsSequence(), myStartOffset, myEndOffset, progress, myPriorityRange, gotHighlights, outsideResult); } collectHighlights( inside, new Runnable() { @Override public void run() { // all infos for the "injected fragment for the host which is inside" are indeed // inside // but some of the infos for the "injected fragment for the host which is outside" can // be still inside Set<HighlightInfo> injectedResult = new THashSet<HighlightInfo>(); final Set<PsiFile> injected = new THashSet<PsiFile>(); getInjectedPsiFiles(inside, outside, progress, injected); if (!addInjectedPsiHighlights( injected, progress, Collections.synchronizedSet(injectedResult))) throw new ProcessCanceledException(); final List<HighlightInfo> injectionsOutside = new ArrayList<HighlightInfo>(gotHighlights.size()); Set<HighlightInfo> result; synchronized (injectedResult) { // sync here because all writes happened in another thread result = injectedResult; } for (HighlightInfo info : result) { if (myPriorityRange.containsRange(info.getStartOffset(), info.getEndOffset())) { gotHighlights.add(info); } else { // nonconditionally apply injected results regardless whether they are in // myStartOffset,myEndOffset injectionsOutside.add(info); } } if (outsideResult.isEmpty() && injectionsOutside.isEmpty()) { return; // apply only result (by default apply command) and only within inside } final ProperTextRange priorityIntersection = myPriorityRange.intersection(new TextRange(myStartOffset, myEndOffset)); if ((!inside.isEmpty() || !gotHighlights.isEmpty()) && priorityIntersection != null) { // do not apply when there were no elements to highlight // clear infos found in visible area to avoid applying them twice final List<HighlightInfo> toApplyInside = new ArrayList<HighlightInfo>(gotHighlights); myHighlights.addAll(toApplyInside); gotHighlights.clear(); gotHighlights.addAll(outsideResult); final long modificationStamp = myDocument.getModificationStamp(); UIUtil.invokeLaterIfNeeded( new Runnable() { @Override public void run() { if (myProject.isDisposed() || modificationStamp != myDocument.getModificationStamp()) return; MarkupModel markupModel = DocumentMarkupModel.forDocument(myDocument, myProject, true); UpdateHighlightersUtil.setHighlightersInRange( myProject, myDocument, priorityIntersection, getColorsScheme(), toApplyInside, (MarkupModelEx) markupModel, Pass.UPDATE_ALL); if (myEditor != null) { new ShowAutoImportPass(myProject, myFile, myEditor) .applyInformationToEditor(); } } }); } myApplyCommand = new Runnable() { @Override public void run() { ProperTextRange range = new ProperTextRange(myStartOffset, myEndOffset); List<HighlightInfo> toApply = new ArrayList<HighlightInfo>(); for (HighlightInfo info : gotHighlights) { if (!range.containsRange(info.getStartOffset(), info.getEndOffset())) continue; if (!myPriorityRange.containsRange( info.getStartOffset(), info.getEndOffset())) { toApply.add(info); } } toApply.addAll(injectionsOutside); UpdateHighlightersUtil.setHighlightersOutsideRange( myProject, myDocument, toApply, getColorsScheme(), myStartOffset, myEndOffset, myPriorityRange, Pass.UPDATE_ALL); } }; } }, outside, progress, filteredVisitors, gotHighlights, forceHighlightParents); if (myUpdateAll) { ((DaemonCodeAnalyzerImpl) daemonCodeAnalyzer) .getFileStatusMap() .setErrorFoundFlag(myDocument, myErrorFound); } } finally { incVisitorUsageCount(-1); } myHighlights.addAll(gotHighlights); } private void getInjectedPsiFiles( @NotNull final List<PsiElement> elements1, @NotNull final List<PsiElement> elements2, @NotNull final ProgressIndicator progress, @NotNull final Set<PsiFile> outInjected) { List<DocumentWindow> injected = InjectedLanguageUtil.getCachedInjectedDocuments(myFile); Collection<PsiElement> hosts = new THashSet<PsiElement>(elements1.size() + elements2.size() + injected.size()); // rehighlight all injected PSI regardless the range, // since change in one place can lead to invalidation of injected PSI in (completely) other // place. for (DocumentWindow documentRange : injected) { progress.checkCanceled(); if (!documentRange.isValid()) continue; PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(documentRange); if (file == null) continue; PsiElement context = file.getContext(); if (context != null && context.isValid() && !file.getProject().isDisposed() && (myUpdateAll || new ProperTextRange(myStartOffset, myEndOffset) .intersects(context.getTextRange()))) { hosts.add(context); } } hosts.addAll(elements1); hosts.addAll(elements2); final PsiLanguageInjectionHost.InjectedPsiVisitor visitor = new PsiLanguageInjectionHost.InjectedPsiVisitor() { @Override public void visit( @NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) { synchronized (outInjected) { outInjected.add(injectedPsi); } } }; if (!JobUtil.invokeConcurrentlyUnderProgress( new ArrayList<PsiElement>(hosts), progress, false, new Processor<PsiElement>() { @Override public boolean process(PsiElement element) { progress.checkCanceled(); InjectedLanguageUtil.enumerate(element, myFile, false, visitor); return true; } })) throw new ProcessCanceledException(); } // returns false if canceled private boolean addInjectedPsiHighlights( @NotNull final Set<PsiFile> injectedFiles, @NotNull final ProgressIndicator progress, @NotNull final Collection<HighlightInfo> outInfos) { if (injectedFiles.isEmpty()) return true; final InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(myProject); final TextAttributes injectedAttributes = myGlobalScheme.getAttributes(EditorColors.INJECTED_LANGUAGE_FRAGMENT); return JobUtil.invokeConcurrentlyUnderProgress( new ArrayList<PsiFile>(injectedFiles), progress, myFailFastOnAcquireReadAction, new Processor<PsiFile>() { @Override public boolean process(final PsiFile injectedPsi) { DocumentWindow documentWindow = (DocumentWindow) PsiDocumentManager.getInstance(myProject).getCachedDocument(injectedPsi); if (documentWindow == null) return true; Place places = InjectedLanguageUtil.getShreds(injectedPsi); for (PsiLanguageInjectionHost.Shred place : places) { TextRange textRange = place.getRangeInsideHost().shiftRight(place.host.getTextRange().getStartOffset()); if (textRange.isEmpty()) continue; String desc = injectedPsi.getLanguage().getDisplayName() + ": " + injectedPsi.getText(); HighlightInfo info = HighlightInfo.createHighlightInfo( HighlightInfoType.INJECTED_LANGUAGE_FRAGMENT, textRange, null, desc, injectedAttributes); info.fromInjection = true; outInfos.add(info); } HighlightInfoHolder holder = createInfoHolder(injectedPsi); runHighlightVisitorsForInjected(injectedPsi, holder, progress); for (int i = 0; i < holder.size(); i++) { HighlightInfo info = holder.get(i); final int startOffset = documentWindow.injectedToHost(info.startOffset); final TextRange fixedTextRange = getFixedTextRange(documentWindow, startOffset); addPatchedInfos( info, injectedPsi, documentWindow, injectedLanguageManager, fixedTextRange, outInfos); } holder.clear(); highlightInjectedSyntax(injectedPsi, holder); for (int i = 0; i < holder.size(); i++) { HighlightInfo info = holder.get(i); final int startOffset = info.startOffset; final TextRange fixedTextRange = getFixedTextRange(documentWindow, startOffset); if (fixedTextRange == null) { info.fromInjection = true; outInfos.add(info); } else { HighlightInfo patched = new HighlightInfo( info.forcedTextAttributes, info.forcedTextAttributesKey, info.type, fixedTextRange.getStartOffset(), fixedTextRange.getEndOffset(), info.description, info.toolTip, info.type.getSeverity(null), info.isAfterEndOfLine, null, false); patched.fromInjection = true; outInfos.add(patched); } } if (!isDumbMode()) { List<HighlightInfo> todos = new ArrayList<HighlightInfo>(); highlightTodos( injectedPsi, injectedPsi.getText(), 0, injectedPsi.getTextLength(), progress, myPriorityRange, todos, todos); for (HighlightInfo info : todos) { addPatchedInfos( info, injectedPsi, documentWindow, injectedLanguageManager, null, outInfos); } } return true; } }); } private static TextRange getFixedTextRange( @NotNull DocumentWindow documentWindow, int startOffset) { final TextRange fixedTextRange; TextRange textRange = documentWindow.getHostRange(startOffset); if (textRange == null) { // todo[cdr] check this fix. prefix/suffix code annotation case textRange = findNearestTextRange(documentWindow, startOffset); final boolean isBefore = startOffset < textRange.getStartOffset(); fixedTextRange = new ProperTextRange( isBefore ? textRange.getStartOffset() - 1 : textRange.getEndOffset(), isBefore ? textRange.getStartOffset() : textRange.getEndOffset() + 1); } else { fixedTextRange = null; } return fixedTextRange; } private static void addPatchedInfos( @NotNull HighlightInfo info, @NotNull PsiFile injectedPsi, @NotNull DocumentWindow documentWindow, @NotNull InjectedLanguageManager injectedLanguageManager, @Nullable TextRange fixedTextRange, @NotNull Collection<HighlightInfo> out) { ProperTextRange textRange = new ProperTextRange(info.startOffset, info.endOffset); List<TextRange> editables = injectedLanguageManager.intersectWithAllEditableFragments(injectedPsi, textRange); for (TextRange editable : editables) { TextRange hostRange = fixedTextRange == null ? documentWindow.injectedToHost(editable) : fixedTextRange; boolean isAfterEndOfLine = info.isAfterEndOfLine; if (isAfterEndOfLine) { // convert injected afterEndOfLine to either host' afterEndOfLine or not-afterEndOfLine // highlight of the injected fragment boundary int hostEndOffset = hostRange.getEndOffset(); int lineNumber = documentWindow.getDelegate().getLineNumber(hostEndOffset); int hostLineEndOffset = documentWindow.getDelegate().getLineEndOffset(lineNumber); if (hostEndOffset < hostLineEndOffset) { // convert to non-afterEndOfLine isAfterEndOfLine = false; hostRange = new ProperTextRange(hostRange.getStartOffset(), hostEndOffset + 1); } } HighlightInfo patched = new HighlightInfo( info.forcedTextAttributes, info.forcedTextAttributesKey, info.type, hostRange.getStartOffset(), hostRange.getEndOffset(), info.description, info.toolTip, info.type.getSeverity(null), isAfterEndOfLine, null, false); patched.setHint(info.hasHint()); patched.setGutterIconRenderer(info.getGutterIconRenderer()); if (info.quickFixActionRanges != null) { for (Pair<HighlightInfo.IntentionActionDescriptor, TextRange> pair : info.quickFixActionRanges) { TextRange quickfixTextRange = pair.getSecond(); List<TextRange> editableQF = injectedLanguageManager.intersectWithAllEditableFragments( injectedPsi, quickfixTextRange); for (TextRange editableRange : editableQF) { HighlightInfo.IntentionActionDescriptor descriptor = pair.getFirst(); if (patched.quickFixActionRanges == null) patched.quickFixActionRanges = new ArrayList<Pair<HighlightInfo.IntentionActionDescriptor, TextRange>>(); TextRange hostEditableRange = documentWindow.injectedToHost(editableRange); patched.quickFixActionRanges.add(Pair.create(descriptor, hostEditableRange)); } } } patched.fromInjection = true; out.add(patched); } } // finds the first nearest text range private static TextRange findNearestTextRange( final DocumentWindow documentWindow, final int startOffset) { TextRange textRange = null; for (RangeMarker marker : documentWindow.getHostRanges()) { TextRange curRange = ProperTextRange.create(marker); if (curRange.getStartOffset() > startOffset && textRange != null) break; textRange = curRange; } assert textRange != null; return textRange; } private void runHighlightVisitorsForInjected( @NotNull PsiFile injectedPsi, @NotNull final HighlightInfoHolder holder, @NotNull final ProgressIndicator progress) { HighlightVisitor[] visitors = getHighlightVisitors(); try { HighlightVisitor[] filtered = filterVisitors(visitors, injectedPsi); final List<PsiElement> elements = CollectHighlightsUtil.getElementsInRange(injectedPsi, 0, injectedPsi.getTextLength()); for (final HighlightVisitor visitor : filtered) { visitor.analyze( injectedPsi, true, holder, new Runnable() { @Override public void run() { for (PsiElement element : elements) { progress.checkCanceled(); visitor.visit(element); } } }); } } finally { incVisitorUsageCount(-1); } } private void highlightInjectedSyntax(final PsiFile injectedPsi, HighlightInfoHolder holder) { List<Trinity<IElementType, PsiLanguageInjectionHost, TextRange>> tokens = InjectedLanguageUtil.getHighlightTokens(injectedPsi); if (tokens == null) return; final Language injectedLanguage = injectedPsi.getLanguage(); Project project = injectedPsi.getProject(); SyntaxHighlighter syntaxHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter( injectedLanguage, project, injectedPsi.getVirtualFile()); final TextAttributes defaultAttrs = myGlobalScheme.getAttributes(HighlighterColors.TEXT); for (Trinity<IElementType, PsiLanguageInjectionHost, TextRange> token : tokens) { ProgressManager.checkCanceled(); IElementType tokenType = token.getFirst(); PsiLanguageInjectionHost injectionHost = token.getSecond(); TextRange textRange = token.getThird(); TextAttributesKey[] keys = syntaxHighlighter.getTokenHighlights(tokenType); if (textRange.getLength() == 0) continue; TextRange annRange = textRange.shiftRight(injectionHost.getTextRange().getStartOffset()); // force attribute colors to override host' ones TextAttributes attributes = null; for (TextAttributesKey key : keys) { TextAttributes attrs2 = myGlobalScheme.getAttributes(key); if (attrs2 != null) { attributes = attributes == null ? attrs2 : TextAttributes.merge(attributes, attrs2); } } TextAttributes forcedAttributes; if (attributes == null || attributes.isEmpty() || attributes.equals(defaultAttrs)) { forcedAttributes = TextAttributes.ERASE_MARKER; } else { Color back = attributes.getBackgroundColor() == null ? myGlobalScheme.getDefaultBackground() : attributes.getBackgroundColor(); Color fore = attributes.getForegroundColor() == null ? myGlobalScheme.getDefaultForeground() : attributes.getForegroundColor(); forcedAttributes = new TextAttributes( fore, back, attributes.getEffectColor(), attributes.getEffectType(), attributes.getFontType()); } HighlightInfo info = HighlightInfo.createHighlightInfo( HighlightInfoType.INJECTED_LANGUAGE_FRAGMENT, annRange, null, null, forcedAttributes); holder.add(info); } } private boolean isWholeFileHighlighting() { return myUpdateAll && myStartOffset == 0 && myEndOffset == myDocument.getTextLength(); } @Override protected void applyInformationWithProgress() { myFile.putUserData(HAS_ERROR_ELEMENT, myHasErrorElement); myApplyCommand.run(); if (myUpdateAll) { reportErrorsToWolf(); } } @Override @NotNull public List<HighlightInfo> getInfos() { return new ArrayList<HighlightInfo>(myHighlights); } private void collectHighlights( @NotNull final List<PsiElement> elements1, @NotNull final Runnable after1, @NotNull final List<PsiElement> elements2, @NotNull final ProgressIndicator progress, @NotNull final HighlightVisitor[] visitors, @NotNull final Set<HighlightInfo> gotHighlights, final boolean forceHighlightParents) { final Set<PsiElement> skipParentsSet = new THashSet<PsiElement>(); // TODO - add color scheme to holder final HighlightInfoHolder holder = createInfoHolder(myFile); final int chunkSize = Math.max(1, (elements1.size() + elements2.size()) / 100); // one percent precision is enough final Runnable action = new Runnable() { @Override public void run() { //noinspection unchecked boolean failed = false; for (List<PsiElement> elements : new List[] {elements1, elements2}) { int nextLimit = chunkSize; for (int i = 0; i < elements.size(); i++) { PsiElement element = elements.get(i); progress.checkCanceled(); if (element != myFile && !skipParentsSet.isEmpty() && element.getFirstChild() != null && skipParentsSet.contains(element)) { skipParentsSet.add(element.getParent()); continue; } if (element instanceof PsiErrorElement) { myHasErrorElement = true; } holder.clear(); for (final HighlightVisitor visitor : visitors) { try { visitor.visit(element); } catch (ProcessCanceledException e) { throw e; } catch (IndexNotReadyException e) { throw e; } catch (WolfTheProblemSolverImpl.HaveGotErrorException e) { throw e; } catch (Exception e) { if (!failed) { LOG.error(e); } failed = true; } } if (i == nextLimit) { advanceProgress(chunkSize); nextLimit = i + chunkSize; } //noinspection ForLoopReplaceableByForEach for (int j = 0; j < holder.size(); j++) { final HighlightInfo info = holder.get(j); assert info != null; // have to filter out already obtained highlights if (!gotHighlights.add(info)) continue; boolean isError = info.getSeverity() == HighlightSeverity.ERROR; if (isError) { if (!forceHighlightParents) { skipParentsSet.add(element.getParent()); } myErrorFound = true; } myTransferToEDTQueue.offer(Pair.create(info, progress)); } } advanceProgress(elements.size() - (nextLimit - chunkSize)); if (elements == elements1) after1.run(); } } }; analyzeByVisitors(progress, visitors, holder, 0, action); } // private void collectHighlights(@NotNull final List<PsiElement> elements1, // @NotNull final Runnable after1, // @NotNull final List<PsiElement> elements2, // @NotNull final ProgressIndicator progress, // @NotNull final HighlightVisitor[] visitors, // @NotNull final Set<HighlightInfo> gotHighlights, // final boolean forceHighlightParents) { // final HighlightInfoHolder holder = createInfoHolder(myFile); // // final int chunkSize = Math.max(1, (elements1.size()+elements2.size()) / 100); // one percent // precision is enough // // final Runnable action = new Runnable() { // public void run() { // //noinspection unchecked // boolean failed = false; // PsiNodeTask task = createPsiTask(myFile); // JobSchedulerImpl.submitTask(task); // // for (List<PsiElement> elements : new List[]{elements1, elements2}) { // int nextLimit = chunkSize; // for (int i = 0; i < elements.size(); i++) { // PsiElement element = elements.get(i); // progress.checkCanceled(); // // if (element != myFile && !skipParentsSet.isEmpty() && element.getFirstChild() != null // && skipParentsSet.contains(element)) { // skipParentsSet.add(element.getParent()); // continue; // } // // if (element instanceof PsiErrorElement) { // myHasErrorElement = true; // } // kjlhkjh // if (i == nextLimit) { // advanceProgress(chunkSize); // nextLimit = i + chunkSize; // } // // } // advanceProgress(elements.size() - (nextLimit-chunkSize)); // if (elements == elements1) after1.run(); // } // } // // private PsiNodeTask createPsiTask(@NotNull PsiElement root) { // return new PsiNodeTask(root) { // @Override // public void onEnter(@NotNull PsiElement element) { // if (element instanceof PsiErrorElement) { // myHasErrorElement = true; // } // super.onEnter(element); // } // // @Override // protected PsiNodeTask forkNode(@NotNull PsiElement child) { // return createPsiTask(child); // } // // @Override // protected boolean highlight(PsiElement element) { // holder.clear(); // // for (final HighlightVisitor visitor : visitors) { // try { // visitor.visit(element); // } // catch (ProcessCanceledException e) { // throw e; // } // catch (IndexNotReadyException e) { // throw e; // } // catch (WolfTheProblemSolverImpl.HaveGotErrorException e) { // throw e; // } // catch (Exception e) { // LOG.error(e); // } // } // // //noinspection ForLoopReplaceableByForEach // for (int j = 0; j < holder.size(); j++) { // final HighlightInfo info = holder.get(j); // assert info != null; // // have to filter out already obtained highlights // if (!gotHighlights.add(info)) continue; // boolean isError = info.getSeverity() == HighlightSeverity.ERROR; // if (isError) { // if (!forceHighlightParents) { // skipParentsSet.add(element.getParent()); // } // myErrorFound = true; // } // myTransferToEDTQueue.offer(Pair.create(info, progress)); // } // return true; // } // }; // } // }; // // analyzeByVisitors(progress, visitors, holder, 0, action); // } private final Map<TextRange, RangeMarker> ranges2markersCache = new THashMap<TextRange, RangeMarker>(); private final TransferToEDTQueue<Pair<HighlightInfo, ProgressIndicator>> myTransferToEDTQueue = new TransferToEDTQueue<Pair<HighlightInfo, ProgressIndicator>>( "Apply highlighting results", new Processor<Pair<HighlightInfo, ProgressIndicator>>() { @Override public boolean process(Pair<HighlightInfo, ProgressIndicator> pair) { ApplicationManager.getApplication().assertIsDispatchThread(); ProgressIndicator indicator = pair.getSecond(); if (indicator.isCanceled()) { return false; } HighlightInfo info = pair.getFirst(); final EditorColorsScheme colorsScheme = getColorsScheme(); UpdateHighlightersUtil.addHighlighterToEditorIncrementally( myProject, myDocument, myFile, myStartOffset, myEndOffset, info, colorsScheme, Pass.UPDATE_ALL, ranges2markersCache); return true; } }, myProject.getDisposed(), 200); private void analyzeByVisitors( @NotNull final ProgressIndicator progress, @NotNull final HighlightVisitor[] visitors, @NotNull final HighlightInfoHolder holder, final int i, @NotNull final Runnable action) { if (i == visitors.length) { action.run(); } else { if (!visitors[i].analyze( myFile, myUpdateAll, holder, new Runnable() { @Override public void run() { analyzeByVisitors(progress, visitors, holder, i + 1, action); } })) { cancelAndRestartDaemonLater(progress, myProject, this); } } } private static HighlightVisitor[] filterVisitors( HighlightVisitor[] highlightVisitors, final PsiFile file) { final List<HighlightVisitor> visitors = new ArrayList<HighlightVisitor>(highlightVisitors.length); List<HighlightVisitor> list = Arrays.asList(highlightVisitors); for (HighlightVisitor visitor : DumbService.getInstance(file.getProject()).filterByDumbAwareness(list)) { if (visitor.suitableForFile(file)) visitors.add(visitor); } LOG.assertTrue(!visitors.isEmpty(), list); HighlightVisitor[] visitorArray = visitors.toArray(new HighlightVisitor[visitors.size()]); Arrays.sort(visitorArray, VISITOR_ORDER_COMPARATOR); return visitorArray; } static Void cancelAndRestartDaemonLater( ProgressIndicator progress, final Project project, TextEditorHighlightingPass pass) { PassExecutorService.log(progress, pass, "Cancel and restart"); progress.cancel(); ApplicationManager.getApplication() .invokeLater( new Runnable() { @Override public void run() { try { Thread.sleep(new Random().nextInt(100)); } catch (InterruptedException e) { LOG.error(e); } DaemonCodeAnalyzer.getInstance(project).restart(); } }, project.getDisposed()); throw new ProcessCanceledException(); } private boolean forceHighlightParents() { boolean forceHighlightParents = false; for (HighlightRangeExtension extension : Extensions.getExtensions(HighlightRangeExtension.EP_NAME)) { if (extension.isForceHighlightParents(myFile)) { forceHighlightParents = true; break; } } return forceHighlightParents; } protected HighlightInfoHolder createInfoHolder(final PsiFile file) { final HighlightInfoFilter[] filters = ApplicationManager.getApplication().getExtensions(HighlightInfoFilter.EXTENSION_POINT_NAME); return new HighlightInfoHolder(file, getColorsScheme(), filters); } private static void highlightTodos( @NotNull PsiFile file, @NotNull CharSequence text, int startOffset, int endOffset, @NotNull ProgressIndicator progress, @NotNull ProperTextRange priorityRange, @NotNull Collection<HighlightInfo> result, @NotNull Collection<HighlightInfo> outsideResult) { PsiSearchHelper helper = PsiSearchHelper.SERVICE.getInstance(file.getProject()); TodoItem[] todoItems = helper.findTodoItems(file, startOffset, endOffset); if (todoItems.length == 0) return; for (TodoItem todoItem : todoItems) { progress.checkCanceled(); TextRange range = todoItem.getTextRange(); String description = text.subSequence(range.getStartOffset(), range.getEndOffset()).toString(); TextAttributes attributes = todoItem.getPattern().getAttributes().getTextAttributes(); HighlightInfo info = HighlightInfo.createHighlightInfo( HighlightInfoType.TODO, range, description, description, attributes); assert info != null; if (priorityRange.containsRange(info.getStartOffset(), info.getEndOffset())) { result.add(info); } else { outsideResult.add(info); } } } private void reportErrorsToWolf() { if (!myFile.getViewProvider().isPhysical()) return; // e.g. errors in evaluate expression Project project = myFile.getProject(); if (!PsiManager.getInstance(project).isInProject(myFile)) return; // do not report problems in libraries VirtualFile file = myFile.getVirtualFile(); if (file == null) return; List<Problem> problems = convertToProblems(getInfos(), file, myHasErrorElement); WolfTheProblemSolver wolf = WolfTheProblemSolver.getInstance(project); boolean hasErrors = DaemonCodeAnalyzerImpl.hasErrors(project, getDocument()); if (!hasErrors || isWholeFileHighlighting()) { wolf.reportProblems(file, problems); } else { wolf.weHaveGotProblems(file, problems); } } @Override public double getProgress() { // do not show progress of visible highlighters update return myUpdateAll ? super.getProgress() : -1; } private static List<Problem> convertToProblems( final Collection<HighlightInfo> infos, final VirtualFile file, final boolean hasErrorElement) { List<Problem> problems = new SmartList<Problem>(); for (HighlightInfo info : infos) { if (info.getSeverity() == HighlightSeverity.ERROR) { Problem problem = new ProblemImpl(file, info, hasErrorElement); problems.add(problem); } } return problems; } @Override public String toString() { return super.toString() + " updateAll=" + myUpdateAll + " range=(" + myStartOffset + "," + myEndOffset + ")"; } public void setFailFastOnAcquireReadAction(boolean failFastOnAcquireReadAction) { myFailFastOnAcquireReadAction = failFastOnAcquireReadAction; } }
/** Implements PyFunction. */ public class PyFunctionImpl extends PyBaseElementImpl<PyFunctionStub> implements PyFunction { private static final Key<CachedValue<List<PyAssignmentStatement>>> ATTRIBUTES_KEY = Key.create("attributes"); public PyFunctionImpl(ASTNode astNode) { super(astNode); } public PyFunctionImpl(final PyFunctionStub stub) { this(stub, PyElementTypes.FUNCTION_DECLARATION); } public PyFunctionImpl(PyFunctionStub stub, IStubElementType nodeType) { super(stub, nodeType); } private class CachedStructuredDocStringProvider implements CachedValueProvider<StructuredDocString> { @Nullable @Override public Result<StructuredDocString> compute() { final PyFunctionImpl f = PyFunctionImpl.this; return Result.create(DocStringUtil.getStructuredDocString(f), f); } } private CachedStructuredDocStringProvider myCachedStructuredDocStringProvider = new CachedStructuredDocStringProvider(); @Nullable @Override public String getName() { final PyFunctionStub stub = getStub(); if (stub != null) { return stub.getName(); } ASTNode node = getNameNode(); return node != null ? node.getText() : null; } public PsiElement getNameIdentifier() { final ASTNode nameNode = getNameNode(); return nameNode != null ? nameNode.getPsi() : null; } public PsiElement setName(@NotNull String name) throws IncorrectOperationException { final ASTNode nameElement = PyUtil.createNewName(this, name); final ASTNode nameNode = getNameNode(); if (nameNode != null) { getNode().replaceChild(nameNode, nameElement); } return this; } @Override public Icon getIcon(int flags) { PyPsiUtils.assertValid(this); final Property property = getProperty(); if (property != null) { if (property.getGetter().valueOrNull() == this) { return PythonIcons.Python.PropertyGetter; } if (property.getSetter().valueOrNull() == this) { return PythonIcons.Python.PropertySetter; } if (property.getDeleter().valueOrNull() == this) { return PythonIcons.Python.PropertyDeleter; } return PlatformIcons.PROPERTY_ICON; } if (getContainingClass() != null) { return PlatformIcons.METHOD_ICON; } return PythonIcons.Python.Function; } @Nullable public ASTNode getNameNode() { return getNode().findChildByType(PyTokenTypes.IDENTIFIER); } @NotNull public PyParameterList getParameterList() { return getRequiredStubOrPsiChild(PyElementTypes.PARAMETER_LIST); } @Override @NotNull public PyStatementList getStatementList() { final PyStatementList statementList = childToPsi(PyElementTypes.STATEMENT_LIST); assert statementList != null : "Statement list missing for function " + getText(); return statementList; } public PyClass getContainingClass() { final PyFunctionStub stub = getStub(); if (stub != null) { final StubElement parentStub = stub.getParentStub(); if (parentStub instanceof PyClassStub) { return ((PyClassStub) parentStub).getPsi(); } return null; } final PsiElement parent = PsiTreeUtil.getParentOfType(this, StubBasedPsiElement.class); if (parent instanceof PyClass) { return (PyClass) parent; } return null; } @Nullable public PyDecoratorList getDecoratorList() { return getStubOrPsiChild( PyElementTypes.DECORATOR_LIST); // PsiTreeUtil.getChildOfType(this, PyDecoratorList.class); } @Nullable @Override public PyType getReturnType(@NotNull TypeEvalContext context, @NotNull TypeEvalContext.Key key) { final PyType type = getReturnType(context); return isAsync() && isAsyncAllowed() ? createCoroutineType(type) : type; } @Nullable private PyType getReturnType(@NotNull TypeEvalContext context) { for (PyTypeProvider typeProvider : Extensions.getExtensions(PyTypeProvider.EP_NAME)) { final Ref<PyType> returnTypeRef = typeProvider.getReturnType(this, context); if (returnTypeRef != null) { return derefType(returnTypeRef, typeProvider); } } if (context.allowReturnTypes(this)) { final Ref<? extends PyType> yieldTypeRef = getYieldStatementType(context); if (yieldTypeRef != null) { return yieldTypeRef.get(); } return getReturnStatementType(context); } return null; } @Nullable @Override public PyType getCallType( @NotNull TypeEvalContext context, @NotNull PyCallSiteExpression callSite) { for (PyTypeProvider typeProvider : Extensions.getExtensions(PyTypeProvider.EP_NAME)) { final Ref<PyType> typeRef = typeProvider.getCallType(this, callSite, context); if (typeRef != null) { return derefType(typeRef, typeProvider); } } final PyExpression receiver = PyTypeChecker.getReceiver(callSite, this); final Map<PyExpression, PyNamedParameter> mapping = PyCallExpressionHelper.mapArguments(callSite, this, context); return getCallType(receiver, mapping, context); } @Nullable private static PyType derefType( @NotNull Ref<PyType> typeRef, @NotNull PyTypeProvider typeProvider) { final PyType type = typeRef.get(); if (type != null) { type.assertValid(typeProvider.toString()); } return type; } @Nullable @Override public PyType getCallType( @Nullable PyExpression receiver, @NotNull Map<PyExpression, PyNamedParameter> parameters, @NotNull TypeEvalContext context) { return analyzeCallType(context.getReturnType(this), receiver, parameters, context); } @Nullable private PyType analyzeCallType( @Nullable PyType type, @Nullable PyExpression receiver, @NotNull Map<PyExpression, PyNamedParameter> parameters, @NotNull TypeEvalContext context) { if (PyTypeChecker.hasGenerics(type, context)) { final Map<PyGenericType, PyType> substitutions = PyTypeChecker.unifyGenericCall(receiver, parameters, context); if (substitutions != null) { type = PyTypeChecker.substitute(type, substitutions, context); } else { type = null; } } if (receiver != null) { type = replaceSelf(type, receiver, context); } if (type != null && isDynamicallyEvaluated(parameters.values(), context)) { type = PyUnionType.createWeakType(type); } return type; } @Override public ItemPresentation getPresentation() { return new PyElementPresentation(this) { @Nullable @Override public String getPresentableText() { return notNullize(getName(), PyNames.UNNAMED_ELEMENT) + getParameterList().getPresentableText(true); } @Nullable @Override public String getLocationString() { final PyClass containingClass = getContainingClass(); if (containingClass != null) { return "(" + containingClass.getName() + " in " + getPackageForFile(getContainingFile()) + ")"; } return super.getLocationString(); } }; } @Nullable private PyType replaceSelf( @Nullable PyType returnType, @Nullable PyExpression receiver, @NotNull TypeEvalContext context) { if (receiver != null) { // TODO: Currently we substitute only simple subclass types, but we could handle union and // collection types as well if (returnType instanceof PyClassType) { final PyClassType returnClassType = (PyClassType) returnType; if (returnClassType.getPyClass() == getContainingClass()) { final PyType receiverType = context.getType(receiver); if (receiverType instanceof PyClassType && PyTypeChecker.match(returnType, receiverType, context)) { return returnClassType.isDefinition() ? receiverType : ((PyClassType) receiverType).toInstance(); } } } } return returnType; } private static boolean isDynamicallyEvaluated( @NotNull Collection<PyNamedParameter> parameters, @NotNull TypeEvalContext context) { for (PyNamedParameter parameter : parameters) { final PyType type = context.getType(parameter); if (type instanceof PyDynamicallyEvaluatedType) { return true; } } return false; } @Nullable private Ref<? extends PyType> getYieldStatementType(@NotNull final TypeEvalContext context) { Ref<PyType> elementType = null; final PyBuiltinCache cache = PyBuiltinCache.getInstance(this); final PyStatementList statements = getStatementList(); final Set<PyType> types = new LinkedHashSet<>(); statements.accept( new PyRecursiveElementVisitor() { @Override public void visitPyYieldExpression(PyYieldExpression node) { final PyExpression expr = node.getExpression(); final PyType type = expr != null ? context.getType(expr) : null; if (node.isDelegating() && type instanceof PyCollectionType) { final PyCollectionType collectionType = (PyCollectionType) type; // TODO: Select the parameter types that matches T in Iterable[T] final List<PyType> elementTypes = collectionType.getElementTypes(context); types.add(elementTypes.isEmpty() ? null : elementTypes.get(0)); } else { types.add(type); } } @Override public void visitPyFunction(PyFunction node) { // Ignore nested functions } }); final int n = types.size(); if (n == 1) { elementType = Ref.create(types.iterator().next()); } else if (n > 0) { elementType = Ref.create(PyUnionType.union(types)); } if (elementType != null) { final PyClass generator = cache.getClass(PyNames.FAKE_GENERATOR); if (generator != null) { final List<PyType> parameters = Arrays.asList(elementType.get(), null, getReturnStatementType(context)); return Ref.create(new PyCollectionTypeImpl(generator, false, parameters)); } } if (!types.isEmpty()) { return Ref.create(null); } return null; } @Nullable public PyType getReturnStatementType(TypeEvalContext typeEvalContext) { final ReturnVisitor visitor = new ReturnVisitor(this, typeEvalContext); final PyStatementList statements = getStatementList(); statements.accept(visitor); if (isGeneratedStub() && !visitor.myHasReturns) { if (PyNames.INIT.equals(getName())) { return PyNoneType.INSTANCE; } return null; } return visitor.result(); } @Nullable private PyType createCoroutineType(@Nullable PyType returnType) { final PyBuiltinCache cache = PyBuiltinCache.getInstance(this); if (returnType instanceof PyClassLikeType && PyNames.FAKE_COROUTINE.equals(((PyClassLikeType) returnType).getClassQName())) { return returnType; } final PyClass generator = cache.getClass(PyNames.FAKE_COROUTINE); return generator != null ? new PyCollectionTypeImpl(generator, false, Collections.singletonList(returnType)) : null; } public PyFunction asMethod() { if (getContainingClass() != null) { return this; } else { return null; } } @Nullable @Override public PyType getReturnTypeFromDocString() { final String typeName = extractReturnType(); return typeName != null ? PyTypeParser.getTypeByName(this, typeName) : null; } @Nullable @Override public String getDeprecationMessage() { PyFunctionStub stub = getStub(); if (stub != null) { return stub.getDeprecationMessage(); } return extractDeprecationMessage(); } @Nullable public String extractDeprecationMessage() { PyStatementList statementList = getStatementList(); return extractDeprecationMessage(Arrays.asList(statementList.getStatements())); } @Override public PyType getType(@NotNull TypeEvalContext context, @NotNull TypeEvalContext.Key key) { for (PyTypeProvider provider : Extensions.getExtensions(PyTypeProvider.EP_NAME)) { final PyType type = provider.getCallableType(this, context); if (type != null) { return type; } } final PyFunctionTypeImpl type = new PyFunctionTypeImpl(this); if (PyKnownDecoratorUtil.hasUnknownDecorator(this, context) && getProperty() == null) { return PyUnionType.createWeakType(type); } return type; } @Nullable public static String extractDeprecationMessage(List<PyStatement> statements) { for (PyStatement statement : statements) { if (statement instanceof PyExpressionStatement) { PyExpressionStatement expressionStatement = (PyExpressionStatement) statement; if (expressionStatement.getExpression() instanceof PyCallExpression) { PyCallExpression callExpression = (PyCallExpression) expressionStatement.getExpression(); if (callExpression.isCalleeText(PyNames.WARN)) { PyReferenceExpression warningClass = callExpression.getArgument(1, PyReferenceExpression.class); if (warningClass != null && (PyNames.DEPRECATION_WARNING.equals(warningClass.getReferencedName()) || PyNames.PENDING_DEPRECATION_WARNING.equals( warningClass.getReferencedName()))) { return PyPsiUtils.strValue(callExpression.getArguments()[0]); } } } } } return null; } @Override public String getDocStringValue() { final PyFunctionStub stub = getStub(); if (stub != null) { return stub.getDocString(); } return DocStringUtil.getDocStringValue(this); } @Nullable @Override public StructuredDocString getStructuredDocString() { return CachedValuesManager.getCachedValue(this, myCachedStructuredDocStringProvider); } private boolean isGeneratedStub() { VirtualFile vFile = getContainingFile().getVirtualFile(); if (vFile != null) { vFile = vFile.getParent(); if (vFile != null) { vFile = vFile.getParent(); if (vFile != null && vFile.getName().equals(PythonSdkType.SKELETON_DIR_NAME)) { return true; } } } return false; } @Nullable private String extractReturnType() { final String ARROW = "->"; final StructuredDocString structuredDocString = getStructuredDocString(); if (structuredDocString != null) { return structuredDocString.getReturnType(); } final String docString = getDocStringValue(); if (docString != null && docString.contains(ARROW)) { final List<String> lines = StringUtil.split(docString, "\n"); while (lines.size() > 0 && lines.get(0).trim().length() == 0) { lines.remove(0); } if (lines.size() > 1 && lines.get(1).trim().length() == 0) { String firstLine = lines.get(0); int pos = firstLine.lastIndexOf(ARROW); if (pos >= 0) { return firstLine.substring(pos + 2).trim(); } } } return null; } private static class ReturnVisitor extends PyRecursiveElementVisitor { private final PyFunction myFunction; private final TypeEvalContext myContext; private PyType myResult = null; private boolean myHasReturns = false; private boolean myHasRaises = false; public ReturnVisitor(PyFunction function, final TypeEvalContext context) { myFunction = function; myContext = context; } @Override public void visitPyReturnStatement(PyReturnStatement node) { if (PsiTreeUtil.getParentOfType(node, ScopeOwner.class, true) == myFunction) { final PyExpression expr = node.getExpression(); PyType returnType; returnType = expr == null ? PyNoneType.INSTANCE : myContext.getType(expr); if (!myHasReturns) { myResult = returnType; myHasReturns = true; } else { myResult = PyUnionType.union(myResult, returnType); } } } @Override public void visitPyRaiseStatement(PyRaiseStatement node) { myHasRaises = true; } @Nullable PyType result() { return myHasReturns || myHasRaises ? myResult : PyNoneType.INSTANCE; } } @Override protected void acceptPyVisitor(PyElementVisitor pyVisitor) { pyVisitor.visitPyFunction(this); } public int getTextOffset() { final ASTNode name = getNameNode(); return name != null ? name.getStartOffset() : super.getTextOffset(); } public PyStringLiteralExpression getDocStringExpression() { final PyStatementList stmtList = getStatementList(); return DocStringUtil.findDocStringExpression(stmtList); } @Override public String toString() { return super.toString() + "('" + getName() + "')"; } public void subtreeChanged() { super.subtreeChanged(); ControlFlowCache.clear(this); } public Property getProperty() { final PyClass containingClass = getContainingClass(); if (containingClass != null) { return containingClass.findPropertyByCallable(this); } return null; } @Override public PyAnnotation getAnnotation() { return getStubOrPsiChild(PyElementTypes.ANNOTATION); } @Nullable @Override public PsiComment getTypeComment() { final PsiComment inlineComment = PyUtil.getCommentOnHeaderLine(this); if (inlineComment != null && PyTypingTypeProvider.getTypeCommentValue(inlineComment.getText()) != null) { return inlineComment; } final PyStatementList statements = getStatementList(); if (statements.getStatements().length != 0) { final PsiComment comment = as(statements.getFirstChild(), PsiComment.class); if (comment != null && PyTypingTypeProvider.getTypeCommentValue(comment.getText()) != null) { return comment; } } return null; } @Nullable @Override public String getTypeCommentAnnotation() { final PyFunctionStub stub = getStub(); if (stub != null) { return stub.getTypeComment(); } final PsiComment comment = getTypeComment(); if (comment != null) { return PyTypingTypeProvider.getTypeCommentValue(comment.getText()); } return null; } @NotNull @Override public SearchScope getUseScope() { final ScopeOwner scopeOwner = ScopeUtil.getScopeOwner(this); if (scopeOwner instanceof PyFunction) { return new LocalSearchScope(scopeOwner); } return super.getUseScope(); } /** * Looks for two standard decorators to a function, or a wrapping assignment that closely follows * it. * * @return a flag describing what was detected. */ @Nullable public Modifier getModifier() { final String deconame = getClassOrStaticMethodDecorator(); if (PyNames.CLASSMETHOD.equals(deconame)) { return CLASSMETHOD; } else if (PyNames.STATICMETHOD.equals(deconame)) { return STATICMETHOD; } // implicit staticmethod __new__ final PyClass cls = getContainingClass(); if (cls != null && PyNames.NEW.equals(getName()) && cls.isNewStyleClass(null)) { return STATICMETHOD; } // if (getStub() != null) { return getWrappersFromStub(); } final String funcName = getName(); if (funcName != null) { PyAssignmentStatement currentAssignment = PsiTreeUtil.getNextSiblingOfType(this, PyAssignmentStatement.class); while (currentAssignment != null) { final String modifier = currentAssignment .getTargetsToValuesMapping() .stream() .filter( pair -> pair.getFirst() instanceof PyTargetExpression && funcName.equals(pair.getFirst().getName())) .filter(pair -> pair.getSecond() instanceof PyCallExpression) .map( pair -> interpretAsModifierWrappingCall((PyCallExpression) pair.getSecond(), this)) .filter(interpreted -> interpreted != null && interpreted.getSecond() == this) .map(interpreted -> interpreted.getFirst()) .filter( wrapperName -> PyNames.CLASSMETHOD.equals(wrapperName) || PyNames.STATICMETHOD.equals(wrapperName)) .findAny() .orElse(null); if (PyNames.CLASSMETHOD.equals(modifier)) { return CLASSMETHOD; } else if (PyNames.STATICMETHOD.equals(modifier)) { return STATICMETHOD; } currentAssignment = PsiTreeUtil.getNextSiblingOfType(currentAssignment, PyAssignmentStatement.class); } } return null; } @Override public boolean isAsync() { final PyFunctionStub stub = getStub(); if (stub != null) { return stub.isAsync(); } return getNode().findChildByType(PyTokenTypes.ASYNC_KEYWORD) != null; } @Override public boolean isAsyncAllowed() { final LanguageLevel languageLevel = LanguageLevel.forElement(this); final String functionName = getName(); return languageLevel.isAtLeast(LanguageLevel.PYTHON35) && (functionName == null || ArrayUtil.contains( functionName, PyNames.AITER, PyNames.ANEXT, PyNames.AENTER, PyNames.AEXIT) || !PyNames.getBuiltinMethods(languageLevel).containsKey(functionName)); } @Nullable private Modifier getWrappersFromStub() { final StubElement parentStub = getStub().getParentStub(); final List childrenStubs = parentStub.getChildrenStubs(); int index = childrenStubs.indexOf(getStub()); if (index >= 0 && index < childrenStubs.size() - 1) { StubElement nextStub = (StubElement) childrenStubs.get(index + 1); if (nextStub instanceof PyTargetExpressionStub) { final PyTargetExpressionStub targetExpressionStub = (PyTargetExpressionStub) nextStub; if (targetExpressionStub.getInitializerType() == PyTargetExpressionStub.InitializerType.CallExpression) { final QualifiedName qualifiedName = targetExpressionStub.getInitializer(); if (QualifiedName.fromComponents(PyNames.CLASSMETHOD).equals(qualifiedName)) { return CLASSMETHOD; } if (QualifiedName.fromComponents(PyNames.STATICMETHOD).equals(qualifiedName)) { return STATICMETHOD; } } } } return null; } /** * When a function is decorated many decorators, finds the deepest builtin decorator: * * <pre> * @foo * @classmethod <b># <-- that's it</b> * @bar * def moo(cls): * pass * </pre> * * @return name of the built-in decorator, or null (even if there are non-built-in decorators). */ @Nullable private String getClassOrStaticMethodDecorator() { PyDecoratorList decolist = getDecoratorList(); if (decolist != null) { PyDecorator[] decos = decolist.getDecorators(); if (decos.length > 0) { for (int i = decos.length - 1; i >= 0; i -= 1) { PyDecorator deco = decos[i]; String deconame = deco.getName(); if (PyNames.CLASSMETHOD.equals(deconame) || PyNames.STATICMETHOD.equals(deconame)) { return deconame; } for (PyKnownDecoratorProvider provider : PyUtil.KnownDecoratorProviderHolder.KNOWN_DECORATOR_PROVIDERS) { String name = provider.toKnownDecorator(deconame); if (name != null) { return name; } } } } } return null; } @Nullable @Override public String getQualifiedName() { return QualifiedNameFinder.getQualifiedName(this); } @NotNull @Override public List<PyAssignmentStatement> findAttributes() { /** * TODO: This method if insanely heavy since it unstubs foreign files. Need to save stubs and * use them somehow. */ return CachedValuesManager.getManager(getProject()) .getCachedValue( this, ATTRIBUTES_KEY, () -> { final List<PyAssignmentStatement> result = findAttributesStatic(this); return CachedValueProvider.Result.create( result, PsiModificationTracker.MODIFICATION_COUNT); }, false); } /** @param self should be this */ @NotNull private static List<PyAssignmentStatement> findAttributesStatic(@NotNull final PsiElement self) { final List<PyAssignmentStatement> result = new ArrayList<>(); for (final PyAssignmentStatement statement : new PsiQuery(self).siblings(PyAssignmentStatement.class).getElements()) { for (final PyQualifiedExpression targetExpression : new PsiQuery(statement.getTargets()).filter(PyQualifiedExpression.class).getElements()) { final PyExpression qualifier = targetExpression.getQualifier(); if (qualifier == null) { continue; } final PsiReference qualifierReference = qualifier.getReference(); if (qualifierReference == null) { continue; } if (qualifierReference.isReferenceTo(self)) { result.add(statement); } } } return result; } @NotNull @Override public ProtectionLevel getProtectionLevel() { final int underscoreLevels = PyUtil.getInitialUnderscores(getName()); for (final ProtectionLevel level : ProtectionLevel.values()) { if (level.getUnderscoreLevel() == underscoreLevels) { return level; } } return ProtectionLevel.PRIVATE; } }
public class FileDocumentManagerImpl extends FileDocumentManager implements ApplicationComponent, VirtualFileListener, ProjectManagerListener, SafeWriteRequestor { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl"); private static final Key<String> LINE_SEPARATOR_KEY = Key.create("LINE_SEPARATOR_KEY"); public static final Key<Reference<Document>> DOCUMENT_KEY = Key.create("DOCUMENT_KEY"); private static final Key<VirtualFile> FILE_KEY = Key.create("FILE_KEY"); private final Set<Document> myUnsavedDocuments = new ConcurrentHashSet<Document>(); private final MessageBus myBus; private static final Object lock = new Object(); private final FileDocumentManagerListener myMultiCaster; private final TrailingSpacesStripper myTrailingSpacesStripper = new TrailingSpacesStripper(); private boolean myOnClose = false; public FileDocumentManagerImpl( @NotNull VirtualFileManager virtualFileManager, @NotNull ProjectManager projectManager) { virtualFileManager.addVirtualFileListener(this); projectManager.addProjectManagerListener(this); myBus = ApplicationManager.getApplication().getMessageBus(); InvocationHandler handler = new InvocationHandler() { @Nullable @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { multiCast(method, args); return null; } }; final ClassLoader loader = FileDocumentManagerListener.class.getClassLoader(); myMultiCaster = (FileDocumentManagerListener) Proxy.newProxyInstance( loader, new Class[] {FileDocumentManagerListener.class}, handler); } @SuppressWarnings("OverlyBroadCatchBlock") private void multiCast(@NotNull Method method, Object[] args) { try { method.invoke(myBus.syncPublisher(AppTopics.FILE_DOCUMENT_SYNC), args); } catch (ClassCastException e) { LOG.error("Arguments: " + Arrays.toString(args), e); } catch (Exception e) { LOG.error(e); } // Allows pre-save document modification for (FileDocumentManagerListener listener : getListeners()) { try { method.invoke(listener, args); } catch (Exception e) { LOG.error(e); } } // stripping trailing spaces try { method.invoke(myTrailingSpacesStripper, args); } catch (Exception e) { LOG.error(e); } } @Override @NotNull public String getComponentName() { return "FileDocumentManager"; } @Override public void initComponent() {} @Override public void disposeComponent() {} @Override @Nullable public Document getDocument(@NotNull final VirtualFile file) { DocumentEx document = (DocumentEx) getCachedDocument(file); if (document == null) { if (file.isDirectory() || isBinaryWithoutDecompiler(file) || SingleRootFileViewProvider.isTooLargeForContentLoading(file)) { return null; } final CharSequence text = LoadTextUtil.loadText(file); synchronized (lock) { document = (DocumentEx) getCachedDocument(file); if (document != null) return document; // Double checking document = (DocumentEx) createDocument(text); document.setModificationStamp(file.getModificationStamp()); final FileType fileType = file.getFileType(); document.setReadOnly(!file.isWritable() || fileType.isBinary()); file.putUserData(DOCUMENT_KEY, new WeakReference<Document>(document)); document.putUserData(FILE_KEY, file); if (!(file instanceof LightVirtualFile || file.getFileSystem() instanceof DummyFileSystem)) { document.addDocumentListener( new DocumentAdapter() { @Override public void documentChanged(DocumentEvent e) { final Document document = e.getDocument(); myUnsavedDocuments.add(document); final Runnable currentCommand = CommandProcessor.getInstance().getCurrentCommand(); Project project = currentCommand == null ? null : CommandProcessor.getInstance().getCurrentCommandProject(); String lineSeparator = CodeStyleFacade.getInstance(project).getLineSeparator(); document.putUserData(LINE_SEPARATOR_KEY, lineSeparator); // avoid documents piling up during batch processing if (areTooManyDocumentsInTheQueue(myUnsavedDocuments)) { saveAllDocumentsLater(); } } }); } } myMultiCaster.fileContentLoaded(file, document); } return document; } public static boolean areTooManyDocumentsInTheQueue(Collection<Document> documents) { if (documents.size() > 100) return true; int totalSize = 0; for (Document document : documents) { totalSize += document.getTextLength(); if (totalSize > 10 * FileUtilRt.MEGABYTE) return true; } return false; } private static Document createDocument(final CharSequence text) { return EditorFactory.getInstance().createDocument(text); } @Override @Nullable public Document getCachedDocument(@NotNull VirtualFile file) { Reference<Document> reference = file.getUserData(DOCUMENT_KEY); Document document = reference == null ? null : reference.get(); if (document != null && isBinaryWithoutDecompiler(file)) { file.putUserData(DOCUMENT_KEY, null); document.putUserData(FILE_KEY, null); return null; } return document; } public static void registerDocument( @NotNull final Document document, @NotNull VirtualFile virtualFile) { synchronized (lock) { virtualFile.putUserData( DOCUMENT_KEY, new SoftReference<Document>(document) { @Override public Document get() { return document; } }); document.putUserData(FILE_KEY, virtualFile); } } @Override @Nullable public VirtualFile getFile(@NotNull Document document) { return document.getUserData(FILE_KEY); } @TestOnly public void dropAllUnsavedDocuments() { if (!ApplicationManager.getApplication().isUnitTestMode()) { throw new RuntimeException("This method is only for test mode!"); } ApplicationManager.getApplication().assertWriteAccessAllowed(); if (!myUnsavedDocuments.isEmpty()) { myUnsavedDocuments.clear(); fireUnsavedDocumentsDropped(); } } private void saveAllDocumentsLater() { // later because some document might have been blocked by PSI right now ApplicationManager.getApplication() .invokeLater( new Runnable() { @Override public void run() { if (ApplicationManager.getApplication().isDisposed()) { return; } final Document[] unsavedDocuments = getUnsavedDocuments(); for (Document document : unsavedDocuments) { VirtualFile file = getFile(document); if (file == null) continue; Project project = ProjectUtil.guessProjectForFile(file); if (project == null) continue; if (PsiDocumentManager.getInstance(project).isDocumentBlockedByPsi(document)) continue; saveDocument(document); } } }); } @Override public void saveAllDocuments() { ApplicationManager.getApplication().assertIsDispatchThread(); myMultiCaster.beforeAllDocumentsSaving(); if (myUnsavedDocuments.isEmpty()) return; final Map<Document, IOException> failedToSave = new HashMap<Document, IOException>(); final Set<Document> vetoed = new HashSet<Document>(); while (true) { int count = 0; for (Document document : myUnsavedDocuments) { if (failedToSave.containsKey(document)) continue; if (vetoed.contains(document)) continue; try { doSaveDocument(document); } catch (IOException e) { //noinspection ThrowableResultOfMethodCallIgnored failedToSave.put(document, e); } catch (SaveVetoException e) { vetoed.add(document); } count++; } if (count == 0) break; } if (!failedToSave.isEmpty()) { handleErrorsOnSave(failedToSave); } } @Override public void saveDocument(@NotNull final Document document) { ApplicationManager.getApplication().assertIsDispatchThread(); if (!myUnsavedDocuments.contains(document)) return; try { doSaveDocument(document); } catch (IOException e) { handleErrorsOnSave(Collections.singletonMap(document, e)); } catch (SaveVetoException ignored) { } } @Override public void saveDocumentAsIs(@NotNull Document document) { EditorSettingsExternalizable editorSettings = EditorSettingsExternalizable.getInstance(); String trailer = editorSettings.getStripTrailingSpaces(); boolean ensureEOLonEOF = editorSettings.isEnsureNewLineAtEOF(); editorSettings.setStripTrailingSpaces(EditorSettingsExternalizable.STRIP_TRAILING_SPACES_NONE); editorSettings.setEnsureNewLineAtEOF(false); try { saveDocument(document); } finally { editorSettings.setStripTrailingSpaces(trailer); editorSettings.setEnsureNewLineAtEOF(ensureEOLonEOF); } } private static class SaveVetoException extends Exception {} private void doSaveDocument(@NotNull final Document document) throws IOException, SaveVetoException { VirtualFile file = getFile(document); if (file == null || file instanceof LightVirtualFile || file.isValid() && !isFileModified(file)) { removeFromUnsaved(document); return; } if (file.isValid() && needsRefresh(file)) { file.refresh(false, false); if (!myUnsavedDocuments.contains(document)) return; } for (FileDocumentSynchronizationVetoer vetoer : Extensions.getExtensions(FileDocumentSynchronizationVetoer.EP_NAME)) { if (!vetoer.maySaveDocument(document)) { throw new SaveVetoException(); } } final AccessToken token = ApplicationManager.getApplication().acquireWriteActionLock(getClass()); try { doSaveDocumentInWriteAction(document, file); } finally { token.finish(); } } private void doSaveDocumentInWriteAction(@NotNull Document document, @NotNull VirtualFile file) throws IOException { if (!file.isValid()) { removeFromUnsaved(document); return; } if (!file.equals(getFile(document))) { registerDocument(document, file); } if (!isSaveNeeded(document, file)) { if (document instanceof DocumentEx) { ((DocumentEx) document).setModificationStamp(file.getModificationStamp()); } removeFromUnsaved(document); updateModifiedProperty(file); return; } myMultiCaster.beforeDocumentSaving(document); LOG.assertTrue(file.isValid()); String text = document.getText(); String lineSeparator = getLineSeparator(document, file); if (!lineSeparator.equals("\n")) { text = StringUtil.convertLineSeparators(text, lineSeparator); } Project project = ProjectLocator.getInstance().guessProjectForFile(file); LoadTextUtil.write(project, file, this, text, document.getModificationStamp()); myUnsavedDocuments.remove(document); LOG.assertTrue(!myUnsavedDocuments.contains(document)); myTrailingSpacesStripper.clearLineModificationFlags(document); } private static void updateModifiedProperty(@NotNull VirtualFile file) { for (Project project : ProjectManager.getInstance().getOpenProjects()) { FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); for (FileEditor editor : fileEditorManager.getAllEditors(file)) { if (editor instanceof TextEditorImpl) { ((TextEditorImpl) editor).updateModifiedProperty(); } } } } private void removeFromUnsaved(@NotNull Document document) { myUnsavedDocuments.remove(document); fireUnsavedDocumentsDropped(); LOG.assertTrue(!myUnsavedDocuments.contains(document)); } private static boolean isSaveNeeded(@NotNull Document document, @NotNull VirtualFile file) throws IOException { if (file.getFileType().isBinary() || document.getTextLength() > 1000 * 1000) { // don't compare if the file is too big return true; } byte[] bytes = file.contentsToByteArray(); CharSequence loaded = LoadTextUtil.getTextByBinaryPresentation(bytes, file, false, false); return !Comparing.equal(document.getCharsSequence(), loaded); } private static boolean needsRefresh(final VirtualFile file) { final VirtualFileSystem fs = file.getFileSystem(); return fs instanceof NewVirtualFileSystem && file.getTimeStamp() != ((NewVirtualFileSystem) fs).getTimeStamp(file); } @NotNull public static String getLineSeparator(@NotNull Document document, @NotNull VirtualFile file) { String lineSeparator = LoadTextUtil.getDetectedLineSeparator(file); if (lineSeparator == null) { lineSeparator = document.getUserData(LINE_SEPARATOR_KEY); assert lineSeparator != null : document; } return lineSeparator; } @Override @NotNull public String getLineSeparator(@Nullable VirtualFile file, @Nullable Project project) { String lineSeparator = file == null ? null : LoadTextUtil.getDetectedLineSeparator(file); if (lineSeparator == null) { CodeStyleFacade settingsManager = project == null ? CodeStyleFacade.getInstance() : CodeStyleFacade.getInstance(project); lineSeparator = settingsManager.getLineSeparator(); } return lineSeparator; } @Override public boolean requestWriting(@NotNull Document document, Project project) { final VirtualFile file = getInstance().getFile(document); if (project != null && file != null && file.isValid()) { return !file.getFileType().isBinary() && ReadonlyStatusHandler.ensureFilesWritable(project, file); } if (document.isWritable()) { return true; } document.fireReadOnlyModificationAttempt(); return false; } @Override public void reloadFiles(final VirtualFile... files) { for (VirtualFile file : files) { if (file.exists()) { final Document doc = getCachedDocument(file); if (doc != null) { reloadFromDisk(doc); } } } } @Override @NotNull public Document[] getUnsavedDocuments() { if (myUnsavedDocuments.isEmpty()) { return Document.EMPTY_ARRAY; } List<Document> list = new ArrayList<Document>(myUnsavedDocuments); return list.toArray(new Document[list.size()]); } @Override public boolean isDocumentUnsaved(@NotNull Document document) { return myUnsavedDocuments.contains(document); } @Override public boolean isFileModified(@NotNull VirtualFile file) { final Document doc = getCachedDocument(file); return doc != null && isDocumentUnsaved(doc) && doc.getModificationStamp() != file.getModificationStamp(); } @Override public void propertyChanged(final VirtualFilePropertyEvent event) { if (VirtualFile.PROP_WRITABLE.equals(event.getPropertyName())) { final VirtualFile file = event.getFile(); final Document document = getCachedDocument(file); if (document == null) return; ApplicationManager.getApplication() .runWriteAction( new ExternalChangeAction() { @Override public void run() { document.setReadOnly(!event.getFile().isWritable()); } }); // myUnsavedDocuments.remove(document); //? } } private static boolean isBinaryWithDecompiler(VirtualFile file) { final FileType ft = file.getFileType(); return ft.isBinary() && BinaryFileTypeDecompilers.INSTANCE.forFileType(ft) != null; } private static boolean isBinaryWithoutDecompiler(VirtualFile file) { final FileType ft = file.getFileType(); return ft.isBinary() && BinaryFileTypeDecompilers.INSTANCE.forFileType(ft) == null; } @Override public void contentsChanged(VirtualFileEvent event) { if (event.isFromSave()) return; final VirtualFile file = event.getFile(); final Document document = getCachedDocument(file); if (document == null) { myMultiCaster.fileWithNoDocumentChanged(file); return; } if (isBinaryWithDecompiler(file)) { myMultiCaster.fileWithNoDocumentChanged( file); // This will generate PSI event at FileManagerImpl } long documentStamp = document.getModificationStamp(); long oldFileStamp = event.getOldModificationStamp(); if (documentStamp != oldFileStamp) { LOG.info("reload from disk?"); LOG.info(" documentStamp:" + documentStamp); LOG.info(" oldFileStamp:" + oldFileStamp); if (file.isValid() && askReloadFromDisk(file, document)) { reloadFromDisk(document); } } else { reloadFromDisk(document); } } @Override public void reloadFromDisk(@NotNull final Document document) { ApplicationManager.getApplication().assertIsDispatchThread(); final VirtualFile file = getFile(document); assert file != null; if (!fireBeforeFileContentReload(file, document)) { return; } final Project project = ProjectLocator.getInstance().guessProjectForFile(file); CommandProcessor.getInstance() .executeCommand( project, new Runnable() { @Override public void run() { ApplicationManager.getApplication() .runWriteAction( new ExternalChangeAction.ExternalDocumentChange(document, project) { @Override public void run() { boolean wasWritable = document.isWritable(); DocumentEx documentEx = (DocumentEx) document; documentEx.setReadOnly(false); LoadTextUtil.setCharsetWasDetectedFromBytes(file, null); documentEx.replaceText( LoadTextUtil.loadText(file), file.getModificationStamp()); documentEx.setReadOnly(!wasWritable); } }); } }, UIBundle.message("file.cache.conflict.action"), null, UndoConfirmationPolicy.REQUEST_CONFIRMATION); myUnsavedDocuments.remove(document); myMultiCaster.fileContentReloaded(file, document); } protected boolean askReloadFromDisk(final VirtualFile file, final Document document) { ApplicationManager.getApplication().assertIsDispatchThread(); if (!isDocumentUnsaved(document)) return true; String message = UIBundle.message("file.cache.conflict.message.text", file.getPresentableUrl()); if (ApplicationManager.getApplication().isUnitTestMode()) throw new RuntimeException(message); final DialogBuilder builder = new DialogBuilder((Project) null); builder.setCenterPanel(new JLabel(message, Messages.getQuestionIcon(), SwingConstants.CENTER)); builder.addOkAction().setText(UIBundle.message("file.cache.conflict.load.fs.changes.button")); builder .addCancelAction() .setText(UIBundle.message("file.cache.conflict.keep.memory.changes.button")); builder.addAction( new AbstractAction(UIBundle.message("file.cache.conflict.show.difference.button")) { @Override public void actionPerformed(ActionEvent e) { String title = UIBundle.message( "file.cache.conflict.for.file.dialog.title", file.getPresentableUrl()); final ProjectEx project = (ProjectEx) ProjectLocator.getInstance().guessProjectForFile(file); SimpleDiffRequest request = new SimpleDiffRequest(project, title); FileType fileType = file.getFileType(); String fsContent = LoadTextUtil.loadText(file).toString(); request.setContents( new SimpleContent(fsContent, fileType), new DocumentContent(project, document, fileType)); request.setContentTitles( UIBundle.message("file.cache.conflict.diff.content.file.system.content"), UIBundle.message("file.cache.conflict.diff.content.memory.content")); DialogBuilder diffBuilder = new DialogBuilder(project); DiffPanelImpl diffPanel = (DiffPanelImpl) DiffManager.getInstance() .createDiffPanel(diffBuilder.getWindow(), project, diffBuilder, null); diffPanel.getOptions().setShowSourcePolicy(DiffPanelOptions.ShowSourcePolicy.DONT_SHOW); diffBuilder.setCenterPanel(diffPanel.getComponent()); diffBuilder.setDimensionServiceKey("FileDocumentManager.FileCacheConflict"); diffPanel.setDiffRequest(request); diffBuilder .addOkAction() .setText(UIBundle.message("file.cache.conflict.save.changes.button")); diffBuilder.addCancelAction(); diffBuilder.setTitle(title); if (diffBuilder.show() == DialogWrapper.OK_EXIT_CODE) { builder.getDialogWrapper().close(DialogWrapper.CANCEL_EXIT_CODE); } } }); builder.setTitle(UIBundle.message("file.cache.conflict.dialog.title")); builder.setButtonsAlignment(SwingConstants.CENTER); builder.setHelpId("reference.dialogs.fileCacheConflict"); return builder.show() == 0; } @Override public void fileCreated(VirtualFileEvent event) {} @Override public void fileDeleted(VirtualFileEvent event) { Document doc = getCachedDocument(event.getFile()); if (doc != null) { myTrailingSpacesStripper.documentDeleted(doc); } } @Override public void fileMoved(VirtualFileMoveEvent event) {} @Override public void fileCopied(VirtualFileCopyEvent event) { fileCreated(event); } @Override public void beforePropertyChange(VirtualFilePropertyEvent event) {} @Override public void beforeContentsChange(VirtualFileEvent event) {} @Override public void beforeFileDeletion(VirtualFileEvent event) { /* if (!event.isFromRefresh()) { VirtualFile file = event.getFile(); if (file.getFileSystem() instanceof TempFileSystem) { return; //hack: this fs fails in getChildren during beforeFileDeletion } VfsUtilCore.visitChildrenRecursively(file, new VirtualFileVisitor() { @Override public boolean visitFile(@NotNull VirtualFile file) { Document document = getCachedDocument(file); if (document != null) { removeFromUnsaved(document); } return true; } }); } */ } @Override public void beforeFileMovement(VirtualFileMoveEvent event) {} @Override public void projectOpened(Project project) {} @Override public boolean canCloseProject(Project project) { if (!myUnsavedDocuments.isEmpty()) { myOnClose = true; try { saveAllDocuments(); } finally { myOnClose = false; } } return myUnsavedDocuments.isEmpty(); } @Override public void projectClosed(Project project) {} @Override public void projectClosing(Project project) {} private void fireUnsavedDocumentsDropped() { myMultiCaster.unsavedDocumentsDropped(); } private boolean fireBeforeFileContentReload(final VirtualFile file, @NotNull Document document) { for (FileDocumentSynchronizationVetoer vetoer : Extensions.getExtensions(FileDocumentSynchronizationVetoer.EP_NAME)) { try { if (!vetoer.mayReloadFileContent(file, document)) { return false; } } catch (Exception e) { LOG.error(e); } } myMultiCaster.beforeFileContentReload(file, document); return true; } @NotNull protected FileDocumentManagerListener[] getListeners() { return FileDocumentManagerListener.EP_NAME.getExtensions(); } protected void handleErrorsOnSave(@NotNull Map<Document, IOException> failures) { for (IOException exception : failures.values()) { LOG.warn(exception); } final String text = StringUtil.join( failures.values(), new Function<IOException, String>() { @Override public String fun(IOException e) { return e.getMessage(); } }, "\n"); final DialogWrapper dialog = new DialogWrapper(null) { { init(); setTitle(UIBundle.message("cannot.save.files.dialog.title")); } @Override protected void createDefaultActions() { super.createDefaultActions(); myOKAction.putValue( Action.NAME, UIBundle.message( myOnClose ? "cannot.save.files.dialog.ignore.changes" : "cannot.save.files.dialog.revert.changes")); myOKAction.putValue(DEFAULT_ACTION, null); if (!myOnClose) { myCancelAction.putValue(Action.NAME, CommonBundle.getCloseButtonText()); } } @Override protected JComponent createCenterPanel() { final JPanel panel = new JPanel(new BorderLayout(0, 5)); panel.add( new JLabel(UIBundle.message("cannot.save.files.dialog.message")), BorderLayout.NORTH); final JTextPane area = new JTextPane(); area.setText(text); area.setEditable(false); area.setMinimumSize(new Dimension(area.getMinimumSize().width, 50)); panel.add( new JBScrollPane( area, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.CENTER); return panel; } }; dialog.show(); if (dialog.isOK()) { for (Document document : failures.keySet()) { reloadFromDisk(document); } } } }
public class VariableAccessFromInnerClassFix implements IntentionAction { private static final Logger LOG = Logger.getInstance( "#com.intellij.codeInsight.daemon.impl.quickfix.VariableAccessFromInnerClassFix"); private final PsiVariable myVariable; private final PsiClass myClass; private final int myFixType; private static final int MAKE_FINAL = 0; private static final int MAKE_ARRAY = 1; private static final int COPY_TO_FINAL = 2; private static final Key<Map<PsiVariable, Boolean>>[] VARS = new Key[] { Key.create("VARS_TO_MAKE_FINAL"), Key.create("VARS_TO_TRANSFORM"), Key.create("???") }; public VariableAccessFromInnerClassFix(PsiVariable variable, PsiClass aClass) { myVariable = variable; myClass = aClass; myFixType = getQuickFixType(variable); if (myFixType == -1) return; getVariablesToFix().add(variable); } @Override @NotNull public String getText() { @NonNls String message; switch (myFixType) { case MAKE_FINAL: message = "make.final.text"; break; case MAKE_ARRAY: message = "make.final.transform.to.one.element.array"; break; case COPY_TO_FINAL: return QuickFixBundle.message("make.final.copy.to.temp", myVariable.getName()); default: return ""; } Collection<PsiVariable> vars = getVariablesToFix(); String varNames = vars.size() == 1 ? "'" + myVariable.getName() + "'" : "variables"; return QuickFixBundle.message(message, varNames); } @Override @NotNull public String getFamilyName() { return QuickFixBundle.message("make.final.family"); } @Override public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { return myClass != null && myClass.isValid() && myClass.getManager().isInProject(myClass) && myVariable != null && myVariable.isValid() && myFixType != -1 && !getVariablesToFix().isEmpty() && !inOwnInitializer(myVariable, myClass); } private static boolean inOwnInitializer(PsiVariable variable, PsiClass aClass) { return PsiTreeUtil.isAncestor(variable, aClass, false); } @Override public void invoke(@NotNull Project project, Editor editor, PsiFile file) { if (!FileModificationService.getInstance().preparePsiElementsForWrite(myClass, myVariable)) return; try { switch (myFixType) { case MAKE_FINAL: makeFinal(); break; case MAKE_ARRAY: makeArray(); break; case COPY_TO_FINAL: copyToFinal(); break; } } catch (IncorrectOperationException e) { LOG.error(e); } finally { getVariablesToFix().clear(); } } private void makeArray() { for (PsiVariable var : getVariablesToFix()) { makeArray(var); } } @NotNull private Collection<PsiVariable> getVariablesToFix() { Map<PsiVariable, Boolean> vars = myClass.getUserData(VARS[myFixType]); if (vars == null) myClass.putUserData( VARS[myFixType], vars = new ConcurrentWeakHashMap<PsiVariable, Boolean>(1)); final Map<PsiVariable, Boolean> finalVars = vars; return new AbstractCollection<PsiVariable>() { @Override public boolean add(PsiVariable psiVariable) { return finalVars.put(psiVariable, Boolean.TRUE) == null; } @Override public Iterator<PsiVariable> iterator() { return finalVars.keySet().iterator(); } @Override public int size() { return finalVars.size(); } }; } private void makeFinal() { for (PsiVariable var : getVariablesToFix()) { if (var.isValid()) { PsiUtil.setModifierProperty(var, PsiModifier.FINAL, true); } } } private void makeArray(PsiVariable variable) throws IncorrectOperationException { PsiType type = variable.getType(); PsiElementFactory factory = JavaPsiFacade.getInstance(myClass.getProject()).getElementFactory(); PsiType newType = type.createArrayType(); PsiDeclarationStatement variableDeclarationStatement; PsiExpression initializer = variable.getInitializer(); if (initializer == null) { String expression = "[1]"; while (type instanceof PsiArrayType) { expression += "[1]"; type = ((PsiArrayType) type).getComponentType(); } PsiExpression init = factory.createExpressionFromText("new " + type.getCanonicalText() + expression, variable); variableDeclarationStatement = factory.createVariableDeclarationStatement(variable.getName(), newType, init); } else { PsiExpression init = factory.createExpressionFromText("{ " + initializer.getText() + " }", variable); variableDeclarationStatement = factory.createVariableDeclarationStatement(variable.getName(), newType, init); } PsiVariable newVariable = (PsiVariable) variableDeclarationStatement.getDeclaredElements()[0]; PsiUtil.setModifierProperty(newVariable, PsiModifier.FINAL, true); PsiElement newExpression = factory.createExpressionFromText(variable.getName() + "[0]", variable); PsiElement outerCodeBlock = PsiUtil.getVariableCodeBlock(variable, null); if (outerCodeBlock == null) return; List<PsiReferenceExpression> outerReferences = new ArrayList<PsiReferenceExpression>(); collectReferences(outerCodeBlock, variable, outerReferences); replaceReferences(outerReferences, newExpression); variable.replace(newVariable); } private void copyToFinal() throws IncorrectOperationException { PsiManager psiManager = myClass.getManager(); PsiElementFactory factory = JavaPsiFacade.getInstance(psiManager.getProject()).getElementFactory(); PsiExpression initializer = factory.createExpressionFromText(myVariable.getName(), myClass); String newName = suggestNewName(psiManager.getProject(), myVariable); PsiType type = myVariable.getType(); PsiDeclarationStatement copyDecl = factory.createVariableDeclarationStatement(newName, type, initializer); PsiVariable newVariable = (PsiVariable) copyDecl.getDeclaredElements()[0]; PsiUtil.setModifierProperty(newVariable, PsiModifier.FINAL, true); PsiElement statement = getStatementToInsertBefore(); if (statement == null) return; statement.getParent().addBefore(copyDecl, statement); PsiExpression newExpression = factory.createExpressionFromText(newName, myVariable); replaceReferences(myClass, myVariable, newExpression); } private PsiElement getStatementToInsertBefore() { PsiElement declarationScope = myVariable instanceof PsiParameter ? ((PsiParameter) myVariable).getDeclarationScope() : PsiUtil.getVariableCodeBlock(myVariable, null); if (declarationScope == null) return null; PsiElement statement = myClass; nextInnerClass: do { statement = PsiUtil.getEnclosingStatement(statement); if (statement == null || statement.getParent() == null) { return null; } PsiElement element = statement; while (element != declarationScope && !(element instanceof PsiFile)) { if (element instanceof PsiClass) { statement = statement.getParent(); continue nextInnerClass; } element = element.getParent(); } return statement; } while (true); } private static String suggestNewName(Project project, PsiVariable variable) { // new name should not conflict with another variable at the variable declaration level and // usage level String name = variable.getName(); // trim last digit to suggest variable names like i1,i2, i3... if (name.length() > 1 && Character.isDigit(name.charAt(name.length() - 1))) { name = name.substring(0, name.length() - 1); } name = "final" + StringUtil.capitalize(StringUtil.trimStart(name, "final")); return JavaCodeStyleManager.getInstance(project) .suggestUniqueVariableName(name, variable, true); } private static void replaceReferences( PsiElement context, final PsiVariable variable, final PsiElement newExpression) { context.accept( new JavaRecursiveElementVisitor() { @Override public void visitReferenceExpression(PsiReferenceExpression expression) { if (expression.resolve() == variable) try { expression.replace(newExpression); } catch (IncorrectOperationException e) { LOG.error(e); } super.visitReferenceExpression(expression); } }); } private static void replaceReferences( List<PsiReferenceExpression> references, PsiElement newExpression) throws IncorrectOperationException { for (PsiReferenceExpression reference : references) { reference.replace(newExpression); } } private static void collectReferences( PsiElement context, final PsiVariable variable, final List<PsiReferenceExpression> references) { context.accept( new JavaRecursiveElementWalkingVisitor() { @Override public void visitReferenceExpression(PsiReferenceExpression expression) { if (expression.resolve() == variable) references.add(expression); super.visitReferenceExpression(expression); } }); } private static int getQuickFixType(PsiVariable variable) { PsiElement outerCodeBlock = PsiUtil.getVariableCodeBlock(variable, null); if (outerCodeBlock == null) return -1; List<PsiReferenceExpression> outerReferences = new ArrayList<PsiReferenceExpression>(); collectReferences(outerCodeBlock, variable, outerReferences); int type = MAKE_FINAL; for (PsiReferenceExpression expression : outerReferences) { // if it happens that variable referenced from another inner class, make sure it can be make // final from there PsiClass innerClass = HighlightControlFlowUtil.getInnerClassVariableReferencedFrom(variable, expression); if (innerClass != null) { int thisType = MAKE_FINAL; if (writtenInside(variable, innerClass)) { // cannot make parameter array if (variable instanceof PsiParameter) return -1; thisType = MAKE_ARRAY; } if (thisType == MAKE_FINAL && !canBeFinal(variable, outerReferences)) { thisType = COPY_TO_FINAL; } type = Math.max(type, thisType); } } return type; } private static boolean canBeFinal(PsiVariable variable, List<PsiReferenceExpression> references) { // if there is at least one assignment to this variable, it cannot be final Map<PsiElement, Collection<PsiReferenceExpression>> uninitializedVarProblems = new THashMap<PsiElement, Collection<PsiReferenceExpression>>(); Map<PsiElement, Collection<ControlFlowUtil.VariableInfo>> finalVarProblems = new THashMap<PsiElement, Collection<ControlFlowUtil.VariableInfo>>(); for (PsiReferenceExpression expression : references) { if (ControlFlowUtil.isVariableAssignedInLoop(expression, variable)) return false; HighlightInfo highlightInfo = HighlightControlFlowUtil.checkVariableInitializedBeforeUsage( expression, variable, uninitializedVarProblems); if (highlightInfo != null) return false; highlightInfo = HighlightControlFlowUtil.checkFinalVariableMightAlreadyHaveBeenAssignedTo( variable, expression, finalVarProblems); if (highlightInfo != null) return false; if (variable instanceof PsiParameter && PsiUtil.isAccessedForWriting(expression)) return false; } return true; } private static boolean writtenInside(PsiVariable variable, PsiElement element) { if (element instanceof PsiAssignmentExpression) { PsiAssignmentExpression assignmentExpression = (PsiAssignmentExpression) element; PsiExpression lExpression = assignmentExpression.getLExpression(); if (lExpression instanceof PsiReferenceExpression && ((PsiReferenceExpression) lExpression).resolve() == variable) return true; } else if (PsiUtil.isIncrementDecrementOperation(element)) { PsiElement operand = element instanceof PsiPostfixExpression ? ((PsiPostfixExpression) element).getOperand() : ((PsiPrefixExpression) element).getOperand(); if (operand instanceof PsiReferenceExpression && ((PsiReferenceExpression) operand).resolve() == variable) return true; } PsiElement[] children = element.getChildren(); for (PsiElement child : children) { if (writtenInside(variable, child)) return true; } return false; } @Override public boolean startInWriteAction() { return true; } }
class PsiChangeHandler extends PsiTreeChangeAdapter implements Disposable { private static final ExtensionPointName<ChangeLocalityDetector> EP_NAME = ExtensionPointName.create("com.intellij.daemon.changeLocalityDetector"); private /*NOT STATIC!!!*/ final Key<Boolean> UPDATE_ON_COMMIT_ENGAGED = Key.create("UPDATE_ON_COMMIT_ENGAGED"); private final Project myProject; private final Map<Document, List<Pair<PsiElement, Boolean>>> changedElements = new WeakHashMap<>(); private final FileStatusMap myFileStatusMap; PsiChangeHandler( @NotNull Project project, @NotNull final PsiDocumentManagerImpl documentManager, @NotNull EditorFactory editorFactory, @NotNull MessageBusConnection connection, @NotNull FileStatusMap fileStatusMap) { myProject = project; myFileStatusMap = fileStatusMap; editorFactory .getEventMulticaster() .addDocumentListener( new DocumentAdapter() { @Override public void beforeDocumentChange(DocumentEvent e) { final Document document = e.getDocument(); if (documentManager.getSynchronizer().isInSynchronization(document)) return; if (documentManager.getCachedPsiFile(document) == null) return; if (document.getUserData(UPDATE_ON_COMMIT_ENGAGED) == null) { document.putUserData(UPDATE_ON_COMMIT_ENGAGED, Boolean.TRUE); PsiDocumentManagerBase.addRunOnCommit( document, () -> { if (document.getUserData(UPDATE_ON_COMMIT_ENGAGED) != null) { updateChangesForDocument(document); document.putUserData(UPDATE_ON_COMMIT_ENGAGED, null); } }); } } }, this); connection.subscribe( PsiDocumentTransactionListener.TOPIC, new PsiDocumentTransactionListener() { @Override public void transactionStarted( @NotNull final Document doc, @NotNull final PsiFile file) {} @Override public void transactionCompleted( @NotNull final Document document, @NotNull final PsiFile file) { updateChangesForDocument(document); document.putUserData( UPDATE_ON_COMMIT_ENGAGED, null); // ensure we don't call updateChangesForDocument() twice which can lead to // whole file re-highlight } }); } @Override public void dispose() {} private void updateChangesForDocument(@NotNull final Document document) { ApplicationManager.getApplication().assertIsDispatchThread(); if (DaemonListeners.isUnderIgnoredAction(null) || myProject.isDisposed()) return; List<Pair<PsiElement, Boolean>> toUpdate = changedElements.get(document); if (toUpdate == null) { // The document has been changed, but psi hasn't // We may still need to rehighlight the file if there were changes inside highlighted ranges. if (UpdateHighlightersUtil.isWhitespaceOptimizationAllowed(document)) return; // don't create PSI for files in other projects PsiElement file = PsiDocumentManager.getInstance(myProject).getCachedPsiFile(document); if (file == null) return; toUpdate = Collections.singletonList(Pair.create(file, true)); } Application application = ApplicationManager.getApplication(); final Editor editor = FileEditorManager.getInstance(myProject).getSelectedTextEditor(); if (editor != null && !application.isUnitTestMode()) { application.invokeLater( () -> { if (!editor.isDisposed()) { EditorMarkupModel markupModel = (EditorMarkupModel) editor.getMarkupModel(); PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(editor.getDocument()); TrafficLightRenderer.setOrRefreshErrorStripeRenderer( markupModel, myProject, editor.getDocument(), file); } }, ModalityState.stateForComponent(editor.getComponent()), myProject.getDisposed()); } for (Pair<PsiElement, Boolean> changedElement : toUpdate) { PsiElement element = changedElement.getFirst(); Boolean whiteSpaceOptimizationAllowed = changedElement.getSecond(); updateByChange(element, document, whiteSpaceOptimizationAllowed); } changedElements.remove(document); } @Override public void childAdded(@NotNull PsiTreeChangeEvent event) { queueElement(event.getParent(), true, event); } @Override public void childRemoved(@NotNull PsiTreeChangeEvent event) { queueElement(event.getParent(), true, event); } @Override public void childReplaced(@NotNull PsiTreeChangeEvent event) { queueElement(event.getNewChild(), typesEqual(event.getNewChild(), event.getOldChild()), event); } private static boolean typesEqual(final PsiElement newChild, final PsiElement oldChild) { return newChild != null && oldChild != null && newChild.getClass() == oldChild.getClass(); } @Override public void childrenChanged(@NotNull PsiTreeChangeEvent event) { if (((PsiTreeChangeEventImpl) event).isGenericChange()) { return; } queueElement(event.getParent(), true, event); } @Override public void beforeChildMovement(@NotNull PsiTreeChangeEvent event) { queueElement(event.getOldParent(), true, event); queueElement(event.getNewParent(), true, event); } @Override public void beforeChildrenChange(@NotNull PsiTreeChangeEvent event) { // this event sent always before every PSI change, even not significant one (like after quick // typing/backspacing char) // mark file dirty just in case PsiFile psiFile = event.getFile(); if (psiFile != null) { myFileStatusMap.markFileScopeDirtyDefensively(psiFile, event); } } @Override public void propertyChanged(@NotNull PsiTreeChangeEvent event) { String propertyName = event.getPropertyName(); if (!propertyName.equals(PsiTreeChangeEvent.PROP_WRITABLE)) { Object oldValue = event.getOldValue(); if (oldValue instanceof VirtualFile && shouldBeIgnored((VirtualFile) oldValue)) { // ignore workspace.xml return; } myFileStatusMap.markAllFilesDirty(event); } } private void queueElement( @NotNull PsiElement child, final boolean whitespaceOptimizationAllowed, @NotNull PsiTreeChangeEvent event) { ApplicationManager.getApplication().assertIsDispatchThread(); PsiFile file = event.getFile(); if (file == null) file = child.getContainingFile(); if (file == null) { myFileStatusMap.markAllFilesDirty(child); return; } if (!child.isValid()) return; PsiDocumentManagerImpl pdm = (PsiDocumentManagerImpl) PsiDocumentManager.getInstance(myProject); Document document = pdm.getCachedDocument(file); if (document != null) { if (pdm.getSynchronizer().getTransaction(document) == null) { // content reload, language level change or some other big change myFileStatusMap.markAllFilesDirty(child); return; } List<Pair<PsiElement, Boolean>> toUpdate = changedElements.get(document); if (toUpdate == null) { toUpdate = new SmartList<>(); changedElements.put(document, toUpdate); } toUpdate.add(Pair.create(child, whitespaceOptimizationAllowed)); } } private void updateByChange( @NotNull PsiElement child, @NotNull final Document document, final boolean whitespaceOptimizationAllowed) { ApplicationManager.getApplication().assertIsDispatchThread(); final PsiFile file; try { file = child.getContainingFile(); } catch (PsiInvalidElementAccessException e) { myFileStatusMap.markAllFilesDirty(e); return; } if (file == null || file instanceof PsiCompiledElement) { myFileStatusMap.markAllFilesDirty(child); return; } VirtualFile virtualFile = file.getVirtualFile(); if (virtualFile != null && shouldBeIgnored(virtualFile)) { // ignore workspace.xml return; } int fileLength = file.getTextLength(); if (!file.getViewProvider().isPhysical()) { myFileStatusMap.markFileScopeDirty( document, new TextRange(0, fileLength), fileLength, "Non-physical file update: " + file); return; } PsiElement element = whitespaceOptimizationAllowed && UpdateHighlightersUtil.isWhitespaceOptimizationAllowed(document) ? child : child.getParent(); while (true) { if (element == null || element instanceof PsiFile || element instanceof PsiDirectory) { myFileStatusMap.markAllFilesDirty("Top element: " + element); return; } final PsiElement scope = getChangeHighlightingScope(element); if (scope != null) { myFileStatusMap.markFileScopeDirty( document, scope.getTextRange(), fileLength, "Scope: " + scope); return; } element = element.getParent(); } } private boolean shouldBeIgnored(@NotNull VirtualFile virtualFile) { return ProjectCoreUtil.isProjectOrWorkspaceFile(virtualFile) || ProjectRootManager.getInstance(myProject).getFileIndex().isExcluded(virtualFile); } @Nullable private static PsiElement getChangeHighlightingScope(@NotNull PsiElement element) { DefaultChangeLocalityDetector defaultDetector = null; for (ChangeLocalityDetector detector : Extensions.getExtensions(EP_NAME)) { if (detector instanceof DefaultChangeLocalityDetector) { // run default detector last assert defaultDetector == null : defaultDetector; defaultDetector = (DefaultChangeLocalityDetector) detector; continue; } final PsiElement scope = detector.getChangeHighlightingDirtyScopeFor(element); if (scope != null) return scope; } assert defaultDetector != null : "com.intellij.codeInsight.daemon.impl.DefaultChangeLocalityDetector is unregistered"; return defaultDetector.getChangeHighlightingDirtyScopeFor(element); } }
/** @author Mike */ public class XmlDocumentImpl extends XmlElementImpl implements XmlDocument { private static final Key<Boolean> AUTO_GENERATED = Key.create("auto-generated xml schema"); public static boolean isAutoGeneratedSchema(XmlFile file) { return file.getUserData(AUTO_GENERATED) != null; } private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.xml.XmlDocumentImpl"); private volatile XmlProlog myProlog; private volatile XmlTag myRootTag; private volatile long myExtResourcesModCount = -1; public XmlDocumentImpl() { this(XmlElementType.XML_DOCUMENT); } protected XmlDocumentImpl(IElementType type) { super(type); } @Override public void accept(@NotNull PsiElementVisitor visitor) { if (visitor instanceof XmlElementVisitor) { ((XmlElementVisitor) visitor).visitXmlDocument(this); } else { visitor.visitElement(this); } } @Override public int getChildRole(ASTNode child) { LOG.assertTrue(child.getTreeParent() == this); IElementType i = child.getElementType(); if (i == XmlElementType.XML_PROLOG) { return XmlChildRole.XML_PROLOG; } else if (i == XmlElementType.XML_TAG) { return XmlChildRole.XML_TAG; } else { return ChildRoleBase.NONE; } } @Override public XmlProlog getProlog() { XmlProlog prolog = myProlog; if (prolog == null) { synchronized (PsiLock.LOCK) { prolog = myProlog; if (prolog == null) { prolog = (XmlProlog) findElementByTokenType(XmlElementType.XML_PROLOG); myProlog = prolog; } } } return myProlog; } @Override public XmlTag getRootTag() { XmlTag rootTag = myRootTag; if (rootTag == null) { synchronized (PsiLock.LOCK) { rootTag = myRootTag; if (rootTag == null) { rootTag = (XmlTag) findElementByTokenType(XmlElementType.XML_TAG); myRootTag = rootTag; } } } return myRootTag; } @Override @SuppressWarnings("ConstantConditions") public XmlNSDescriptor getRootTagNSDescriptor() { XmlTag rootTag = getRootTag(); return rootTag != null ? rootTag.getNSDescriptor(rootTag.getNamespace(), false) : null; } private ConcurrentMap<String, CachedValue<XmlNSDescriptor>> myDefaultDescriptorsCacheStrict = ContainerUtil.newConcurrentMap(); private ConcurrentMap<String, CachedValue<XmlNSDescriptor>> myDefaultDescriptorsCacheNotStrict = ContainerUtil.newConcurrentMap(); @Override public void clearCaches() { myDefaultDescriptorsCacheStrict.clear(); myDefaultDescriptorsCacheNotStrict.clear(); myProlog = null; myRootTag = null; super.clearCaches(); } @Override public XmlNSDescriptor getDefaultNSDescriptor(final String namespace, final boolean strict) { long curExtResourcesModCount = ExternalResourceManagerEx.getInstanceEx().getModificationCount(getProject()); if (myExtResourcesModCount != curExtResourcesModCount) { myDefaultDescriptorsCacheNotStrict.clear(); myDefaultDescriptorsCacheStrict.clear(); myExtResourcesModCount = curExtResourcesModCount; } final ConcurrentMap<String, CachedValue<XmlNSDescriptor>> defaultDescriptorsCache; if (strict) { defaultDescriptorsCache = myDefaultDescriptorsCacheStrict; } else { defaultDescriptorsCache = myDefaultDescriptorsCacheNotStrict; } CachedValue<XmlNSDescriptor> cachedValue = defaultDescriptorsCache.get(namespace); if (cachedValue == null) { defaultDescriptorsCache.put( namespace, cachedValue = new PsiCachedValueImpl<XmlNSDescriptor>( getManager(), new CachedValueProvider<XmlNSDescriptor>() { @Override public Result<XmlNSDescriptor> compute() { final XmlNSDescriptor defaultNSDescriptorInner = getDefaultNSDescriptorInner(namespace, strict); if (isGeneratedFromDtd(defaultNSDescriptorInner)) { return new Result<XmlNSDescriptor>( defaultNSDescriptorInner, XmlDocumentImpl.this, ExternalResourceManager.getInstance()); } return new Result<XmlNSDescriptor>( defaultNSDescriptorInner, defaultNSDescriptorInner != null ? defaultNSDescriptorInner.getDependences() : ExternalResourceManager.getInstance()); } })); } return cachedValue.getValue(); } private boolean isGeneratedFromDtd(XmlNSDescriptor defaultNSDescriptorInner) { if (defaultNSDescriptorInner == null) { return false; } XmlFile descriptorFile = defaultNSDescriptorInner.getDescriptorFile(); if (descriptorFile == null) { return false; } @NonNls String otherName = XmlUtil.getContainingFile(this).getName() + ".dtd"; return descriptorFile.getName().equals(otherName); } private XmlNSDescriptor getDefaultNSDescriptorInner( final String namespace, final boolean strict) { final XmlFile containingFile = XmlUtil.getContainingFile(this); if (containingFile == null) return null; final XmlProlog prolog = getProlog(); final XmlDoctype doctype = prolog != null ? prolog.getDoctype() : null; boolean dtdUriFromDocTypeIsNamespace = false; if (XmlUtil.HTML_URI.equals(namespace)) { XmlNSDescriptor nsDescriptor = doctype != null ? getNsDescriptorFormDocType(doctype, containingFile, true) : null; if (doctype != null) { LOG.debug( "Descriptor from doctype " + doctype + " is " + (nsDescriptor != null ? nsDescriptor.getClass().getCanonicalName() : "NULL")); } if (nsDescriptor == null) { String htmlns = ExternalResourceManagerEx.getInstanceEx().getDefaultHtmlDoctype(getProject()); if (htmlns.isEmpty()) { htmlns = Html5SchemaProvider.getHtml5SchemaLocation(); } nsDescriptor = getDefaultNSDescriptor(htmlns, false); } return new HtmlNSDescriptorImpl(nsDescriptor); } else if (XmlUtil.XHTML_URI.equals(namespace)) { String xhtmlNamespace = XmlUtil.getDefaultXhtmlNamespace(getProject()); if (xhtmlNamespace == null || xhtmlNamespace.isEmpty()) { xhtmlNamespace = Html5SchemaProvider.getXhtml5SchemaLocation(); } return getDefaultNSDescriptor(xhtmlNamespace, false); } else if (namespace != null && namespace != XmlUtil.EMPTY_URI) { if (doctype == null || !namespace.equals(XmlUtil.getDtdUri(doctype))) { boolean documentIsSchemaThatDefinesNs = namespace.equals(XmlUtil.getTargetSchemaNsFromTag(getRootTag())); final XmlFile xmlFile = documentIsSchemaThatDefinesNs ? containingFile : XmlUtil.findNamespace(containingFile, namespace); if (xmlFile != null) { final XmlDocument document = xmlFile.getDocument(); if (document != null) { return (XmlNSDescriptor) document.getMetaData(); } } } else { dtdUriFromDocTypeIsNamespace = true; } } if (strict && !dtdUriFromDocTypeIsNamespace) return null; if (doctype != null) { XmlNSDescriptor descr = getNsDescriptorFormDocType(doctype, containingFile, false); if (descr != null) { return XmlExtension.getExtension(containingFile) .getDescriptorFromDoctype(containingFile, descr); } } if (strict) return null; if (namespace == XmlUtil.EMPTY_URI) { final XmlFile xmlFile = XmlUtil.findNamespace(containingFile, namespace); if (xmlFile != null) { return (XmlNSDescriptor) xmlFile.getDocument().getMetaData(); } } try { final PsiFile fileFromText = PsiFileFactory.getInstance(getProject()) .createFileFromText( containingFile.getName() + ".dtd", DTDLanguage.INSTANCE, XmlUtil.generateDocumentDTD(this, false), false, false); if (fileFromText instanceof XmlFile) { fileFromText.putUserData(AUTO_GENERATED, Boolean.TRUE); return (XmlNSDescriptor) ((XmlFile) fileFromText).getDocument().getMetaData(); } } catch (ProcessCanceledException ex) { throw ex; } catch (RuntimeException ignored) { } // e.g. dtd isn't mapped to xml type return null; } @NotNull private static String getFilePathForLogging(@Nullable PsiFile file) { if (file == null) { return "NULL"; } final VirtualFile vFile = file.getVirtualFile(); return vFile != null ? vFile.getPath() : "NULL_VFILE"; } @Nullable private XmlNSDescriptor getNsDescriptorFormDocType( final XmlDoctype doctype, final XmlFile containingFile, final boolean forHtml) { XmlNSDescriptor descriptor = getNSDescriptorFromMetaData(doctype.getMarkupDecl(), true); final String filePath = getFilePathForLogging(containingFile); final String dtdUri = XmlUtil.getDtdUri(doctype); LOG.debug( "DTD url for doctype " + doctype.getText() + " in file " + filePath + " is " + dtdUri); if (dtdUri != null && !dtdUri.isEmpty()) { XmlFile xmlFile = XmlUtil.findNamespace(containingFile, dtdUri); if (xmlFile == null) { // try to auto-detect it xmlFile = XmlNamespaceIndex.guessDtd(dtdUri, containingFile); } final String schemaFilePath = getFilePathForLogging(xmlFile); LOG.debug("Schema file for " + filePath + " is " + schemaFilePath); XmlNSDescriptor descriptorFromDtd = getNSDescriptorFromMetaData(xmlFile == null ? null : xmlFile.getDocument(), forHtml); LOG.debug( "Descriptor from meta data for schema file " + schemaFilePath + " is " + (descriptorFromDtd != null ? descriptorFromDtd.getClass().getCanonicalName() : "NULL")); if (descriptor != null && descriptorFromDtd != null) { descriptor = new XmlNSDescriptorSequence(new XmlNSDescriptor[] {descriptor, descriptorFromDtd}); } else if (descriptorFromDtd != null) { descriptor = descriptorFromDtd; } } return descriptor; } @Nullable private XmlNSDescriptor getNSDescriptorFromMetaData( @Nullable PsiMetaOwner metaOwner, boolean nonEmpty) { if (metaOwner == null) return null; XmlNSDescriptor descriptor = (XmlNSDescriptor) metaOwner.getMetaData(); if (descriptor == null) return null; if (nonEmpty && descriptor.getRootElementsDescriptors(this).length == 0) { return null; } return descriptor; } @Override public CompositePsiElement clone() { HashMap<String, CachedValue<XmlNSDescriptor>> cacheStrict = new HashMap<String, CachedValue<XmlNSDescriptor>>(myDefaultDescriptorsCacheStrict); HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict = new HashMap<String, CachedValue<XmlNSDescriptor>>(myDefaultDescriptorsCacheNotStrict); final XmlDocumentImpl copy = (XmlDocumentImpl) super.clone(); updateSelfDependentDtdDescriptors(copy, cacheStrict, cacheNotStrict); return copy; } @Override public PsiElement copy() { HashMap<String, CachedValue<XmlNSDescriptor>> cacheStrict = new HashMap<String, CachedValue<XmlNSDescriptor>>(myDefaultDescriptorsCacheStrict); HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict = new HashMap<String, CachedValue<XmlNSDescriptor>>(myDefaultDescriptorsCacheNotStrict); final XmlDocumentImpl copy = (XmlDocumentImpl) super.copy(); updateSelfDependentDtdDescriptors(copy, cacheStrict, cacheNotStrict); return copy; } private void updateSelfDependentDtdDescriptors( XmlDocumentImpl copy, HashMap<String, CachedValue<XmlNSDescriptor>> cacheStrict, HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict) { copy.myDefaultDescriptorsCacheNotStrict = ContainerUtil.newConcurrentMap(); copy.myDefaultDescriptorsCacheStrict = ContainerUtil.newConcurrentMap(); for (Map.Entry<String, CachedValue<XmlNSDescriptor>> e : cacheStrict.entrySet()) { if (e.getValue().hasUpToDateValue()) { final XmlNSDescriptor nsDescriptor = e.getValue().getValue(); if (!isGeneratedFromDtd(nsDescriptor)) copy.myDefaultDescriptorsCacheStrict.put(e.getKey(), e.getValue()); } } for (Map.Entry<String, CachedValue<XmlNSDescriptor>> e : cacheNotStrict.entrySet()) { if (e.getValue().hasUpToDateValue()) { final XmlNSDescriptor nsDescriptor = e.getValue().getValue(); if (!isGeneratedFromDtd(nsDescriptor)) copy.myDefaultDescriptorsCacheNotStrict.put(e.getKey(), e.getValue()); } } } @Override public PsiMetaData getMetaData() { return MetaRegistry.getMeta(this); } @SuppressWarnings({"HardCodedStringLiteral"}) public void dumpStatistics() { System.out.println("Statistics:"); final TObjectIntHashMap<Object> map = new TObjectIntHashMap<Object>(); final PsiElementVisitor psiRecursiveElementVisitor = new XmlRecursiveElementVisitor() { @NonNls private static final String TOKENS_KEY = "Tokens"; @NonNls private static final String ELEMENTS_KEY = "Elements"; @Override public void visitXmlToken(XmlToken token) { inc(TOKENS_KEY); } @Override public void visitElement(PsiElement element) { inc(ELEMENTS_KEY); super.visitElement(element); } private void inc(final String key) { map.put(key, map.get(key) + 1); } }; accept(psiRecursiveElementVisitor); final Object[] keys = map.keys(); for (final Object key : keys) { System.out.println(key + ": " + map.get(key)); } } @Override public TreeElement addInternal( final TreeElement first, final ASTNode last, final ASTNode anchor, final Boolean before) { final PomModel model = PomManager.getModel(getProject()); final XmlAspect aspect = model.getModelAspect(XmlAspect.class); final TreeElement[] holder = new TreeElement[1]; try { model.runTransaction( new PomTransactionBase(this, aspect) { @Override public PomModelEvent runInner() { holder[0] = XmlDocumentImpl.super.addInternal(first, last, anchor, before); return XmlDocumentChangedImpl.createXmlDocumentChanged(model, XmlDocumentImpl.this); } }); } catch (IncorrectOperationException ignored) { } return holder[0]; } @Override public void deleteChildInternal(@NotNull final ASTNode child) { final PomModel model = PomManager.getModel(getProject()); final XmlAspect aspect = model.getModelAspect(XmlAspect.class); try { model.runTransaction( new PomTransactionBase(this, aspect) { @Override public PomModelEvent runInner() { XmlDocumentImpl.super.deleteChildInternal(child); return XmlDocumentChangedImpl.createXmlDocumentChanged(model, XmlDocumentImpl.this); } }); } catch (IncorrectOperationException ignored) { } } @Override public void replaceChildInternal( @NotNull final ASTNode child, @NotNull final TreeElement newElement) { final PomModel model = PomManager.getModel(getProject()); final XmlAspect aspect = model.getModelAspect(XmlAspect.class); try { model.runTransaction( new PomTransactionBase(this, aspect) { @Override public PomModelEvent runInner() { XmlDocumentImpl.super.replaceChildInternal(child, newElement); return XmlDocumentChangedImpl.createXmlDocumentChanged(model, XmlDocumentImpl.this); } }); } catch (IncorrectOperationException ignored) { } } }
public class JavaResolveCache { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.resolve.JavaResolveCache"); private static final NotNullLazyKey<JavaResolveCache, Project> INSTANCE_KEY = ServiceManager.createLazyKey(JavaResolveCache.class); public static JavaResolveCache getInstance(Project project) { return INSTANCE_KEY.getValue(project); } private final ConcurrentMap<PsiExpression, Reference<PsiType>> myCalculatedTypes = new ConcurrentWeakHashMap<PsiExpression, Reference<PsiType>>(); private final Map<PsiVariable, Object> myVarToConstValueMapPhysical = new ConcurrentWeakHashMap<PsiVariable, Object>(); private final Map<PsiVariable, Object> myVarToConstValueMapNonPhysical = new ConcurrentWeakHashMap<PsiVariable, Object>(); private static final Object NULL = Key.create("NULL"); public JavaResolveCache( @Nullable( "can be null in com.intellij.core.JavaCoreApplicationEnvironment.JavaCoreApplicationEnvironment") MessageBus messageBus) { if (messageBus != null) { messageBus .connect() .subscribe( PsiManagerImpl.ANY_PSI_CHANGE_TOPIC, new AnyPsiChangeListener() { @Override public void beforePsiChanged(boolean isPhysical) { clearCaches(isPhysical); } @Override public void afterPsiChanged(boolean isPhysical) {} }); } } private void clearCaches(boolean isPhysical) { myCalculatedTypes.clear(); if (isPhysical) { myVarToConstValueMapPhysical.clear(); } myVarToConstValueMapNonPhysical.clear(); } @Nullable public <T extends PsiExpression> PsiType getType( @NotNull T expr, @NotNull Function<T, PsiType> f) { PsiType type = getCachedType(expr); if (type == null) { final RecursionGuard.StackStamp dStackStamp = PsiDiamondType.ourDiamondGuard.markStack(); final RecursionGuard.StackStamp gStackStamp = PsiResolveHelper.ourGraphGuard.markStack(); type = f.fun(expr); if (!dStackStamp.mayCacheNow() || !gStackStamp.mayCacheNow()) { return type; } if (type == null) type = TypeConversionUtil.NULL_TYPE; Reference<PsiType> ref = new SoftReference<PsiType>(type); myCalculatedTypes.put(expr, ref); if (type instanceof PsiClassReferenceType) { // convert reference-based class type to the PsiImmediateClassType, since the reference may // become invalid PsiClassType.ClassResolveResult result = ((PsiClassReferenceType) type).resolveGenerics(); PsiClass psiClass = result.getElement(); type = psiClass == null ? type // for type with unresolved reference, leave it in the cache // for clients still might be able to retrieve its getCanonicalText() from the // reference text : new PsiImmediateClassType( psiClass, result.getSubstitutor(), ((PsiClassReferenceType) type).getLanguageLevel(), type.getAnnotations()); } } if (!type.isValid()) { if (expr.isValid()) { PsiJavaCodeReferenceElement refInside = type instanceof PsiClassReferenceType ? ((PsiClassReferenceType) type).getReference() : null; @NonNls String typeinfo = type + " (" + type.getClass() + ")" + (refInside == null ? "" : "; ref inside: " + refInside + " (" + refInside.getClass() + ") valid:" + refInside.isValid()); LOG.error( "Type is invalid: " + typeinfo + "; expr: '" + expr + "' (" + expr.getClass() + ") is valid"); } else { LOG.error("Expression: '" + expr + "' is invalid, must not be used for getType()"); } } return type == TypeConversionUtil.NULL_TYPE ? null : type; } private <T extends PsiExpression> PsiType getCachedType(T expr) { Reference<PsiType> reference = myCalculatedTypes.get(expr); return SoftReference.dereference(reference); } @Nullable public Object computeConstantValueWithCaching( @NotNull PsiVariable variable, @NotNull ConstValueComputer computer, Set<PsiVariable> visitedVars) { boolean physical = variable.isPhysical(); Map<PsiVariable, Object> map = physical ? myVarToConstValueMapPhysical : myVarToConstValueMapNonPhysical; Object cached = map.get(variable); if (cached == NULL) return null; if (cached != null) return cached; Object result = computer.execute(variable, visitedVars); map.put(variable, result == null ? NULL : result); return result; } public interface ConstValueComputer { Object execute(PsiVariable variable, Set<PsiVariable> visitedVars); } }
/** @author Eugene Zhuravlev Date: 10/25/11 */ public class GroovyBuilder extends ModuleLevelBuilder { public static final String BUILDER_NAME = "groovy"; private static final Key<Boolean> CHUNK_REBUILD_ORDERED = Key.create("CHUNK_REBUILD_ORDERED"); private final boolean myForStubs; private final String myBuilderName; public GroovyBuilder(boolean forStubs) { super(forStubs ? BuilderCategory.SOURCE_GENERATOR : BuilderCategory.OVERWRITING_TRANSLATOR); myForStubs = forStubs; myBuilderName = BUILDER_NAME + (forStubs ? "-stubs" : "-classes"); } public String getName() { return myBuilderName; } public ModuleLevelBuilder.ExitCode build(final CompileContext context, ModuleChunk chunk) throws ProjectBuildException { try { final List<File> toCompile = collectChangedFiles(context, chunk); if (toCompile.isEmpty()) { return ExitCode.NOTHING_DONE; } String moduleOutput = getModuleOutput(context, chunk); String compilerOutput = getCompilerOutput(moduleOutput); final Set<String> toCompilePaths = new LinkedHashSet<String>(); for (File file : toCompile) { toCompilePaths.add(FileUtil.toSystemIndependentName(file.getPath())); } Map<String, String> class2Src = buildClassToSourceMap(chunk, context, toCompilePaths, moduleOutput); String ideCharset = chunk.getProject().getProjectCharset(); String encoding = !Comparing.equal(CharsetToolkit.getDefaultSystemCharset().name(), ideCharset) ? ideCharset : null; List<String> patchers = Collections.emptyList(); // todo patchers final File tempFile = GroovycOSProcessHandler.fillFileWithGroovycParameters( compilerOutput, toCompilePaths, FileUtil.toSystemDependentName(moduleOutput), class2Src, encoding, patchers); // todo different outputs in a chunk // todo xmx final List<String> cmd = ExternalProcessUtil.buildJavaCommandLine( getJavaExecutable(chunk), "org.jetbrains.groovy.compiler.rt.GroovycRunner", Collections.<String>emptyList(), new ArrayList<String>(generateClasspath(context, chunk)), Arrays.asList( "-Xmx384m", "-Dfile.encoding=" + CharsetToolkit.getDefaultSystemCharset().name() /*, "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5239"*/), Arrays.<String>asList(myForStubs ? "stubs" : "groovyc", tempFile.getPath())); final Process process = Runtime.getRuntime().exec(ArrayUtil.toStringArray(cmd)); GroovycOSProcessHandler handler = GroovycOSProcessHandler.runGroovyc( process, new Consumer<String>() { public void consume(String s) { context.processMessage(new ProgressMessage(s)); } }); if (handler.shouldRetry()) { if (CHUNK_REBUILD_ORDERED.get(context) != null) { CHUNK_REBUILD_ORDERED.set(context, null); } else { CHUNK_REBUILD_ORDERED.set(context, Boolean.TRUE); return ExitCode.CHUNK_REBUILD_REQUIRED; } } if (myForStubs) { JavaBuilder.addTempSourcePathRoot(context, new File(compilerOutput)); } for (CompilerMessage message : handler.getCompilerMessages()) { context.processMessage(message); } if (!myForStubs && updateDependencies( context, chunk, toCompile, moduleOutput, handler.getSuccessfullyCompiled())) { return ExitCode.ADDITIONAL_PASS_REQUIRED; } return ExitCode.OK; } catch (Exception e) { throw new ProjectBuildException(e); } } private static String getJavaExecutable(ModuleChunk chunk) { Sdk sdk = chunk.getModules().iterator().next().getSdk(); if (sdk instanceof JavaSdk) { return ((JavaSdk) sdk).getJavaExecutable(); } return SystemProperties.getJavaHome() + "/bin/java"; } private static String getModuleOutput(CompileContext context, ModuleChunk chunk) { final Module representativeModule = chunk.getModules().iterator().next(); File moduleOutputDir = context .getProjectPaths() .getModuleOutputDir(representativeModule, context.isCompilingTests()); assert moduleOutputDir != null; String moduleOutputPath = FileUtil.toCanonicalPath(moduleOutputDir.getPath()); return moduleOutputPath.endsWith("/") ? moduleOutputPath : moduleOutputPath + "/"; } private String getCompilerOutput(String moduleOutputDir) throws IOException { return FileUtil.toCanonicalPath( (myForStubs ? FileUtil.createTempDirectory("groovyStubs", null) : new File(moduleOutputDir)) .getPath()); } private static List<File> collectChangedFiles(CompileContext context, ModuleChunk chunk) throws IOException { final List<File> toCompile = new ArrayList<File>(); context.processFilesToRecompile( chunk, new FileProcessor() { public boolean apply(Module module, File file, String sourceRoot) throws IOException { final String path = file.getPath(); if (isGroovyFile(path)) { // todo file type check toCompile.add(file); } return true; } }); return toCompile; } private boolean updateDependencies( CompileContext context, ModuleChunk chunk, List<File> toCompile, String moduleOutputPath, List<GroovycOSProcessHandler.OutputItem> successfullyCompiled) throws IOException { final Mappings delta = context.createDelta(); final List<File> successfullyCompiledFiles = new ArrayList<File>(); if (!successfullyCompiled.isEmpty()) { final Callbacks.Backend callback = delta.getCallback(); final FileGeneratedEvent generatedEvent = new FileGeneratedEvent(); for (GroovycOSProcessHandler.OutputItem item : successfullyCompiled) { final String sourcePath = FileUtil.toSystemIndependentName(item.sourcePath); final String outputPath = FileUtil.toSystemIndependentName(item.outputPath); final RootDescriptor moduleAndRoot = context.getModuleAndRoot(new File(sourcePath)); if (moduleAndRoot != null) { final String moduleName = moduleAndRoot.module.getName().toLowerCase(Locale.US); context .getDataManager() .getSourceToOutputMap(moduleName, moduleAndRoot.isTestRoot) .appendData(sourcePath, outputPath); } callback.associate( outputPath, Callbacks.getDefaultLookup(sourcePath), new ClassReader(FileUtil.loadFileBytes(new File(outputPath)))); successfullyCompiledFiles.add(new File(sourcePath)); generatedEvent.add( moduleOutputPath, FileUtil.getRelativePath(moduleOutputPath, outputPath, '/')); } context.processMessage(generatedEvent); } return updateMappings(context, delta, chunk, toCompile, successfullyCompiledFiles); } private static List<String> generateClasspath(CompileContext context, ModuleChunk chunk) { final Set<String> cp = new LinkedHashSet<String>(); // groovy_rt.jar // IMPORTANT! must be the first in classpath cp.add(ClasspathBootstrap.getResourcePath(GroovyCompilerWrapper.class).getPath()); for (File file : context .getProjectPaths() .getClasspathFiles(chunk, ClasspathKind.compile(context.isCompilingTests()), false)) { cp.add(FileUtil.toCanonicalPath(file.getPath())); } for (File file : context .getProjectPaths() .getClasspathFiles(chunk, ClasspathKind.runtime(context.isCompilingTests()), false)) { cp.add(FileUtil.toCanonicalPath(file.getPath())); } return new ArrayList<String>(cp); } private static boolean isGroovyFile(String path) { return path.endsWith(".groovy") || path.endsWith(".gpp"); } private static Map<String, String> buildClassToSourceMap( ModuleChunk chunk, CompileContext context, Set<String> toCompilePaths, String moduleOutputPath) throws IOException { final Map<String, String> class2Src = new HashMap<String, String>(); for (Module module : chunk.getModules()) { final String moduleName = module.getName().toLowerCase(Locale.US); final SourceToOutputMapping srcToOut = context.getDataManager().getSourceToOutputMap(moduleName, context.isCompilingTests()); for (String src : srcToOut.getKeys()) { if (!toCompilePaths.contains(src) && isGroovyFile(src)) { final Collection<String> outs = srcToOut.getState(src); if (outs != null) { for (String out : outs) { if (out.endsWith(".class") && out.startsWith(moduleOutputPath)) { final String className = out.substring(moduleOutputPath.length(), out.length() - ".class".length()) .replace('/', '.'); class2Src.put(className, src); } } } } } } return class2Src; } @Override public String toString() { return "GroovyBuilder{" + "myForStubs=" + myForStubs + '}'; } public String getDescription() { return "Groovy builder"; } }
public class MagicConstantInspection extends LocalInspectionTool { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.magicConstant.MagicConstantInspection"); @Nls @NotNull @Override public String getGroupDisplayName() { return GroupNames.BUGS_GROUP_NAME; } @Nls @NotNull @Override public String getDisplayName() { return "Magic Constant"; } @NotNull @Override public String getShortName() { return "MagicConstant"; } @NotNull @Override public PsiElementVisitor buildVisitor( @NotNull final ProblemsHolder holder, boolean isOnTheFly, @NotNull LocalInspectionToolSession session) { checkAnnotationsJarAttached(session); return new JavaElementVisitor() { @Override public void visitCallExpression(PsiCallExpression callExpression) { checkCall(callExpression, holder); } @Override public void visitAssignmentExpression(PsiAssignmentExpression expression) { PsiExpression r = expression.getRExpression(); if (r == null) return; PsiExpression l = expression.getLExpression(); if (!(l instanceof PsiReferenceExpression)) return; PsiElement resolved = ((PsiReferenceExpression) l).resolve(); if (!(resolved instanceof PsiModifierListOwner)) return; PsiModifierListOwner owner = (PsiModifierListOwner) resolved; PsiType type = expression.getType(); checkExpression(r, owner, type, holder); } @Override public void visitReturnStatement(PsiReturnStatement statement) { PsiExpression value = statement.getReturnValue(); if (value == null) return; PsiMethod method = PsiTreeUtil.getParentOfType(statement, PsiMethod.class); if (method == null) return; checkExpression(value, method, value.getType(), holder); } @Override public void visitNameValuePair(PsiNameValuePair pair) { PsiAnnotationMemberValue value = pair.getValue(); if (!(value instanceof PsiExpression)) return; PsiReference ref = pair.getReference(); if (ref == null) return; PsiMethod method = (PsiMethod) ref.resolve(); if (method == null) return; checkExpression((PsiExpression) value, method, method.getReturnType(), holder); } @Override public void visitBinaryExpression(PsiBinaryExpression expression) { IElementType tokenType = expression.getOperationTokenType(); if (tokenType != JavaTokenType.EQEQ && tokenType != JavaTokenType.NE) return; PsiExpression l = expression.getLOperand(); PsiExpression r = expression.getROperand(); if (r == null) return; checkBinary(l, r); checkBinary(r, l); } private void checkBinary(PsiExpression l, PsiExpression r) { if (l instanceof PsiReference) { PsiElement resolved = ((PsiReference) l).resolve(); if (resolved instanceof PsiModifierListOwner) { checkExpression( r, (PsiModifierListOwner) resolved, getType((PsiModifierListOwner) resolved), holder); } } else if (l instanceof PsiMethodCallExpression) { PsiMethod method = ((PsiMethodCallExpression) l).resolveMethod(); if (method != null) { checkExpression(r, method, method.getReturnType(), holder); } } } }; } private static void checkAnnotationsJarAttached(@NotNull LocalInspectionToolSession session) { PsiFile file = session.getFile(); final Project project = file.getProject(); PsiClass event = JavaPsiFacade.getInstance(project) .findClass("java.awt.event.InputEvent", GlobalSearchScope.allScope(project)); if (event == null) return; // no jdk to attach PsiMethod[] methods = event.findMethodsByName("getModifiers", false); if (methods.length != 1) return; // no jdk to attach PsiMethod getModifiers = methods[0]; PsiAnnotation annotation = ExternalAnnotationsManager.getInstance(project) .findExternalAnnotation(getModifiers, MagicConstant.class.getName()); if (annotation != null) return; final VirtualFile virtualFile = PsiUtilCore.getVirtualFile(getModifiers); if (virtualFile == null) return; // no jdk to attach final List<OrderEntry> entries = ProjectRootManager.getInstance(project).getFileIndex().getOrderEntriesForFile(virtualFile); Sdk jdk = null; for (OrderEntry orderEntry : entries) { if (orderEntry instanceof JdkOrderEntry) { jdk = ((JdkOrderEntry) orderEntry).getJdk(); if (jdk != null) break; } } if (jdk == null) return; // no jdk to attach if (!ApplicationManager.getApplication().isUnitTestMode()) { final Sdk finalJdk = jdk; ApplicationManager.getApplication() .invokeLater( new Runnable() { @Override public void run() { ApplicationManager.getApplication() .runWriteAction( new Runnable() { public void run() { attachJdkAnnotations(finalJdk); } }); } }, ModalityState.NON_MODAL, project.getDisposed()); } } private static void attachJdkAnnotations(Sdk jdk) { LocalFileSystem lfs = LocalFileSystem.getInstance(); VirtualFile root = null; if (root == null) { // community idea under idea root = lfs.findFileByPath( FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/java/jdkAnnotations"); } if (root == null) { // idea under idea root = lfs.findFileByPath( FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/community/java/jdkAnnotations"); } if (root == null) { // build root = VirtualFileManager.getInstance() .findFileByUrl( "jar://" + FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/lib/jdkAnnotations.jar!/"); } if (root == null) { LOG.error( "jdk annotations not found in: " + FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/lib/jdkAnnotations.jar!/"); return; } SdkModificator modificator = jdk.getSdkModificator(); modificator.removeRoot(root, AnnotationOrderRootType.getInstance()); modificator.addRoot(root, AnnotationOrderRootType.getInstance()); modificator.commitChanges(); } private static void checkExpression( PsiExpression expression, PsiModifierListOwner owner, PsiType type, ProblemsHolder holder) { AllowedValues allowed = getAllowedValues(owner, type, null); if (allowed == null) return; PsiElement scope = PsiUtil.getTopLevelEnclosingCodeBlock(expression, null); if (scope == null) scope = expression; if (!isAllowed(scope, expression, allowed, expression.getManager())) { registerProblem(expression, allowed, holder); } } private static void checkCall( @NotNull PsiCallExpression methodCall, @NotNull ProblemsHolder holder) { PsiMethod method = methodCall.resolveMethod(); if (method == null) return; PsiParameter[] parameters = method.getParameterList().getParameters(); PsiExpression[] arguments = methodCall.getArgumentList().getExpressions(); for (int i = 0; i < parameters.length; i++) { PsiParameter parameter = parameters[i]; AllowedValues values = getAllowedValues(parameter, parameter.getType(), null); if (values == null) continue; if (i >= arguments.length) break; PsiExpression argument = arguments[i]; argument = PsiUtil.deparenthesizeExpression(argument); if (argument == null) continue; checkMagicParameterArgument(parameter, argument, values, holder); } } static class AllowedValues { final PsiAnnotationMemberValue[] values; final boolean canBeOred; private AllowedValues(PsiAnnotationMemberValue[] values, boolean canBeOred) { this.values = values; this.canBeOred = canBeOred; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AllowedValues a2 = (AllowedValues) o; if (canBeOred != a2.canBeOred) { return false; } Set<PsiAnnotationMemberValue> v1 = new THashSet<PsiAnnotationMemberValue>(Arrays.asList(values)); Set<PsiAnnotationMemberValue> v2 = new THashSet<PsiAnnotationMemberValue>(Arrays.asList(a2.values)); if (v1.size() != v2.size()) { return false; } for (PsiAnnotationMemberValue value : v1) { for (PsiAnnotationMemberValue value2 : v2) { if (same(value, value2, value.getManager())) { v2.remove(value2); break; } } } return v2.isEmpty(); } @Override public int hashCode() { int result = values != null ? Arrays.hashCode(values) : 0; result = 31 * result + (canBeOred ? 1 : 0); return result; } public boolean isSubsetOf(@NotNull AllowedValues other, @NotNull PsiManager manager) { for (PsiAnnotationMemberValue value : values) { boolean found = false; for (PsiAnnotationMemberValue otherValue : other.values) { if (same(value, otherValue, manager)) { found = true; break; } } if (!found) return false; } return true; } } private static AllowedValues getAllowedValuesFromMagic( @NotNull PsiModifierListOwner element, @NotNull PsiType type, PsiAnnotation magic) { if (magic == null) return null; PsiAnnotationMemberValue[] allowedValues; final boolean canBeOred; if (TypeConversionUtil.getTypeRank(type) <= TypeConversionUtil.LONG_RANK) { PsiAnnotationMemberValue intValues = magic.findAttributeValue("intValues"); allowedValues = intValues instanceof PsiArrayInitializerMemberValue ? ((PsiArrayInitializerMemberValue) intValues).getInitializers() : PsiAnnotationMemberValue.EMPTY_ARRAY; if (allowedValues.length == 0) { PsiAnnotationMemberValue orValue = magic.findAttributeValue("flags"); allowedValues = orValue instanceof PsiArrayInitializerMemberValue ? ((PsiArrayInitializerMemberValue) orValue).getInitializers() : PsiAnnotationMemberValue.EMPTY_ARRAY; canBeOred = true; } else { canBeOred = false; } } else if (type.equals( PsiType.getJavaLangString( element.getManager(), GlobalSearchScope.allScope(element.getProject())))) { PsiAnnotationMemberValue strValuesAttr = magic.findAttributeValue("stringValues"); allowedValues = strValuesAttr instanceof PsiArrayInitializerMemberValue ? ((PsiArrayInitializerMemberValue) strValuesAttr).getInitializers() : PsiAnnotationMemberValue.EMPTY_ARRAY; canBeOred = false; } else { return null; // other types not supported } if (allowedValues.length != 0) { return new AllowedValues(allowedValues, canBeOred); } // last resort: try valuesFromClass PsiAnnotationMemberValue[] values = readFromClass("valuesFromClass", magic, type); boolean ored = false; if (values == null) { values = readFromClass("flagsFromClass", magic, type); ored = true; } if (values == null) return null; return new AllowedValues(values, ored); } private static PsiAnnotationMemberValue[] readFromClass( @NonNls String attributeName, @NotNull PsiAnnotation magic, PsiType type) { PsiAnnotationMemberValue fromClassAttr = magic.findAttributeValue(attributeName); PsiType fromClassType = fromClassAttr instanceof PsiClassObjectAccessExpression ? ((PsiClassObjectAccessExpression) fromClassAttr).getOperand().getType() : null; PsiClass fromClass = fromClassType instanceof PsiClassType ? ((PsiClassType) fromClassType).resolve() : null; if (fromClass == null) return null; String fqn = fromClass.getQualifiedName(); if (fqn == null) return null; List<PsiAnnotationMemberValue> constants = new ArrayList<PsiAnnotationMemberValue>(); for (PsiField field : fromClass.getFields()) { if (!field.hasModifierProperty(PsiModifier.PUBLIC) || !field.hasModifierProperty(PsiModifier.STATIC) || !field.hasModifierProperty(PsiModifier.FINAL)) continue; PsiType fieldType = field.getType(); if (!Comparing.equal(fieldType, type)) continue; PsiAssignmentExpression e = (PsiAssignmentExpression) JavaPsiFacade.getElementFactory(field.getProject()) .createExpressionFromText("x=" + fqn + "." + field.getName(), field); PsiReferenceExpression refToField = (PsiReferenceExpression) e.getRExpression(); constants.add(refToField); } if (constants.isEmpty()) return null; return constants.toArray(new PsiAnnotationMemberValue[constants.size()]); } static AllowedValues getAllowedValues( @NotNull PsiModifierListOwner element, PsiType type, Set<PsiClass> visited) { PsiAnnotation[] annotations = AnnotationUtil.getAllAnnotations(element, true, null); for (PsiAnnotation annotation : annotations) { AllowedValues values; if (type != null && MagicConstant.class.getName().equals(annotation.getQualifiedName())) { // PsiAnnotation magic = AnnotationUtil.findAnnotationInHierarchy(element, // Collections.singleton(MagicConstant.class.getName())); values = getAllowedValuesFromMagic(element, type, annotation); if (values != null) return values; } PsiJavaCodeReferenceElement ref = annotation.getNameReferenceElement(); PsiElement resolved = ref == null ? null : ref.resolve(); if (!(resolved instanceof PsiClass) || !((PsiClass) resolved).isAnnotationType()) continue; PsiClass aClass = (PsiClass) resolved; if (visited == null) visited = new THashSet<PsiClass>(); if (!visited.add(aClass)) continue; values = getAllowedValues(aClass, type, visited); if (values != null) return values; } return parseBeanInfo(element); } private static AllowedValues parseBeanInfo(@NotNull PsiModifierListOwner owner) { PsiMethod method = null; if (owner instanceof PsiParameter) { PsiParameter parameter = (PsiParameter) owner; PsiElement scope = parameter.getDeclarationScope(); if (!(scope instanceof PsiMethod)) return null; PsiElement nav = scope.getNavigationElement(); if (!(nav instanceof PsiMethod)) return null; method = (PsiMethod) nav; if (method.isConstructor()) { // not a property, try the @ConstructorProperties({"prop"}) PsiAnnotation annotation = AnnotationUtil.findAnnotation(method, "java.beans.ConstructorProperties"); if (annotation == null) return null; PsiAnnotationMemberValue value = annotation.findAttributeValue("value"); if (!(value instanceof PsiArrayInitializerMemberValue)) return null; PsiAnnotationMemberValue[] initializers = ((PsiArrayInitializerMemberValue) value).getInitializers(); PsiElement parent = parameter.getParent(); if (!(parent instanceof PsiParameterList)) return null; int index = ((PsiParameterList) parent).getParameterIndex(parameter); if (index >= initializers.length) return null; PsiAnnotationMemberValue initializer = initializers[index]; if (!(initializer instanceof PsiLiteralExpression)) return null; Object val = ((PsiLiteralExpression) initializer).getValue(); if (!(val instanceof String)) return null; PsiMethod setter = PropertyUtil.findPropertySetter( method.getContainingClass(), (String) val, false, false); if (setter == null) return null; // try the @beaninfo of the corresponding setter method = (PsiMethod) setter.getNavigationElement(); } } else if (owner instanceof PsiMethod) { PsiElement nav = owner.getNavigationElement(); if (!(nav instanceof PsiMethod)) return null; method = (PsiMethod) nav; } if (method == null) return null; PsiClass aClass = method.getContainingClass(); if (aClass == null) return null; if (PropertyUtil.isSimplePropertyGetter(method)) { List<PsiMethod> setters = PropertyUtil.getSetters(aClass, PropertyUtil.getPropertyNameByGetter(method)); if (setters.size() != 1) return null; method = setters.get(0); } if (!PropertyUtil.isSimplePropertySetter(method)) return null; PsiDocComment doc = method.getDocComment(); if (doc == null) return null; PsiDocTag beaninfo = doc.findTagByName("beaninfo"); if (beaninfo == null) return null; String data = StringUtil.join( beaninfo.getDataElements(), new Function<PsiElement, String>() { @Override public String fun(PsiElement element) { return element.getText(); } }, "\n"); int enumIndex = StringUtil.indexOfSubstringEnd(data, "enum:"); if (enumIndex == -1) return null; data = data.substring(enumIndex); int colon = data.indexOf(":"); int last = colon == -1 ? data.length() : data.substring(0, colon).lastIndexOf("\n"); data = data.substring(0, last); List<PsiAnnotationMemberValue> values = new ArrayList<PsiAnnotationMemberValue>(); for (String line : StringUtil.splitByLines(data)) { List<String> words = StringUtil.split(line, " ", true, true); if (words.size() != 2) continue; String ref = words.get(1); PsiExpression constRef = JavaPsiFacade.getElementFactory(aClass.getProject()) .createExpressionFromText(ref, aClass); if (!(constRef instanceof PsiReferenceExpression)) continue; PsiReferenceExpression expr = (PsiReferenceExpression) constRef; values.add(expr); } if (values.isEmpty()) return null; PsiAnnotationMemberValue[] array = values.toArray(new PsiAnnotationMemberValue[values.size()]); return new AllowedValues(array, false); } private static PsiType getType(PsiModifierListOwner element) { return element instanceof PsiVariable ? ((PsiVariable) element).getType() : element instanceof PsiMethod ? ((PsiMethod) element).getReturnType() : null; } private static void checkMagicParameterArgument( @NotNull PsiParameter parameter, PsiExpression argument, @NotNull AllowedValues allowedValues, @NotNull ProblemsHolder holder) { final PsiManager manager = PsiManager.getInstance(holder.getProject()); if (!argument.getTextRange().isEmpty() && !isAllowed(parameter.getDeclarationScope(), argument, allowedValues, manager)) { registerProblem(argument, allowedValues, holder); } } private static void registerProblem( PsiExpression argument, AllowedValues allowedValues, ProblemsHolder holder) { String values = StringUtil.join( allowedValues.values, new Function<PsiAnnotationMemberValue, String>() { @Override public String fun(PsiAnnotationMemberValue value) { if (value instanceof PsiReferenceExpression) { PsiElement resolved = ((PsiReferenceExpression) value).resolve(); if (resolved instanceof PsiVariable) { return PsiFormatUtil.formatVariable( (PsiVariable) resolved, PsiFormatUtilBase.SHOW_NAME | PsiFormatUtilBase.SHOW_CONTAINING_CLASS, PsiSubstitutor.EMPTY); } } return value.getText(); } }, ", "); holder.registerProblem(argument, "Must be one of: " + values); } private static boolean isAllowed( @NotNull final PsiElement scope, @NotNull final PsiExpression argument, @NotNull final AllowedValues allowedValues, @NotNull final PsiManager manager) { if (isGoodExpression(argument, allowedValues, scope, manager)) return true; return processValuesFlownTo( argument, scope, new Processor<PsiExpression>() { @Override public boolean process(PsiExpression expression) { if (false & !PsiTreeUtil.isAncestor(scope, expression, false)) return true; return isGoodExpression(expression, allowedValues, scope, manager); } }); } private static boolean isGoodExpression( PsiExpression expression, @NotNull AllowedValues allowedValues, @NotNull PsiElement scope, @NotNull PsiManager manager) { expression = PsiUtil.deparenthesizeExpression(expression); if (expression == null) return true; if (expression instanceof PsiConditionalExpression) { PsiExpression thenExpression = ((PsiConditionalExpression) expression).getThenExpression(); boolean thenAllowed = thenExpression == null || isAllowed(scope, thenExpression, allowedValues, manager); if (!thenAllowed) return false; PsiExpression elseExpression = ((PsiConditionalExpression) expression).getElseExpression(); return elseExpression == null || isAllowed(scope, elseExpression, allowedValues, manager); } if (isOneOf(expression, allowedValues, manager)) return true; if (allowedValues.canBeOred) { PsiExpression zero = getLiteralExpression(expression, manager, "0"); if (same(expression, zero, manager)) return true; PsiExpression mOne = getLiteralExpression(expression, manager, "-1"); if (same(expression, mOne, manager)) return true; if (expression instanceof PsiPolyadicExpression) { IElementType tokenType = ((PsiPolyadicExpression) expression).getOperationTokenType(); if (JavaTokenType.OR.equals(tokenType) || JavaTokenType.AND.equals(tokenType)) { for (PsiExpression operand : ((PsiPolyadicExpression) expression).getOperands()) { if (!isAllowed(scope, operand, allowedValues, manager)) return false; } return true; } } if (expression instanceof PsiPrefixExpression && JavaTokenType.TILDE.equals( ((PsiPrefixExpression) expression).getOperationTokenType())) { PsiExpression operand = ((PsiPrefixExpression) expression).getOperand(); return operand == null || isAllowed(scope, operand, allowedValues, manager); } } PsiElement resolved = null; if (expression instanceof PsiReference) { resolved = ((PsiReference) expression).resolve(); } else if (expression instanceof PsiCallExpression) { resolved = ((PsiCallExpression) expression).resolveMethod(); } AllowedValues allowedForRef; if (resolved instanceof PsiModifierListOwner && (allowedForRef = getAllowedValues( (PsiModifierListOwner) resolved, getType((PsiModifierListOwner) resolved), null)) != null && allowedForRef.isSubsetOf(allowedValues, manager)) return true; return PsiType.NULL.equals(expression.getType()); } private static Key<Map<String, PsiExpression>> LITERAL_EXPRESSION_CACHE = Key.create("LITERAL_EXPRESSION_CACHE"); private static PsiExpression getLiteralExpression( PsiExpression context, PsiManager manager, @NotNull String text) { Map<String, PsiExpression> cache = LITERAL_EXPRESSION_CACHE.get(manager); if (cache == null) { cache = new ConcurrentSoftValueHashMap<String, PsiExpression>(); cache = manager.putUserDataIfAbsent(LITERAL_EXPRESSION_CACHE, cache); } PsiExpression expression = cache.get(text); if (expression == null) { expression = JavaPsiFacade.getElementFactory(manager.getProject()) .createExpressionFromText(text, context); cache.put(text, expression); } return expression; } private static boolean isOneOf( @NotNull PsiExpression expression, @NotNull AllowedValues allowedValues, @NotNull PsiManager manager) { for (PsiAnnotationMemberValue allowedValue : allowedValues.values) { if (same(allowedValue, expression, manager)) return true; } return false; } private static boolean same(PsiElement e1, PsiElement e2, @NotNull PsiManager manager) { if (e1 instanceof PsiLiteralExpression && e2 instanceof PsiLiteralExpression) { return Comparing.equal( ((PsiLiteralExpression) e1).getValue(), ((PsiLiteralExpression) e2).getValue()); } if (e1 instanceof PsiPrefixExpression && e2 instanceof PsiPrefixExpression && ((PsiPrefixExpression) e1).getOperationTokenType() == ((PsiPrefixExpression) e2).getOperationTokenType()) { return same( ((PsiPrefixExpression) e1).getOperand(), ((PsiPrefixExpression) e2).getOperand(), manager); } if (e1 instanceof PsiReference && e2 instanceof PsiReference) { e1 = ((PsiReference) e1).resolve(); e2 = ((PsiReference) e2).resolve(); } return manager.areElementsEquivalent(e2, e1); } private static boolean processValuesFlownTo( @NotNull final PsiExpression argument, PsiElement scope, @NotNull final Processor<PsiExpression> processor) { SliceAnalysisParams params = new SliceAnalysisParams(); params.dataFlowToThis = true; params.scope = new AnalysisScope(new LocalSearchScope(scope), argument.getProject()); SliceRootNode rootNode = new SliceRootNode( scope.getProject(), new DuplicateMap(), SliceManager.createRootUsage(argument, params)); Collection<? extends AbstractTreeNode> children = rootNode.getChildren().iterator().next().getChildren(); for (AbstractTreeNode child : children) { SliceUsage usage = (SliceUsage) child.getValue(); PsiElement element = usage.getElement(); if (element instanceof PsiExpression && !processor.process((PsiExpression) element)) return false; } return !children.isEmpty(); } }
/** Author: msk */ public class EditorsSplitters extends IdePanePanel implements UISettingsListener, Disposable { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.fileEditor.impl.EditorsSplitters"); private static final String PINNED = "pinned"; private static final String CURRENT_IN_TAB = "current-in-tab"; private static final Key<Object> DUMMY_KEY = Key.create("EditorsSplitters.dummy.key"); private EditorWindow myCurrentWindow; private final Set<EditorWindow> myWindows = new CopyOnWriteArraySet<EditorWindow>(); private final FileEditorManagerImpl myManager; private Element mySplittersElement; // temporarily used during initialization int myInsideChange; private final MyFocusWatcher myFocusWatcher; private final Alarm myIconUpdaterAlarm = new Alarm(); private final UIBuilder myUIBuilder = new UIBuilder(); EditorsSplitters( final FileEditorManagerImpl manager, DockManager dockManager, boolean createOwnDockableContainer) { super(new BorderLayout()); myManager = manager; myFocusWatcher = new MyFocusWatcher(); setFocusTraversalPolicy(new MyFocusTraversalPolicy()); clear(); if (createOwnDockableContainer) { DockableEditorTabbedContainer dockable = new DockableEditorTabbedContainer(myManager.getProject(), this, false); Disposer.register(manager.getProject(), dockable); dockManager.register(dockable); } KeymapManagerListener keymapListener = new KeymapManagerListener() { @Override public void activeKeymapChanged(Keymap keymap) { invalidate(); repaint(); } }; KeymapManager.getInstance().addKeymapManagerListener(keymapListener, this); } public FileEditorManagerImpl getManager() { return myManager; } public void clear() { for (EditorWindow window : myWindows) { window.dispose(); } removeAll(); myWindows.clear(); setCurrentWindow(null); repaint(); // revalidate doesn't repaint correctly after "Close All" } void startListeningFocus() { myFocusWatcher.install(this); } private void stopListeningFocus() { myFocusWatcher.deinstall(this); } @Override public void dispose() { myIconUpdaterAlarm.cancelAllRequests(); stopListeningFocus(); } @Nullable public VirtualFile getCurrentFile() { if (myCurrentWindow != null) { return myCurrentWindow.getSelectedFile(); } return null; } private boolean showEmptyText() { return myCurrentWindow == null || myCurrentWindow.getFiles().length == 0; } @Override protected void paintComponent(Graphics g) { if (showEmptyText()) { Graphics2D gg = IdeBackgroundUtil.withFrameBackground(g, this); super.paintComponent(gg); g.setColor(UIUtil.isUnderDarcula() ? UIUtil.getBorderColor() : new Color(0, 0, 0, 50)); g.drawLine(0, 0, getWidth(), 0); } } public void writeExternal(final Element element) { if (getComponentCount() != 0) { final Component comp = getComponent(0); LOG.assertTrue(comp instanceof JPanel); final JPanel panel = (JPanel) comp; if (panel.getComponentCount() != 0) { element.addContent(writePanel(panel)); } } } @SuppressWarnings("HardCodedStringLiteral") private Element writePanel(final JPanel panel) { final Component comp = panel.getComponent(0); if (comp instanceof Splitter) { final Splitter splitter = (Splitter) comp; final Element res = new Element("splitter"); res.setAttribute("split-orientation", splitter.getOrientation() ? "vertical" : "horizontal"); res.setAttribute("split-proportion", Float.toString(splitter.getProportion())); final Element first = new Element("split-first"); first.addContent(writePanel((JPanel) splitter.getFirstComponent())); final Element second = new Element("split-second"); second.addContent(writePanel((JPanel) splitter.getSecondComponent())); res.addContent(first); res.addContent(second); return res; } else if (comp instanceof JBTabs) { final Element res = new Element("leaf"); Integer limit = UIUtil.getClientProperty( ((JBTabs) comp).getComponent(), JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY); if (limit != null) { res.setAttribute(JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY.toString(), String.valueOf(limit)); } writeWindow(res, findWindowWith(comp)); return res; } else if (comp instanceof EditorWindow.TCompForTablessMode) { EditorWithProviderComposite composite = ((EditorWindow.TCompForTablessMode) comp).myEditor; Element res = new Element("leaf"); res.addContent(writeComposite(composite.getFile(), composite, false, composite)); return res; } else { LOG.error(comp != null ? comp.getClass().getName() : null); return null; } } private void writeWindow(@NotNull Element res, @Nullable EditorWindow window) { if (window != null) { EditorWithProviderComposite[] composites = window.getEditors(); for (int i = 0; i < composites.length; i++) { VirtualFile file = window.getFileAt(i); res.addContent( writeComposite( file, composites[i], window.isFilePinned(file), window.getSelectedEditor())); } } } @NotNull private Element writeComposite( VirtualFile file, EditorWithProviderComposite composite, boolean pinned, EditorWithProviderComposite selectedEditor) { Element fileElement = new Element("file"); fileElement.setAttribute("leaf-file-name", file.getName()); // TODO: all files composite.currentStateAsHistoryEntry().writeExternal(fileElement, getManager().getProject()); fileElement.setAttribute(PINNED, Boolean.toString(pinned)); fileElement.setAttribute(CURRENT_IN_TAB, Boolean.toString(composite.equals(selectedEditor))); return fileElement; } public void openFiles() { if (mySplittersElement != null) { initializeProgress(); final JPanel comp = myUIBuilder.process(mySplittersElement, getTopPanel()); UIUtil.invokeAndWaitIfNeeded( new Runnable() { @Override public void run() { if (comp != null) { removeAll(); add(comp, BorderLayout.CENTER); mySplittersElement = null; } // clear empty splitters for (EditorWindow window : getWindows()) { if (window.getEditors().length == 0) { for (EditorWindow sibling : window.findSiblings()) { sibling.unsplit(false); } } } } }); } } private static void initializeProgress() { ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); if (indicator != null) { indicator.setText(IdeBundle.message("loading.editors")); } } public int getEditorsCount() { return mySplittersElement == null ? 0 : countFiles(mySplittersElement); } private double myProgressStep; public void setProgressStep(double step) { myProgressStep = step; } private void updateProgress() { ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); if (indicator != null) { indicator.setFraction(indicator.getFraction() + myProgressStep); } } private static int countFiles(Element element) { Integer value = new ConfigTreeReader<Integer>() { @Override protected Integer processFiles( @NotNull List<Element> fileElements, @Nullable Integer context) { return fileElements.size(); } @Override protected Integer processSplitter( @NotNull Element element, @Nullable Element firstChild, @Nullable Element secondChild, @Nullable Integer context) { Integer first = process(firstChild, null); Integer second = process(secondChild, null); return (first == null ? 0 : first) + (second == null ? 0 : second); } }.process(element, null); return value == null ? 0 : value; } public void readExternal(final Element element) { mySplittersElement = element; } @NotNull public VirtualFile[] getOpenFiles() { final Set<VirtualFile> files = new ArrayListSet<VirtualFile>(); for (final EditorWindow myWindow : myWindows) { final EditorWithProviderComposite[] editors = myWindow.getEditors(); for (final EditorWithProviderComposite editor : editors) { VirtualFile file = editor.getFile(); // background thread may call this method when invalid file is being removed // do not return it here as it will quietly drop out soon if (file.isValid()) { files.add(file); } } } return VfsUtilCore.toVirtualFileArray(files); } @NotNull public VirtualFile[] getSelectedFiles() { final Set<VirtualFile> files = new ArrayListSet<VirtualFile>(); for (final EditorWindow window : myWindows) { final VirtualFile file = window.getSelectedFile(); if (file != null) { files.add(file); } } final VirtualFile[] virtualFiles = VfsUtilCore.toVirtualFileArray(files); final VirtualFile currentFile = getCurrentFile(); if (currentFile != null) { for (int i = 0; i != virtualFiles.length; ++i) { if (Comparing.equal(virtualFiles[i], currentFile)) { virtualFiles[i] = virtualFiles[0]; virtualFiles[0] = currentFile; break; } } } return virtualFiles; } @NotNull public FileEditor[] getSelectedEditors() { List<FileEditor> editors = new ArrayList<FileEditor>(); Set<EditorWindow> windows = new THashSet<EditorWindow>(myWindows); final EditorWindow currentWindow = getCurrentWindow(); if (currentWindow != null) { windows.add(currentWindow); } for (final EditorWindow window : windows) { final EditorWithProviderComposite composite = window.getSelectedEditor(); if (composite != null) { editors.add(composite.getSelectedEditor()); } } return editors.toArray(new FileEditor[editors.size()]); } public void updateFileIcon(@NotNull final VirtualFile file) { updateFileIconLater(file); } private void updateFileIconImmediately(final VirtualFile file) { final Collection<EditorWindow> windows = findWindows(file); for (EditorWindow window : windows) { window.updateFileIcon(file); } } private final Set<VirtualFile> myFilesToUpdateIconsFor = new HashSet<VirtualFile>(); private void updateFileIconLater(VirtualFile file) { myFilesToUpdateIconsFor.add(file); myIconUpdaterAlarm.cancelAllRequests(); myIconUpdaterAlarm.addRequest( () -> { if (myManager.getProject().isDisposed()) return; for (VirtualFile file1 : myFilesToUpdateIconsFor) { updateFileIconImmediately(file1); } myFilesToUpdateIconsFor.clear(); }, 200, ModalityState.stateForComponent(this)); } void updateFileColor(@NotNull final VirtualFile file) { final Collection<EditorWindow> windows = findWindows(file); for (final EditorWindow window : windows) { final int index = window.findEditorIndex(window.findFileComposite(file)); LOG.assertTrue(index != -1); window.setForegroundAt(index, getManager().getFileColor(file)); window.setWaveColor(index, getManager().isProblem(file) ? JBColor.red : null); } } public void trimToSize(final int editor_tab_limit) { for (final EditorWindow window : myWindows) { window.trimToSize(editor_tab_limit, null, true); } } public void setTabsPlacement(final int tabPlacement) { final EditorWindow[] windows = getWindows(); for (int i = 0; i != windows.length; ++i) { windows[i].setTabsPlacement(tabPlacement); } } void setTabLayoutPolicy(int scrollTabLayout) { final EditorWindow[] windows = getWindows(); for (int i = 0; i != windows.length; ++i) { windows[i].setTabLayoutPolicy(scrollTabLayout); } } void updateFileName(@Nullable final VirtualFile updatedFile) { final EditorWindow[] windows = getWindows(); for (int i = 0; i != windows.length; ++i) { for (VirtualFile file : windows[i].getFiles()) { if (updatedFile == null || file.getName().equals(updatedFile.getName())) { windows[i].updateFileName(file); } } } Project project = myManager.getProject(); final IdeFrame frame = getFrame(project); if (frame != null) { VirtualFile file = getCurrentFile(); File ioFile = file == null ? null : new File(file.getPresentableUrl()); String fileTitle = null; if (file != null) { fileTitle = DumbService.isDumb(project) ? file.getName() : FrameTitleBuilder.getInstance().getFileTitle(project, file); } frame.setFileTitle(fileTitle, ioFile); } } protected IdeFrame getFrame(Project project) { final WindowManagerEx windowManagerEx = WindowManagerEx.getInstanceEx(); final IdeFrame frame = windowManagerEx.getFrame(project); LOG.assertTrue(ApplicationManager.getApplication().isUnitTestMode() || frame != null); return frame; } boolean isInsideChange() { return myInsideChange > 0; } private void setCurrentWindow(@Nullable final EditorWindow currentWindow) { if (currentWindow != null && !myWindows.contains(currentWindow)) { throw new IllegalArgumentException(currentWindow + " is not a member of this container"); } myCurrentWindow = currentWindow; } void updateFileBackgroundColor(@NotNull VirtualFile file) { final EditorWindow[] windows = getWindows(); for (int i = 0; i != windows.length; ++i) { windows[i].updateFileBackgroundColor(file); } } int getSplitCount() { if (getComponentCount() > 0) { JPanel panel = (JPanel) getComponent(0); return getSplitCount(panel); } return 0; } private static int getSplitCount(JComponent component) { if (component.getComponentCount() > 0) { final JComponent firstChild = (JComponent) component.getComponent(0); if (firstChild instanceof Splitter) { final Splitter splitter = (Splitter) firstChild; return getSplitCount(splitter.getFirstComponent()) + getSplitCount(splitter.getSecondComponent()); } return 1; } return 0; } protected void afterFileClosed(VirtualFile file) {} protected void afterFileOpen(VirtualFile file) {} @Nullable JBTabs getTabsAt(RelativePoint point) { Point thisPoint = point.getPoint(this); Component c = SwingUtilities.getDeepestComponentAt(this, thisPoint.x, thisPoint.y); while (c != null) { if (c instanceof JBTabs) { return (JBTabs) c; } c = c.getParent(); } return null; } boolean isEmptyVisible() { EditorWindow[] windows = getWindows(); for (EditorWindow each : windows) { if (!each.isEmptyVisible()) { return false; } } return true; } @Nullable private VirtualFile findNextFile(final VirtualFile file) { final EditorWindow[] windows = getWindows(); // TODO: use current file as base for (int i = 0; i != windows.length; ++i) { final VirtualFile[] files = windows[i].getFiles(); for (final VirtualFile fileAt : files) { if (!Comparing.equal(fileAt, file)) { return fileAt; } } } return null; } void closeFile(VirtualFile file, boolean moveFocus) { final List<EditorWindow> windows = findWindows(file); if (!windows.isEmpty()) { final VirtualFile nextFile = findNextFile(file); for (final EditorWindow window : windows) { LOG.assertTrue(window.getSelectedEditor() != null); window.closeFile(file, false, moveFocus); if (window.getTabCount() == 0 && nextFile != null && myManager.getProject().isOpen()) { EditorWithProviderComposite newComposite = myManager.newEditorComposite(nextFile); window.setEditor(newComposite, moveFocus); // newComposite can be null } } // cleanup windows with no tabs for (final EditorWindow window : windows) { if (window.isDisposed()) { // call to window.unsplit() which might make its sibling disposed continue; } if (window.getTabCount() == 0) { window.unsplit(false); } } } } @Override public void uiSettingsChanged(UISettings source) { if (!myManager.getProject().isOpen()) return; for (VirtualFile file : getOpenFiles()) { updateFileBackgroundColor(file); updateFileIcon(file); updateFileColor(file); } } private final class MyFocusTraversalPolicy extends IdeFocusTraversalPolicy { @Override public final Component getDefaultComponentImpl(final Container focusCycleRoot) { if (myCurrentWindow != null) { final EditorWithProviderComposite selectedEditor = myCurrentWindow.getSelectedEditor(); if (selectedEditor != null) { return IdeFocusTraversalPolicy.getPreferredFocusedComponent( selectedEditor.getComponent(), this); } } return IdeFocusTraversalPolicy.getPreferredFocusedComponent(EditorsSplitters.this, this); } } @Nullable public JPanel getTopPanel() { return getComponentCount() > 0 ? (JPanel) getComponent(0) : null; } public EditorWindow getCurrentWindow() { return myCurrentWindow; } public EditorWindow getOrCreateCurrentWindow(final VirtualFile file) { final List<EditorWindow> windows = findWindows(file); if (getCurrentWindow() == null) { final Iterator<EditorWindow> iterator = myWindows.iterator(); if (!windows.isEmpty()) { setCurrentWindow(windows.get(0), false); } else if (iterator.hasNext()) { setCurrentWindow(iterator.next(), false); } else { createCurrentWindow(); } } else if (!windows.isEmpty()) { if (!windows.contains(getCurrentWindow())) { setCurrentWindow(windows.get(0), false); } } return getCurrentWindow(); } void createCurrentWindow() { LOG.assertTrue(myCurrentWindow == null); setCurrentWindow(createEditorWindow()); add(myCurrentWindow.myPanel, BorderLayout.CENTER); } protected EditorWindow createEditorWindow() { return new EditorWindow(this); } /** * sets the window passed as a current ('focused') window among all splitters. All file openings * will be done inside this current window * * @param window a window to be set as current * @param requestFocus whether to request focus to the editor currently selected in this window */ void setCurrentWindow(@Nullable final EditorWindow window, final boolean requestFocus) { final EditorWithProviderComposite newEditor = window == null ? null : window.getSelectedEditor(); Runnable fireRunnable = () -> getManager().fireSelectionChanged(newEditor); setCurrentWindow(window); getManager().updateFileName(window == null ? null : window.getSelectedFile()); if (window != null) { final EditorWithProviderComposite selectedEditor = window.getSelectedEditor(); if (selectedEditor != null) { fireRunnable.run(); } if (requestFocus) { window.requestFocus(true); } } else { fireRunnable.run(); } } void addWindow(EditorWindow window) { myWindows.add(window); } void removeWindow(EditorWindow window) { myWindows.remove(window); if (myCurrentWindow == window) { myCurrentWindow = null; } } boolean containsWindow(EditorWindow window) { return myWindows.contains(window); } // --------------------------------------------------------- public EditorWithProviderComposite[] getEditorsComposites() { List<EditorWithProviderComposite> res = new ArrayList<EditorWithProviderComposite>(); for (final EditorWindow myWindow : myWindows) { final EditorWithProviderComposite[] editors = myWindow.getEditors(); ContainerUtil.addAll(res, editors); } return res.toArray(new EditorWithProviderComposite[res.size()]); } // --------------------------------------------------------- @NotNull public List<EditorWithProviderComposite> findEditorComposites(@NotNull VirtualFile file) { List<EditorWithProviderComposite> res = new ArrayList<EditorWithProviderComposite>(); for (final EditorWindow window : myWindows) { final EditorWithProviderComposite fileComposite = window.findFileComposite(file); if (fileComposite != null) { res.add(fileComposite); } } return res; } @NotNull private List<EditorWindow> findWindows(final VirtualFile file) { List<EditorWindow> res = new ArrayList<EditorWindow>(); for (final EditorWindow window : myWindows) { if (window.findFileComposite(file) != null) { res.add(window); } } return res; } @NotNull public EditorWindow[] getWindows() { return myWindows.toArray(new EditorWindow[myWindows.size()]); } @NotNull EditorWindow[] getOrderedWindows() { final List<EditorWindow> res = new ArrayList<EditorWindow>(); // Collector for windows in tree ordering: class Inner { private void collect(final JPanel panel) { final Component comp = panel.getComponent(0); if (comp instanceof Splitter) { final Splitter splitter = (Splitter) comp; collect((JPanel) splitter.getFirstComponent()); collect((JPanel) splitter.getSecondComponent()); } else if (comp instanceof JPanel || comp instanceof JBTabs) { final EditorWindow window = findWindowWith(comp); if (window != null) { res.add(window); } } } } // get root component and traverse splitters tree: if (getComponentCount() != 0) { final Component comp = getComponent(0); LOG.assertTrue(comp instanceof JPanel); final JPanel panel = (JPanel) comp; if (panel.getComponentCount() != 0) { new Inner().collect(panel); } } LOG.assertTrue(res.size() == myWindows.size()); return res.toArray(new EditorWindow[res.size()]); } @Nullable private EditorWindow findWindowWith(final Component component) { if (component != null) { for (final EditorWindow window : myWindows) { if (SwingUtilities.isDescendingFrom(component, window.myPanel)) { return window; } } } return null; } public boolean isFloating() { return false; } public boolean isPreview() { return false; } private final class MyFocusWatcher extends FocusWatcher { @Override protected void focusedComponentChanged(final Component component, final AWTEvent cause) { EditorWindow newWindow = null; if (component != null) { newWindow = findWindowWith(component); } else if (cause instanceof ContainerEvent && cause.getID() == ContainerEvent.COMPONENT_REMOVED) { // do not change current window in case of child removal as in JTable.removeEditor // otherwise Escape in a toolwindow will not focus editor with JTable content return; } setCurrentWindow(newWindow); setCurrentWindow(newWindow, false); } } private abstract static class ConfigTreeReader<T> { @Nullable public T process(@Nullable Element element, @Nullable T context) { if (element == null) { return null; } final Element splitterElement = element.getChild("splitter"); if (splitterElement != null) { final Element first = splitterElement.getChild("split-first"); final Element second = splitterElement.getChild("split-second"); return processSplitter(splitterElement, first, second, context); } final Element leaf = element.getChild("leaf"); if (leaf == null) { return null; } List<Element> fileElements = leaf.getChildren("file"); final List<Element> children = new ArrayList<Element>(fileElements.size()); // trim to EDITOR_TAB_LIMIT, ignoring CLOSE_NON_MODIFIED_FILES_FIRST policy int toRemove = fileElements.size() - UISettings.getInstance().EDITOR_TAB_LIMIT; for (Element fileElement : fileElements) { if (toRemove <= 0 || Boolean.valueOf(fileElement.getAttributeValue(PINNED)).booleanValue()) { children.add(fileElement); } else { toRemove--; } } return processFiles(children, context); } @Nullable protected abstract T processFiles(@NotNull List<Element> fileElements, @Nullable T context); @Nullable protected abstract T processSplitter( @NotNull Element element, @Nullable Element firstChild, @Nullable Element secondChild, @Nullable T context); } private class UIBuilder extends ConfigTreeReader<JPanel> { @Override protected JPanel processFiles(@NotNull List<Element> fileElements, final JPanel context) { final Ref<EditorWindow> windowRef = new Ref<EditorWindow>(); UIUtil.invokeAndWaitIfNeeded( new Runnable() { @Override public void run() { windowRef.set(context == null ? createEditorWindow() : findWindowWith(context)); } }); final EditorWindow window = windowRef.get(); LOG.assertTrue(window != null); VirtualFile focusedFile = null; for (int i = 0; i < fileElements.size(); i++) { final Element file = fileElements.get(i); if (i == 0) { EditorTabbedContainer tabbedPane = window.getTabbedPane(); if (tabbedPane != null) { try { int limit = Integer.parseInt( file.getParentElement() .getAttributeValue( JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY.toString(), String.valueOf(JBTabsImpl.DEFAULT_MAX_TAB_WIDTH))); UIUtil.putClientProperty( tabbedPane.getComponent(), JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY, limit); } catch (NumberFormatException e) { // ignore } } } try { final FileEditorManagerImpl fileEditorManager = getManager(); Element historyElement = file.getChild(HistoryEntry.TAG); final HistoryEntry entry = HistoryEntry.createLight(fileEditorManager.getProject(), historyElement); final VirtualFile virtualFile = entry.getFile(); if (virtualFile == null) throw new InvalidDataException("No file exists: " + entry.getFilePointer().getUrl()); Document document = ApplicationManager.getApplication() .runReadAction( new Computable<Document>() { @Override public Document compute() { return virtualFile.isValid() ? FileDocumentManager.getInstance().getDocument(virtualFile) : null; } }); final boolean isCurrentInTab = Boolean.valueOf(file.getAttributeValue(CURRENT_IN_TAB)).booleanValue(); Boolean pin = Boolean.valueOf(file.getAttributeValue(PINNED)); fileEditorManager.openFileImpl4( window, virtualFile, entry, isCurrentInTab, isCurrentInTab, pin, i); if (isCurrentInTab) { focusedFile = virtualFile; } if (document != null) { // This is just to make sure document reference is kept on stack till this point // so that document is available for folding state deserialization in HistoryEntry // constructor // and that document will be created only once during file opening document.putUserData(DUMMY_KEY, null); } updateProgress(); } catch (InvalidDataException e) { if (ApplicationManager.getApplication().isUnitTestMode()) { LOG.error(e); } } } if (focusedFile != null) { getManager().addSelectionRecord(focusedFile, window); } return window.myPanel; } @Override protected JPanel processSplitter( @NotNull Element splitterElement, Element firstChild, Element secondChild, final JPanel context) { if (context == null) { final boolean orientation = "vertical".equals(splitterElement.getAttributeValue("split-orientation")); final float proportion = Float.valueOf(splitterElement.getAttributeValue("split-proportion")).floatValue(); final JPanel firstComponent = process(firstChild, null); final JPanel secondComponent = process(secondChild, null); final Ref<JPanel> panelRef = new Ref<JPanel>(); UIUtil.invokeAndWaitIfNeeded( new Runnable() { @Override public void run() { JPanel panel = new JPanel(new BorderLayout()); panel.setOpaque(false); Splitter splitter = new OnePixelSplitter(orientation, proportion, 0.1f, 0.9f); panel.add(splitter, BorderLayout.CENTER); splitter.setFirstComponent(firstComponent); splitter.setSecondComponent(secondComponent); panelRef.set(panel); } }); return panelRef.get(); } final Ref<JPanel> firstComponent = new Ref<JPanel>(); final Ref<JPanel> secondComponent = new Ref<JPanel>(); UIUtil.invokeAndWaitIfNeeded( new Runnable() { @Override public void run() { if (context.getComponent(0) instanceof Splitter) { Splitter splitter = (Splitter) context.getComponent(0); firstComponent.set((JPanel) splitter.getFirstComponent()); secondComponent.set((JPanel) splitter.getSecondComponent()); } else { firstComponent.set(context); secondComponent.set(context); } } }); process(firstChild, firstComponent.get()); process(secondChild, secondComponent.get()); return context; } } }
/** @author spleaner */ public class HtmlUnknownTagInspection extends HtmlLocalInspectionTool { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.htmlInspections.HtmlUnknownTagInspection"); public JDOMExternalizableStringList myValues; public boolean myCustomValuesEnabled = true; @NonNls public static final String TAG_SHORT_NAME = "HtmlUnknownTag"; public static final Key<HtmlUnknownTagInspection> TAG_KEY = Key.create(TAG_SHORT_NAME); public HtmlUnknownTagInspection() { this("nobr,noembed,comment,noscript,embed,script"); } protected HtmlUnknownTagInspection(@NonNls @NotNull final String defaultValues) { myValues = reparseProperties(defaultValues); } @Override @Nls @NotNull public String getDisplayName() { return XmlBundle.message("html.inspections.unknown.tag"); } @Override @NonNls @NotNull public String getShortName() { return TAG_SHORT_NAME; } @Override @Nullable public JComponent createOptionsPanel() { final JPanel result = new JPanel(new BorderLayout()); final JPanel internalPanel = new JPanel(new BorderLayout()); result.add(internalPanel, BorderLayout.NORTH); final FieldPanel additionalAttributesPanel = new FieldPanel(null, getPanelTitle(), null, null); additionalAttributesPanel .getTextField() .getDocument() .addDocumentListener( new DocumentAdapter() { @Override protected void textChanged(DocumentEvent e) { final Document document = e.getDocument(); try { final String text = document.getText(0, document.getLength()); if (text != null) { myValues = reparseProperties(text.trim()); } } catch (BadLocationException e1) { getLogger().error(e1); } } }); final JCheckBox checkBox = new JCheckBox(getCheckboxTitle()); checkBox.setSelected(myCustomValuesEnabled); checkBox.addActionListener( new ActionListener() { @Override public void actionPerformed(ActionEvent e) { final boolean b = checkBox.isSelected(); if (b != myCustomValuesEnabled) { myCustomValuesEnabled = b; additionalAttributesPanel.setEnabled(myCustomValuesEnabled); } } }); internalPanel.add(checkBox, BorderLayout.NORTH); internalPanel.add(additionalAttributesPanel, BorderLayout.CENTER); additionalAttributesPanel.setPreferredSize( new Dimension(150, additionalAttributesPanel.getPreferredSize().height)); additionalAttributesPanel.setEnabled(myCustomValuesEnabled); additionalAttributesPanel.setText(createPropertiesString()); return result; } @NotNull protected Logger getLogger() { return LOG; } private String createPropertiesString() { final StringBuffer buffer = new StringBuffer(); for (final String property : myValues) { if (buffer.length() == 0) { buffer.append(property); } else { buffer.append(','); buffer.append(property); } } return buffer.toString(); } public String getAdditionalEntries() { return createPropertiesString(); } protected String getCheckboxTitle() { return XmlBundle.message("html.inspections.unknown.tag.checkbox.title"); } protected static JDOMExternalizableStringList reparseProperties( @NotNull final String properties) { final JDOMExternalizableStringList result = new JDOMExternalizableStringList(); final StringTokenizer tokenizer = new StringTokenizer(properties, ","); while (tokenizer.hasMoreTokens()) { result.add(tokenizer.nextToken().toLowerCase().trim()); } return result; } public void setAdditionalValues(@NotNull final String values) { myValues = reparseProperties(values); } protected String getPanelTitle() { return XmlBundle.message("html.inspections.unknown.tag.title"); } protected boolean isCustomValue(@NotNull final String value) { return myValues.contains(value.toLowerCase()); } public void addCustomPropertyName(@NotNull final String text) { final String s = text.trim().toLowerCase(); if (!isCustomValue(s)) { myValues.add(s); } if (!isCustomValuesEnabled()) { myCustomValuesEnabled = true; } } public boolean isCustomValuesEnabled() { return myCustomValuesEnabled; } private static boolean isAbstractDescriptor(XmlElementDescriptor descriptor) { return descriptor == null || descriptor instanceof AnyXmlElementDescriptor; } // this hack is needed, because we temporarily ignore svg and mathML namespaces // todo: provide schemas for svg and mathML and remove this in IDEA XI private static boolean isInSpecialHtml5Namespace(XmlTag tag) { final String ns = tag.getNamespace(); return HtmlUtil.SVG_NAMESPACE.equals(ns) || HtmlUtil.MATH_ML_NAMESPACE.endsWith(ns); } @Override protected void checkTag( @NotNull final XmlTag tag, @NotNull final ProblemsHolder holder, final boolean isOnTheFly) { if (!(tag instanceof HtmlTag) || !XmlHighlightVisitor.shouldBeValidated(tag) || isInSpecialHtml5Namespace(tag)) { return; } XmlElementDescriptor descriptorFromContext = XmlUtil.getDescriptorFromContext(tag); PsiElement parent = tag.getParent(); XmlElementDescriptor parentDescriptor = parent instanceof XmlTag ? ((XmlTag) parent).getDescriptor() : null; XmlElementDescriptor ownDescriptor = isAbstractDescriptor(descriptorFromContext) ? tag.getDescriptor() : descriptorFromContext; if (isAbstractDescriptor(ownDescriptor) || (parentDescriptor instanceof HtmlElementDescriptorImpl && ownDescriptor instanceof HtmlElementDescriptorImpl && isAbstractDescriptor(descriptorFromContext))) { final String name = tag.getName(); if (!isCustomValuesEnabled() || !isCustomValue(name)) { final AddCustomTagOrAttributeIntentionAction action = new AddCustomTagOrAttributeIntentionAction( TAG_KEY, name, XmlEntitiesInspection.UNKNOWN_TAG); // todo: support "element is not allowed" message for html5 // some tags in html5 cannot be found in xhtml5.xsd if they are located in incorrect // context, so they get any-element descriptor (ex. "canvas: tag) final String message = isAbstractDescriptor(ownDescriptor) ? XmlErrorMessages.message("unknown.html.tag", name) : XmlErrorMessages.message("element.is.not.allowed.here", name); final PsiElement startTagName = XmlTagUtil.getStartTagNameElement(tag); assert startTagName != null; final PsiElement endTagName = XmlTagUtil.getEndTagNameElement(tag); List<LocalQuickFix> quickfixes = new ArrayList<LocalQuickFix>(); quickfixes.add(action); if (isOnTheFly) { ContainerUtil.addIfNotNull( CreateNSDeclarationIntentionFix.createFix(startTagName, ""), quickfixes); } if (HtmlUtil.isHtml5Tag(name) && !HtmlUtil.hasNonHtml5Doctype(tag)) { quickfixes.add(new SwitchToHtml5WithHighPriorityAction()); } ProblemHighlightType highlightType = tag.getContainingFile().getContext() == null ? ProblemHighlightType.GENERIC_ERROR_OR_WARNING : ProblemHighlightType.INFORMATION; if (startTagName.getTextLength() > 0) { holder.registerProblem( startTagName, message, highlightType, quickfixes.toArray(new LocalQuickFix[quickfixes.size()])); } if (endTagName != null) { holder.registerProblem( endTagName, message, highlightType, quickfixes.toArray(new LocalQuickFix[quickfixes.size()])); } } } } }
/** @author cdr */ class PassExecutorService implements Disposable { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.PassExecutorService"); private static final boolean CHECK_CONSISTENCY = ApplicationManager.getApplication().isUnitTestMode(); private final Map<ScheduledPass, Job<Void>> mySubmittedPasses = new ConcurrentHashMap<>(); private final Project myProject; private volatile boolean isDisposed; private final AtomicInteger nextPassId = new AtomicInteger(100); PassExecutorService(@NotNull Project project) { myProject = project; } @Override public void dispose() { cancelAll(true); isDisposed = true; } void cancelAll(boolean waitForTermination) { for (Job<Void> submittedPass : mySubmittedPasses.values()) { submittedPass.cancel(); } if (waitForTermination) { try { while (!waitFor(50)) { int i = 0; } } catch (ProcessCanceledException ignored) { } catch (Error | RuntimeException e) { throw e; } catch (Throwable throwable) { LOG.error(throwable); } } mySubmittedPasses.clear(); } void submitPasses( @NotNull Map<FileEditor, HighlightingPass[]> passesMap, @NotNull DaemonProgressIndicator updateProgress) { if (isDisposed()) return; // null keys are ok MultiMap<Document, FileEditor> documentToEditors = MultiMap.createSet(); MultiMap<FileEditor, TextEditorHighlightingPass> documentBoundPasses = MultiMap.createSmart(); MultiMap<FileEditor, EditorBoundHighlightingPass> editorBoundPasses = MultiMap.createSmart(); List<Pair<FileEditor, TextEditorHighlightingPass>> passesWithNoDocuments = new ArrayList<>(); Set<VirtualFile> vFiles = new HashSet<>(); for (Map.Entry<FileEditor, HighlightingPass[]> entry : passesMap.entrySet()) { FileEditor fileEditor = entry.getKey(); HighlightingPass[] passes = entry.getValue(); Document document; if (fileEditor instanceof TextEditor) { Editor editor = ((TextEditor) fileEditor).getEditor(); LOG.assertTrue(!(editor instanceof EditorWindow)); document = editor.getDocument(); } else { VirtualFile virtualFile = ((FileEditorManagerEx) FileEditorManager.getInstance(myProject)).getFile(fileEditor); document = virtualFile == null ? null : FileDocumentManager.getInstance().getDocument(virtualFile); } if (document != null) { vFiles.add(FileDocumentManager.getInstance().getFile(document)); } int prevId = 0; for (final HighlightingPass pass : passes) { if (pass instanceof EditorBoundHighlightingPass) { EditorBoundHighlightingPass editorPass = (EditorBoundHighlightingPass) pass; editorPass.setId( nextPassId.incrementAndGet()); // have to make ids unique for this document editorBoundPasses.putValue(fileEditor, editorPass); } else { TextEditorHighlightingPass textEditorHighlightingPass = convertToTextHighlightingPass(pass, document, nextPassId, prevId); document = textEditorHighlightingPass.getDocument(); documentBoundPasses.putValue(fileEditor, textEditorHighlightingPass); if (document == null) { passesWithNoDocuments.add(Pair.create(fileEditor, textEditorHighlightingPass)); } else { documentToEditors.putValue(document, fileEditor); } prevId = textEditorHighlightingPass.getId(); } } } List<ScheduledPass> freePasses = new ArrayList<>(documentToEditors.size() * 5); List<ScheduledPass> dependentPasses = new ArrayList<>(documentToEditors.size() * 10); // (fileEditor, passId) -> created pass Map<Pair<FileEditor, Integer>, ScheduledPass> toBeSubmitted = new THashMap<>(passesMap.size()); final AtomicInteger threadsToStartCountdown = new AtomicInteger(0); for (Map.Entry<Document, Collection<FileEditor>> entry : documentToEditors.entrySet()) { Collection<FileEditor> fileEditors = entry.getValue(); Document document = entry.getKey(); FileEditor preferredFileEditor = getPreferredFileEditor(document, fileEditors); List<TextEditorHighlightingPass> passes = (List<TextEditorHighlightingPass>) documentBoundPasses.get(preferredFileEditor); if (passes.isEmpty()) { continue; } sortById(passes); for (TextEditorHighlightingPass currentPass : passes) { createScheduledPass( preferredFileEditor, currentPass, toBeSubmitted, passes, freePasses, dependentPasses, updateProgress, threadsToStartCountdown); } } for (Map.Entry<FileEditor, Collection<EditorBoundHighlightingPass>> entry : editorBoundPasses.entrySet()) { FileEditor fileEditor = entry.getKey(); Collection<EditorBoundHighlightingPass> createdEditorBoundPasses = entry.getValue(); List<TextEditorHighlightingPass> createdDocumentBoundPasses = (List<TextEditorHighlightingPass>) documentBoundPasses.get(fileEditor); List<TextEditorHighlightingPass> allCreatedPasses = new ArrayList<>(createdDocumentBoundPasses); allCreatedPasses.addAll(createdEditorBoundPasses); for (EditorBoundHighlightingPass pass : createdEditorBoundPasses) { createScheduledPass( fileEditor, pass, toBeSubmitted, allCreatedPasses, freePasses, dependentPasses, updateProgress, threadsToStartCountdown); } } for (Pair<FileEditor, TextEditorHighlightingPass> pair : passesWithNoDocuments) { FileEditor fileEditor = pair.first; TextEditorHighlightingPass pass = pair.second; createScheduledPass( fileEditor, pass, toBeSubmitted, ContainerUtil.emptyList(), freePasses, dependentPasses, updateProgress, threadsToStartCountdown); } if (CHECK_CONSISTENCY && !ApplicationInfoImpl.isInPerformanceTest()) { assertConsistency(freePasses, toBeSubmitted, threadsToStartCountdown); } log( updateProgress, null, vFiles + " ----- starting " + threadsToStartCountdown.get(), freePasses); for (ScheduledPass dependentPass : dependentPasses) { mySubmittedPasses.put(dependentPass, Job.NULL_JOB); } for (ScheduledPass freePass : freePasses) { submit(freePass); } } private void assertConsistency( List<ScheduledPass> freePasses, Map<Pair<FileEditor, Integer>, ScheduledPass> toBeSubmitted, AtomicInteger threadsToStartCountdown) { assert threadsToStartCountdown.get() == toBeSubmitted.size(); TIntObjectHashMap<Pair<ScheduledPass, Integer>> id2Visits = new TIntObjectHashMap<>(); for (ScheduledPass freePass : freePasses) { id2Visits.put(freePass.myPass.getId(), Pair.create(freePass, 0)); checkConsistency(freePass, id2Visits); } id2Visits.forEachEntry( (id, pair) -> { int count = pair.second; assert count == 0 : id; return true; }); assert id2Visits.size() == threadsToStartCountdown.get(); } private void checkConsistency( ScheduledPass pass, TIntObjectHashMap<Pair<ScheduledPass, Integer>> id2Visits) { for (ScheduledPass succ : ContainerUtil.concat(pass.mySuccessorsOnCompletion, pass.mySuccessorsOnSubmit)) { int succId = succ.myPass.getId(); Pair<ScheduledPass, Integer> succPair = id2Visits.get(succId); if (succPair == null) { succPair = Pair.create(succ, succ.myRunningPredecessorsCount.get()); id2Visits.put(succId, succPair); } int newPred = succPair.second - 1; id2Visits.put(succId, Pair.create(succ, newPred)); assert newPred >= 0; if (newPred == 0) { checkConsistency(succ, id2Visits); } } } @NotNull private TextEditorHighlightingPass convertToTextHighlightingPass( @NotNull final HighlightingPass pass, final Document document, @NotNull AtomicInteger id, int previousPassId) { TextEditorHighlightingPass textEditorHighlightingPass; if (pass instanceof TextEditorHighlightingPass) { textEditorHighlightingPass = (TextEditorHighlightingPass) pass; } else { // run all passes in sequence textEditorHighlightingPass = new TextEditorHighlightingPass(myProject, document, true) { @Override public void doCollectInformation(@NotNull ProgressIndicator progress) { pass.collectInformation(progress); } @Override public void doApplyInformationToEditor() { pass.applyInformationToEditor(); } }; textEditorHighlightingPass.setId(id.incrementAndGet()); if (previousPassId != 0) { textEditorHighlightingPass.setCompletionPredecessorIds(new int[] {previousPassId}); } } return textEditorHighlightingPass; } @NotNull private FileEditor getPreferredFileEditor( Document document, @NotNull Collection<FileEditor> fileEditors) { assert !fileEditors.isEmpty(); if (document != null) { final VirtualFile file = FileDocumentManager.getInstance().getFile(document); if (file != null) { final FileEditor selected = FileEditorManager.getInstance(myProject).getSelectedEditor(file); if (selected != null && fileEditors.contains(selected)) { return selected; } } } return fileEditors.iterator().next(); } @NotNull private ScheduledPass createScheduledPass( @NotNull FileEditor fileEditor, @NotNull TextEditorHighlightingPass pass, @NotNull Map<Pair<FileEditor, Integer>, ScheduledPass> toBeSubmitted, @NotNull List<TextEditorHighlightingPass> textEditorHighlightingPasses, @NotNull List<ScheduledPass> freePasses, @NotNull List<ScheduledPass> dependentPasses, @NotNull DaemonProgressIndicator updateProgress, @NotNull AtomicInteger threadsToStartCountdown) { int passId = pass.getId(); Pair<FileEditor, Integer> key = Pair.create(fileEditor, passId); ScheduledPass scheduledPass = toBeSubmitted.get(key); if (scheduledPass != null) return scheduledPass; scheduledPass = new ScheduledPass(fileEditor, pass, updateProgress, threadsToStartCountdown); threadsToStartCountdown.incrementAndGet(); toBeSubmitted.put(key, scheduledPass); for (int predecessorId : pass.getCompletionPredecessorIds()) { ScheduledPass predecessor = findOrCreatePredecessorPass( fileEditor, toBeSubmitted, textEditorHighlightingPasses, freePasses, dependentPasses, updateProgress, threadsToStartCountdown, predecessorId); if (predecessor != null) { predecessor.addSuccessorOnCompletion(scheduledPass); } } for (int predecessorId : pass.getStartingPredecessorIds()) { ScheduledPass predecessor = findOrCreatePredecessorPass( fileEditor, toBeSubmitted, textEditorHighlightingPasses, freePasses, dependentPasses, updateProgress, threadsToStartCountdown, predecessorId); if (predecessor != null) { predecessor.addSuccessorOnSubmit(scheduledPass); } } if (scheduledPass.myRunningPredecessorsCount.get() == 0 && !freePasses.contains(scheduledPass)) { freePasses.add(scheduledPass); } else if (!dependentPasses.contains(scheduledPass)) { dependentPasses.add(scheduledPass); } if (pass.isRunIntentionPassAfter() && fileEditor instanceof TextEditor) { Editor editor = ((TextEditor) fileEditor).getEditor(); ShowIntentionsPass ip = new ShowIntentionsPass(myProject, editor, -1); ip.setId(nextPassId.incrementAndGet()); ip.setCompletionPredecessorIds(new int[] {scheduledPass.myPass.getId()}); createScheduledPass( fileEditor, ip, toBeSubmitted, textEditorHighlightingPasses, freePasses, dependentPasses, updateProgress, threadsToStartCountdown); } return scheduledPass; } private ScheduledPass findOrCreatePredecessorPass( @NotNull FileEditor fileEditor, @NotNull Map<Pair<FileEditor, Integer>, ScheduledPass> toBeSubmitted, @NotNull List<TextEditorHighlightingPass> textEditorHighlightingPasses, @NotNull List<ScheduledPass> freePasses, @NotNull List<ScheduledPass> dependentPasses, @NotNull DaemonProgressIndicator updateProgress, @NotNull AtomicInteger myThreadsToStartCountdown, final int predecessorId) { Pair<FileEditor, Integer> predKey = Pair.create(fileEditor, predecessorId); ScheduledPass predecessor = toBeSubmitted.get(predKey); if (predecessor == null) { TextEditorHighlightingPass textEditorPass = findPassById(predecessorId, textEditorHighlightingPasses); predecessor = textEditorPass == null ? null : createScheduledPass( fileEditor, textEditorPass, toBeSubmitted, textEditorHighlightingPasses, freePasses, dependentPasses, updateProgress, myThreadsToStartCountdown); } return predecessor; } private static TextEditorHighlightingPass findPassById( final int id, @NotNull List<TextEditorHighlightingPass> textEditorHighlightingPasses) { return ContainerUtil.find(textEditorHighlightingPasses, pass -> pass.getId() == id); } private void submit(@NotNull ScheduledPass pass) { if (!pass.myUpdateProgress.isCanceled()) { Job<Void> job = JobLauncher.getInstance() .submitToJobThread( pass, future -> { try { if (!future.isCancelled()) { // for canceled task .get() generates // CancellationException which is expensive future.get(); } } catch (CancellationException | InterruptedException ignored) { } catch (ExecutionException e) { LOG.error(e.getCause()); } }); mySubmittedPasses.put(pass, job); } } private class ScheduledPass implements Runnable { private final FileEditor myFileEditor; private final TextEditorHighlightingPass myPass; private final AtomicInteger myThreadsToStartCountdown; private final AtomicInteger myRunningPredecessorsCount = new AtomicInteger(0); private final Collection<ScheduledPass> mySuccessorsOnCompletion = new ArrayList<>(); private final Collection<ScheduledPass> mySuccessorsOnSubmit = new ArrayList<>(); private final DaemonProgressIndicator myUpdateProgress; private ScheduledPass( @NotNull FileEditor fileEditor, @NotNull TextEditorHighlightingPass pass, @NotNull DaemonProgressIndicator progressIndicator, @NotNull AtomicInteger threadsToStartCountdown) { myFileEditor = fileEditor; myPass = pass; myThreadsToStartCountdown = threadsToStartCountdown; myUpdateProgress = progressIndicator; } @Override public void run() { try { doRun(); } catch (RuntimeException | Error e) { saveException(e, myUpdateProgress); throw e; } } private void doRun() { if (myUpdateProgress.isCanceled()) return; log(myUpdateProgress, myPass, "Started. "); for (ScheduledPass successor : mySuccessorsOnSubmit) { int predecessorsToRun = successor.myRunningPredecessorsCount.decrementAndGet(); if (predecessorsToRun == 0) { submit(successor); } } ProgressManager.getInstance() .executeProcessUnderProgress( () -> { boolean success = ApplicationManagerEx.getApplicationEx() .tryRunReadAction( () -> { try { if (DumbService.getInstance(myProject).isDumb() && !DumbService.isDumbAware(myPass)) { return; } if (!myUpdateProgress.isCanceled() && !myProject.isDisposed()) { myPass.collectInformation(myUpdateProgress); } } catch (ProcessCanceledException e) { log(myUpdateProgress, myPass, "Canceled "); if (!myUpdateProgress.isCanceled()) { myUpdateProgress.cancel( e); // in case when some smart asses throw PCE just for fun } } catch (RuntimeException | Error e) { myUpdateProgress.cancel(e); LOG.error(e); throw e; } }); if (!success) { myUpdateProgress.cancel(); } }, myUpdateProgress); log(myUpdateProgress, myPass, "Finished. "); if (!myUpdateProgress.isCanceled()) { applyInformationToEditorsLater( myFileEditor, myPass, myUpdateProgress, myThreadsToStartCountdown); for (ScheduledPass successor : mySuccessorsOnCompletion) { int predecessorsToRun = successor.myRunningPredecessorsCount.decrementAndGet(); if (predecessorsToRun == 0) { submit(successor); } } } } @NonNls @Override public String toString() { return "SP: " + myPass; } private void addSuccessorOnCompletion(@NotNull ScheduledPass successor) { mySuccessorsOnCompletion.add(successor); successor.myRunningPredecessorsCount.incrementAndGet(); } private void addSuccessorOnSubmit(@NotNull ScheduledPass successor) { mySuccessorsOnSubmit.add(successor); successor.myRunningPredecessorsCount.incrementAndGet(); } } private void applyInformationToEditorsLater( @NotNull final FileEditor fileEditor, @NotNull final TextEditorHighlightingPass pass, @NotNull final DaemonProgressIndicator updateProgress, @NotNull final AtomicInteger threadsToStartCountdown) { ApplicationManager.getApplication() .invokeLater( (DumbAwareRunnable) () -> { if (isDisposed() || myProject.isDisposed()) { updateProgress.cancel(); } if (updateProgress.isCanceled()) { log(updateProgress, pass, " is canceled during apply, sorry"); return; } Document document = pass.getDocument(); try { if (fileEditor.getComponent().isDisplayable() || ApplicationManager.getApplication().isUnitTestMode()) { pass.applyInformationToEditor(); FileStatusMap fileStatusMap = DaemonCodeAnalyzerEx.getInstanceEx(myProject).getFileStatusMap(); if (document != null) { fileStatusMap.markFileUpToDate(document, pass.getId()); } log(updateProgress, pass, " Applied"); } } catch (ProcessCanceledException e) { log(updateProgress, pass, "Error " + e); throw e; } catch (RuntimeException e) { VirtualFile file = document == null ? null : FileDocumentManager.getInstance().getFile(document); FileType fileType = file == null ? null : file.getFileType(); String message = "Exception while applying information to " + fileEditor + "(" + fileType + ")"; log(updateProgress, pass, message + e); throw new RuntimeException(message, e); } if (threadsToStartCountdown.decrementAndGet() == 0) { log(updateProgress, pass, "Stopping "); updateProgress.stopIfRunning(); } else { log( updateProgress, pass, "Finished but there are passes in the queue: " + threadsToStartCountdown.get()); } }, Registry.is("ide.perProjectModality") ? ModalityState.defaultModalityState() : ModalityState.stateForComponent(fileEditor.getComponent())); } protected boolean isDisposed() { return isDisposed; } @NotNull List<TextEditorHighlightingPass> getAllSubmittedPasses() { List<TextEditorHighlightingPass> result = new ArrayList<>(mySubmittedPasses.size()); for (ScheduledPass scheduledPass : mySubmittedPasses.keySet()) { if (!scheduledPass.myUpdateProgress.isCanceled()) { result.add(scheduledPass.myPass); } } sortById(result); return result; } private static void sortById(@NotNull List<TextEditorHighlightingPass> result) { ContainerUtil.quickSort(result, (o1, o2) -> o1.getId() - o2.getId()); } private static int getThreadNum() { Matcher matcher = Pattern.compile("JobScheduler FJ pool (\\d*)/(\\d*)") .matcher(Thread.currentThread().getName()); String num = matcher.matches() ? matcher.group(1) : null; return StringUtil.parseInt(num, 0); } static void log( ProgressIndicator progressIndicator, TextEditorHighlightingPass pass, @NonNls @NotNull Object... info) { if (LOG.isDebugEnabled()) { CharSequence docText = pass == null || pass.getDocument() == null ? "" : ": '" + StringUtil.first(pass.getDocument().getCharsSequence(), 10, true) + "'"; synchronized (PassExecutorService.class) { String infos = StringUtil.join(info, Functions.TO_STRING(), " "); String message = StringUtil.repeatSymbol(' ', getThreadNum() * 4) + " " + pass + " " + infos + "; progress=" + (progressIndicator == null ? null : progressIndicator.hashCode()) + " " + (progressIndicator == null ? "?" : progressIndicator.isCanceled() ? "X" : "V") + docText; LOG.debug(message); // System.out.println(message); } } } private static final Key<Throwable> THROWABLE_KEY = Key.create("THROWABLE_KEY"); private static void saveException( @NotNull Throwable e, @NotNull DaemonProgressIndicator indicator) { indicator.putUserDataIfAbsent(THROWABLE_KEY, e); } @TestOnly static Throwable getSavedException(@NotNull DaemonProgressIndicator indicator) { return indicator.getUserData(THROWABLE_KEY); } // return true if terminated boolean waitFor(int millis) throws Throwable { try { for (Job<Void> job : mySubmittedPasses.values()) { job.waitForCompletion(millis); } return true; } catch (TimeoutException ignored) { return false; } catch (InterruptedException e) { return true; } catch (ExecutionException e) { throw e.getCause(); } } }
/** @author Gregory.Shrago */ public class DfaUtil { private static final Key<CachedValue<MultiValuesMap<PsiVariable, PsiExpression>>> DFA_VARIABLE_INFO_KEY = Key.create("DFA_VARIABLE_INFO_KEY"); private DfaUtil() {} private static final MultiValuesMap<PsiVariable, PsiExpression> TOO_COMPLEX = new MultiValuesMap<PsiVariable, PsiExpression>(); @Nullable("null means DFA analysis has failed (too complex to analyze)") public static Collection<PsiExpression> getCachedVariableValues( @Nullable final PsiVariable variable, @Nullable final PsiElement context) { if (variable == null || context == null) return Collections.emptyList(); CachedValue<MultiValuesMap<PsiVariable, PsiExpression>> cachedValue = context.getUserData(DFA_VARIABLE_INFO_KEY); if (cachedValue == null) { final PsiElement codeBlock = DfaPsiUtil.getEnclosingCodeBlock(variable, context); cachedValue = CachedValuesManager.getManager(context.getProject()) .createCachedValue( new CachedValueProvider<MultiValuesMap<PsiVariable, PsiExpression>>() { @Override public Result<MultiValuesMap<PsiVariable, PsiExpression>> compute() { final MultiValuesMap<PsiVariable, PsiExpression> result; if (codeBlock == null) { result = null; } else { final ValuableInstructionVisitor visitor = new ValuableInstructionVisitor(context); RunnerResult runnerResult = new ValuableDataFlowRunner().analyzeMethod(codeBlock, visitor); if (runnerResult == RunnerResult.OK) { result = visitor.myValues; } else { result = TOO_COMPLEX; } } return new Result<MultiValuesMap<PsiVariable, PsiExpression>>( result, variable); } }, false); context.putUserData(DFA_VARIABLE_INFO_KEY, cachedValue); } final MultiValuesMap<PsiVariable, PsiExpression> value = cachedValue.getValue(); if (value == TOO_COMPLEX) return null; final Collection<PsiExpression> expressions = value == null ? null : value.get(variable); return expressions == null ? Collections.<PsiExpression>emptyList() : expressions; } @NotNull public static Nullness checkNullness( @Nullable final PsiVariable variable, @Nullable final PsiElement context) { if (variable == null || context == null) return Nullness.UNKNOWN; final PsiElement codeBlock = DfaPsiUtil.getEnclosingCodeBlock(variable, context); if (codeBlock == null) { return Nullness.UNKNOWN; } final ValuableInstructionVisitor visitor = new ValuableInstructionVisitor(context); RunnerResult result = new ValuableDataFlowRunner().analyzeMethod(codeBlock, visitor); if (result != RunnerResult.OK) { return Nullness.UNKNOWN; } if (visitor.myNulls.contains(variable) && !visitor.myNotNulls.contains(variable)) return Nullness.NULLABLE; if (visitor.myNotNulls.contains(variable) && !visitor.myNulls.contains(variable)) return Nullness.NOT_NULL; return Nullness.UNKNOWN; } @NotNull public static Collection<? extends PsiElement> getPossibleInitializationElements( final PsiElement qualifierExpression) { if (qualifierExpression instanceof PsiMethodCallExpression) { return Collections.singletonList(qualifierExpression); } if (qualifierExpression instanceof PsiReferenceExpression) { final PsiElement targetElement = ((PsiReferenceExpression) qualifierExpression).resolve(); if (!(targetElement instanceof PsiVariable)) { return Collections.emptyList(); } final Collection<? extends PsiElement> variableValues = getCachedVariableValues((PsiVariable) targetElement, qualifierExpression); if (variableValues == null || variableValues.isEmpty()) { return DfaPsiUtil.getVariableAssignmentsInFile( (PsiVariable) targetElement, false, qualifierExpression); } return variableValues; } if (qualifierExpression instanceof PsiLiteralExpression) { return Collections.singletonList(qualifierExpression); } return Collections.emptyList(); } private static class ValuableInstructionVisitor extends StandardInstructionVisitor { final MultiValuesMap<PsiVariable, PsiExpression> myValues = new MultiValuesMap<PsiVariable, PsiExpression>(true); final Set<PsiVariable> myNulls = new THashSet<PsiVariable>(); final Set<PsiVariable> myNotNulls = new THashSet<PsiVariable>(); private final PsiElement myContext; public ValuableInstructionVisitor(@NotNull PsiElement context) { myContext = context; } @Override public DfaInstructionState[] visitPush( PushInstruction instruction, DataFlowRunner runner, DfaMemoryState memState) { if (myContext == instruction.getPlace()) { final Map<DfaVariableValue, DfaVariableState> map = ((ValuableDataFlowRunner.MyDfaMemoryState) memState).getVariableStates(); for (Map.Entry<DfaVariableValue, DfaVariableState> entry : map.entrySet()) { ValuableDataFlowRunner.ValuableDfaVariableState state = (ValuableDataFlowRunner.ValuableDfaVariableState) entry.getValue(); DfaVariableValue variableValue = entry.getKey(); final PsiExpression psiExpression = state.myExpression; if (psiExpression != null && variableValue.getQualifier() == null) { myValues.put(variableValue.getPsiVariable(), psiExpression); } } DfaValue value = instruction.getValue(); if (value instanceof DfaVariableValue && ((DfaVariableValue) value).getQualifier() == null) { if (memState.isNotNull((DfaVariableValue) value)) { myNotNulls.add(((DfaVariableValue) value).getPsiVariable()); } if (memState.isNull(value)) { myNulls.add(((DfaVariableValue) value).getPsiVariable()); } } } return super.visitPush(instruction, runner, memState); } @Override public DfaInstructionState[] visitAssign( AssignInstruction instruction, DataFlowRunner runner, DfaMemoryState memState) { final Instruction nextInstruction = runner.getInstruction(instruction.getIndex() + 1); final DfaValue dfaSource = memState.pop(); final DfaValue dfaDest = memState.pop(); if (dfaDest instanceof DfaVariableValue) { DfaVariableValue var = (DfaVariableValue) dfaDest; final PsiExpression rightValue = instruction.getRExpression(); final PsiElement parent = rightValue == null ? null : rightValue.getParent(); final IElementType type = parent instanceof PsiAssignmentExpression ? ((PsiAssignmentExpression) parent).getOperationTokenType() : JavaTokenType.EQ; // store current value - to use in case of '+=' final PsiExpression prevValue = ((ValuableDataFlowRunner.ValuableDfaVariableState) ((ValuableDataFlowRunner.MyDfaMemoryState) memState).getVariableState(var)) .myExpression; memState.setVarValue(var, dfaSource); // state may have been changed so re-retrieve it final ValuableDataFlowRunner.ValuableDfaVariableState curState = (ValuableDataFlowRunner.ValuableDfaVariableState) ((ValuableDataFlowRunner.MyDfaMemoryState) memState).getVariableState(var); final PsiExpression curValue = curState.myExpression; final PsiExpression nextValue; if (type == JavaTokenType.PLUSEQ && prevValue != null) { PsiExpression tmpExpression; try { tmpExpression = JavaPsiFacade.getElementFactory(myContext.getProject()) .createExpressionFromText( prevValue.getText() + "+" + rightValue.getText(), rightValue); } catch (Exception e) { tmpExpression = curValue == null ? rightValue : curValue; } nextValue = tmpExpression; } else { nextValue = curValue == null ? rightValue : curValue; } curState.myExpression = nextValue; } memState.push(dfaDest); return new DfaInstructionState[] {new DfaInstructionState(nextInstruction, memState)}; } } }
/** @author dsl */ public class ElementToWorkOn { public static final Key<PsiElement> PARENT = Key.create("PARENT"); private final PsiExpression myExpression; private final PsiLocalVariable myLocalVariable; public static final Key<String> PREFIX = Key.create("prefix"); public static final Key<String> SUFFIX = Key.create("suffix"); public static final Key<RangeMarker> TEXT_RANGE = Key.create("range"); public static final Key<Boolean> OUT_OF_CODE_BLOCK = Key.create("out_of_code_block"); private ElementToWorkOn(PsiLocalVariable localVariable, PsiExpression expr) { myLocalVariable = localVariable; myExpression = expr; } public PsiExpression getExpression() { return myExpression; } public PsiLocalVariable getLocalVariable() { return myLocalVariable; } public boolean isInvokedOnDeclaration() { return myExpression == null; } public static void processElementToWorkOn( final Editor editor, final PsiFile file, final String refactoringName, final String helpId, final Project project, final Pass<ElementToWorkOn> processor) { PsiLocalVariable localVar = null; PsiExpression expr = null; if (!editor.getSelectionModel().hasSelection()) { PsiElement element = TargetElementUtilBase.findTargetElement( editor, TargetElementUtilBase.ELEMENT_NAME_ACCEPTED | TargetElementUtilBase.REFERENCED_ELEMENT_ACCEPTED | TargetElementUtilBase.LOOKUP_ITEM_ACCEPTED); if (element instanceof PsiLocalVariable) { localVar = (PsiLocalVariable) element; final PsiElement elementAt = file.findElementAt(editor.getCaretModel().getOffset()); if (elementAt instanceof PsiIdentifier && elementAt.getParent() instanceof PsiReferenceExpression) { expr = (PsiExpression) elementAt.getParent(); } } else { final PsiLocalVariable variable = PsiTreeUtil.getParentOfType( file.findElementAt(editor.getCaretModel().getOffset()), PsiLocalVariable.class); final int offset = editor.getCaretModel().getOffset(); final PsiElement[] statementsInRange = IntroduceVariableBase.findStatementsAtOffset(editor, file, offset); if (statementsInRange.length == 1 && (PsiUtil.hasErrorElementChild(statementsInRange[0]) || !PsiUtil.isStatement(statementsInRange[0]))) { editor.getSelectionModel().selectLineAtCaret(); final ElementToWorkOn elementToWorkOn = getElementToWorkOn(editor, file, refactoringName, helpId, project, localVar, expr); if (elementToWorkOn == null || elementToWorkOn.getLocalVariable() == null && elementToWorkOn.getExpression() == null) { editor.getSelectionModel().removeSelection(); } } if (!editor.getSelectionModel().hasSelection()) { final List<PsiExpression> expressions = IntroduceVariableBase.collectExpressions(file, editor, offset, statementsInRange); if (expressions.isEmpty()) { editor.getSelectionModel().selectLineAtCaret(); } else if (expressions.size() == 1) { expr = expressions.get(0); } else { IntroduceTargetChooser.showChooser( editor, expressions, new Pass<PsiExpression>() { @Override public void pass(final PsiExpression selectedValue) { PsiLocalVariable var = null; // replace var if selected expression == var initializer if (variable != null && variable.getInitializer() == selectedValue) { var = variable; } processor.pass( getElementToWorkOn( editor, file, refactoringName, helpId, project, var, selectedValue)); } }, new PsiExpressionTrimRenderer.RenderFunction()); return; } } } } processor.pass( getElementToWorkOn(editor, file, refactoringName, helpId, project, localVar, expr)); } private static ElementToWorkOn getElementToWorkOn( final Editor editor, final PsiFile file, final String refactoringName, final String helpId, final Project project, PsiLocalVariable localVar, PsiExpression expr) { int startOffset = 0; int endOffset = 0; if (localVar == null && expr == null) { startOffset = editor.getSelectionModel().getSelectionStart(); endOffset = editor.getSelectionModel().getSelectionEnd(); expr = CodeInsightUtil.findExpressionInRange(file, startOffset, endOffset); if (expr == null) { PsiIdentifier ident = CodeInsightUtil.findElementInRange(file, startOffset, endOffset, PsiIdentifier.class); if (ident != null) { localVar = PsiTreeUtil.getParentOfType(ident, PsiLocalVariable.class); } } } if (expr == null && localVar == null) { PsiElement[] statements = CodeInsightUtil.findStatementsInRange(file, startOffset, endOffset); if (statements.length == 1 && statements[0] instanceof PsiExpressionStatement) { expr = ((PsiExpressionStatement) statements[0]).getExpression(); } else if (statements.length == 1 && statements[0] instanceof PsiDeclarationStatement) { PsiDeclarationStatement decl = (PsiDeclarationStatement) statements[0]; PsiElement[] declaredElements = decl.getDeclaredElements(); if (declaredElements.length == 1 && declaredElements[0] instanceof PsiLocalVariable) { localVar = (PsiLocalVariable) declaredElements[0]; } } } if (localVar == null && expr == null) { expr = IntroduceVariableBase.getSelectedExpression(project, file, startOffset, endOffset); } if (localVar == null && expr == null) { String message = RefactoringBundle.getCannotRefactorMessage( RefactoringBundle.message("error.wrong.caret.position.local.or.expression.name")); CommonRefactoringUtil.showErrorHint(project, editor, message, refactoringName, helpId); return null; } return new ElementToWorkOn(localVar, expr); } }
class UndoRedoStacksHolder { private final Key<LinkedList<UndoableGroup>> STACK_IN_DOCUMENT_KEY = Key.create("STACK_IN_DOCUMENT_KEY"); private final boolean myUndo; private final LinkedList<UndoableGroup> myGlobalStack = new LinkedList<UndoableGroup>(); private final Map<DocumentReference, LinkedList<UndoableGroup>> myDocumentStacks = new HashMap<DocumentReference, LinkedList<UndoableGroup>>(); private final WeakList<Document> myDocumentsWithStacks = new WeakList<Document>(); public UndoRedoStacksHolder(boolean isUndo) { myUndo = isUndo; } public LinkedList<UndoableGroup> getStack(Document d) { return getStack(createReferenceOrGetOriginal(d)); } private static DocumentReference createReferenceOrGetOriginal(Document d) { Document original = UndoManagerImpl.getOriginal(d); return DocumentReferenceManager.getInstance().create(original); } public LinkedList<UndoableGroup> getStack(@NotNull DocumentReference r) { return r.getFile() != null ? doGetStackForFile(r) : doGetStackForDocument(r); } private LinkedList<UndoableGroup> doGetStackForFile(DocumentReference r) { LinkedList<UndoableGroup> result = myDocumentStacks.get(r); if (result == null) { result = new LinkedList<UndoableGroup>(); myDocumentStacks.put(r, result); } return result; } private LinkedList<UndoableGroup> doGetStackForDocument(DocumentReference r) { // If document is not associated with file, we have to store its stack in document // itself to avoid memory leaks caused by holding stacks of all documents, ever created, here. // And to know, what documents do exist now, we have to maintain weak reference list of them. Document d = r.getDocument(); LinkedList<UndoableGroup> result = d.getUserData(STACK_IN_DOCUMENT_KEY); if (result == null) { result = new LinkedList<UndoableGroup>(); d.putUserData(STACK_IN_DOCUMENT_KEY, result); myDocumentsWithStacks.add(d); } return result; } public boolean hasActions(Collection<DocumentReference> refs) { if (refs.isEmpty()) return !myGlobalStack.isEmpty(); for (DocumentReference each : refs) { if (!getStack(each).isEmpty()) return true; } return false; } @NotNull public UndoableGroup getLastAction(Collection<DocumentReference> refs) { if (refs.isEmpty()) return myGlobalStack.getLast(); UndoableGroup mostRecentAction = null; int mostRecentDocTimestamp = myUndo ? -1 : Integer.MAX_VALUE; for (DocumentReference each : refs) { LinkedList<UndoableGroup> stack = getStack(each); // the stack for a document can be empty in case of compound editors with several documents if (stack.isEmpty()) continue; UndoableGroup lastAction = stack.getLast(); int timestamp = lastAction.getCommandTimestamp(); if (myUndo ? timestamp > mostRecentDocTimestamp : timestamp < mostRecentDocTimestamp) { mostRecentAction = lastAction; mostRecentDocTimestamp = timestamp; } } // result must not be null return mostRecentAction; } public Set<DocumentReference> collectClashingActions(UndoableGroup group) { Set<DocumentReference> result = new THashSet<DocumentReference>(); for (DocumentReference each : group.getAffectedDocuments()) { UndoableGroup last = getStack(each).getLast(); if (last != group) { result.addAll(last.getAffectedDocuments()); } } if (group.isGlobal()) { UndoableGroup last = myGlobalStack.getLast(); if (last != group) { result.addAll(last.getAffectedDocuments()); } } return result; } public void addToStacks(UndoableGroup group) { if (group.isGlobal()) doAddToStack(myGlobalStack, group, UndoManagerImpl.GLOBAL_UNDO_LIMIT); for (DocumentReference each : group.getAffectedDocuments()) { doAddToStack(getStack(each), group, UndoManagerImpl.LOCAL_UNDO_LIMIT); } } private void doAddToStack(LinkedList<UndoableGroup> stack, UndoableGroup group, int limit) { if (!group.isUndoable() && stack.isEmpty()) return; stack.addLast(group); while (stack.size() > limit) { stack.removeFirst(); } } public void removeFromStacks(UndoableGroup group) { if (group.getAffectedDocuments().isEmpty()) return; if (group.isGlobal()) { assert myGlobalStack.getLast() == group; myGlobalStack.removeLast(); } for (DocumentReference each : group.getAffectedDocuments()) { LinkedList<UndoableGroup> stack = getStack(each); assert stack.getLast() == group; stack.removeLast(); } } public void clearStacks(boolean clearGlobal, Set<DocumentReference> affectedDocuments) { if (clearGlobal) myGlobalStack.clear(); for (DocumentReference each : affectedDocuments) { List<UndoableGroup> stack = getStack(each); stack.clear(); if (each.getFile() != null) { myDocumentStacks.remove(each); } else { Document d = each.getDocument(); d.putUserData(STACK_IN_DOCUMENT_KEY, null); myDocumentsWithStacks.remove(d); } } } public void clearAllStacksInTests() { clearStacks(true, getAffectedDocuments()); } public void invalidateAllGlobalActions() { doInvalidateAllGlobalActions(myGlobalStack); for (DocumentReference each : getAffectedDocuments()) { doInvalidateAllGlobalActions(getStack(each)); } } private void doInvalidateAllGlobalActions(LinkedList<UndoableGroup> stack) { for (UndoableGroup g : stack) { g.invalidateIfGlobal(); } } public void collectAllAffectedDocuments(Collection<DocumentReference> result) { for (UndoableGroup each : myGlobalStack) { result.addAll(each.getAffectedDocuments()); } collectLocalAffectedDocuments(result); } private void collectLocalAffectedDocuments(Collection<DocumentReference> result) { result.addAll(myDocumentStacks.keySet()); for (Document each : myDocumentsWithStacks) { result.add(DocumentReferenceManager.getInstance().create(each)); } } private Set<DocumentReference> getAffectedDocuments() { Set<DocumentReference> result = new THashSet<DocumentReference>(); collectAllAffectedDocuments(result); return result; } public int getLastCommandTimestamp(DocumentReference r) { LinkedList<UndoableGroup> stack = getStack(r); if (stack.isEmpty()) return 0; return Math.max(stack.getFirst().getCommandTimestamp(), stack.getLast().getCommandTimestamp()); } }
public class IndentsPass extends TextEditorHighlightingPass implements DumbAware { private static final ConcurrentMap<IElementType, String> COMMENT_PREFIXES = ContainerUtil.newConcurrentMap(); private static final String NO_COMMENT_INFO_MARKER = "hopefully, noone uses this string as a comment prefix"; private static final Key<List<RangeHighlighter>> INDENT_HIGHLIGHTERS_IN_EDITOR_KEY = Key.create("INDENT_HIGHLIGHTERS_IN_EDITOR_KEY"); private static final Key<Long> LAST_TIME_INDENTS_BUILT = Key.create("LAST_TIME_INDENTS_BUILT"); private final EditorEx myEditor; private final PsiFile myFile; public static final Comparator<TextRange> RANGE_COMPARATOR = new Comparator<TextRange>() { @Override public int compare(TextRange o1, TextRange o2) { if (o1.getStartOffset() == o2.getStartOffset()) { return o1.getEndOffset() - o2.getEndOffset(); } return o1.getStartOffset() - o2.getStartOffset(); } }; private static final CustomHighlighterRenderer RENDERER = new CustomHighlighterRenderer() { @Override @SuppressWarnings({"AssignmentToForLoopParameter"}) public void paint( @NotNull Editor editor, @NotNull RangeHighlighter highlighter, @NotNull Graphics g) { int startOffset = highlighter.getStartOffset(); final Document doc = highlighter.getDocument(); if (startOffset >= doc.getTextLength()) return; final int endOffset = highlighter.getEndOffset(); final int endLine = doc.getLineNumber(endOffset); int off; int startLine = doc.getLineNumber(startOffset); IndentGuideDescriptor descriptor = editor.getIndentsModel().getDescriptor(startLine, endLine); final CharSequence chars = doc.getCharsSequence(); do { int start = doc.getLineStartOffset(startLine); int end = doc.getLineEndOffset(startLine); off = CharArrayUtil.shiftForward(chars, start, end, " \t"); startLine--; } while (startLine > 1 && off < doc.getTextLength() && chars.charAt(off) == '\n'); final VisualPosition startPosition = editor.offsetToVisualPosition(off); int indentColumn = startPosition.column; // It's considered that indent guide can cross not only white space but comments, javadocs // etc. Hence, there is a possible // case that the first indent guide line is, say, single-line comment where comment // symbols ('//') are located at the first // visual column. We need to calculate correct indent guide column then. int lineShift = 1; if (indentColumn <= 0 && descriptor != null) { indentColumn = descriptor.indentLevel; lineShift = 0; } if (indentColumn <= 0) return; final FoldingModel foldingModel = editor.getFoldingModel(); if (foldingModel.isOffsetCollapsed(off)) return; final FoldRegion headerRegion = foldingModel.getCollapsedRegionAtOffset(doc.getLineEndOffset(doc.getLineNumber(off))); final FoldRegion tailRegion = foldingModel.getCollapsedRegionAtOffset( doc.getLineStartOffset(doc.getLineNumber(endOffset))); if (tailRegion != null && tailRegion == headerRegion) return; final boolean selected; final IndentGuideDescriptor guide = editor.getIndentsModel().getCaretIndentGuide(); if (guide != null) { final CaretModel caretModel = editor.getCaretModel(); final int caretOffset = caretModel.getOffset(); selected = caretOffset >= off && caretOffset < endOffset && caretModel.getLogicalPosition().column == indentColumn; } else { selected = false; } Point start = editor.visualPositionToXY( new VisualPosition(startPosition.line + lineShift, indentColumn)); final VisualPosition endPosition = editor.offsetToVisualPosition(endOffset); Point end = editor.visualPositionToXY(new VisualPosition(endPosition.line, endPosition.column)); int maxY = end.y; if (endPosition.line == editor.offsetToVisualPosition(doc.getTextLength()).line) { maxY += editor.getLineHeight(); } Rectangle clip = g.getClipBounds(); if (clip != null) { if (clip.y >= maxY || clip.y + clip.height <= start.y) { return; } maxY = Math.min(maxY, clip.y + clip.height); } final EditorColorsScheme scheme = editor.getColorsScheme(); g.setColor( selected ? scheme.getColor(EditorColors.SELECTED_INDENT_GUIDE_COLOR) : scheme.getColor(EditorColors.INDENT_GUIDE_COLOR)); // There is a possible case that indent line intersects soft wrap-introduced text. // Example: // this is a long line <soft-wrap> // that| is soft-wrapped // | // | <- vertical indent // // Also it's possible that no additional intersections are added because of soft wrap: // this is a long line <soft-wrap> // | that is soft-wrapped // | // | <- vertical indent // We want to use the following approach then: // 1. Show only active indent if it crosses soft wrap-introduced text; // 2. Show indent as is if it doesn't intersect with soft wrap-introduced text; if (selected) { g.drawLine(start.x + 2, start.y, start.x + 2, maxY); } else { int y = start.y; int newY = start.y; SoftWrapModel softWrapModel = editor.getSoftWrapModel(); int lineHeight = editor.getLineHeight(); for (int i = Math.max(0, startLine + lineShift); i < endLine && newY < maxY; i++) { List<? extends SoftWrap> softWraps = softWrapModel.getSoftWrapsForLine(i); int logicalLineHeight = softWraps.size() * lineHeight; if (i > startLine + lineShift) { logicalLineHeight += lineHeight; // We assume that initial 'y' value points just below the target // line. } if (!softWraps.isEmpty() && softWraps.get(0).getIndentInColumns() < indentColumn) { if (y < newY || i > startLine + lineShift) { // There is a possible case that soft wrap is located on // indent start line. g.drawLine(start.x + 2, y, start.x + 2, newY + lineHeight); } newY += logicalLineHeight; y = newY; } else { newY += logicalLineHeight; } FoldRegion foldRegion = foldingModel.getCollapsedRegionAtOffset(doc.getLineEndOffset(i)); if (foldRegion != null && foldRegion.getEndOffset() < doc.getTextLength()) { i = doc.getLineNumber(foldRegion.getEndOffset()); } } if (y < maxY) { g.drawLine(start.x + 2, y, start.x + 2, maxY); } } } }; private volatile List<TextRange> myRanges; private volatile List<IndentGuideDescriptor> myDescriptors; public IndentsPass(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) { super(project, editor.getDocument(), false); myEditor = (EditorEx) editor; myFile = file; } @Override public void doCollectInformation(@NotNull ProgressIndicator progress) { assert myDocument != null; final Long stamp = myEditor.getUserData(LAST_TIME_INDENTS_BUILT); if (stamp != null && stamp.longValue() == nowStamp()) return; myDescriptors = buildDescriptors(); ArrayList<TextRange> ranges = new ArrayList<TextRange>(); for (IndentGuideDescriptor descriptor : myDescriptors) { ProgressManager.checkCanceled(); int endOffset = descriptor.endLine < myDocument.getLineCount() ? myDocument.getLineStartOffset(descriptor.endLine) : myDocument.getTextLength(); ranges.add(new TextRange(myDocument.getLineStartOffset(descriptor.startLine), endOffset)); } Collections.sort(ranges, RANGE_COMPARATOR); myRanges = ranges; } private long nowStamp() { if (!myEditor.getSettings().isIndentGuidesShown()) return -1; assert myDocument != null; return myDocument.getModificationStamp(); } @Override public void doApplyInformationToEditor() { final Long stamp = myEditor.getUserData(LAST_TIME_INDENTS_BUILT); if (stamp != null && stamp.longValue() == nowStamp()) return; List<RangeHighlighter> oldHighlighters = myEditor.getUserData(INDENT_HIGHLIGHTERS_IN_EDITOR_KEY); final List<RangeHighlighter> newHighlighters = new ArrayList<RangeHighlighter>(); final MarkupModel mm = myEditor.getMarkupModel(); int curRange = 0; if (oldHighlighters != null) { int curHighlight = 0; while (curRange < myRanges.size() && curHighlight < oldHighlighters.size()) { TextRange range = myRanges.get(curRange); RangeHighlighter highlighter = oldHighlighters.get(curHighlight); int cmp = compare(range, highlighter); if (cmp < 0) { newHighlighters.add(createHighlighter(mm, range)); curRange++; } else if (cmp > 0) { highlighter.dispose(); curHighlight++; } else { newHighlighters.add(highlighter); curHighlight++; curRange++; } } for (; curHighlight < oldHighlighters.size(); curHighlight++) { RangeHighlighter highlighter = oldHighlighters.get(curHighlight); highlighter.dispose(); } } final int startRangeIndex = curRange; assert myDocument != null; DocumentUtil.executeInBulk( myDocument, myRanges.size() > 10000, new Runnable() { @Override public void run() { for (int i = startRangeIndex; i < myRanges.size(); i++) { newHighlighters.add(createHighlighter(mm, myRanges.get(i))); } } }); myEditor.putUserData(INDENT_HIGHLIGHTERS_IN_EDITOR_KEY, newHighlighters); myEditor.putUserData(LAST_TIME_INDENTS_BUILT, nowStamp()); myEditor.getIndentsModel().assumeIndents(myDescriptors); } private List<IndentGuideDescriptor> buildDescriptors() { if (!myEditor.getSettings().isIndentGuidesShown()) return Collections.emptyList(); IndentsCalculator calculator = new IndentsCalculator(); calculator.calculate(); int[] lineIndents = calculator.lineIndents; TIntIntHashMap effectiveCommentColumns = calculator.indentAfterUncomment; List<IndentGuideDescriptor> descriptors = new ArrayList<IndentGuideDescriptor>(); IntStack lines = new IntStack(); IntStack indents = new IntStack(); lines.push(0); indents.push(0); assert myDocument != null; final CharSequence chars = myDocument.getCharsSequence(); for (int line = 1; line < lineIndents.length; line++) { ProgressManager.checkCanceled(); int curIndent = lineIndents[line]; while (!indents.empty() && curIndent <= indents.peek()) { ProgressManager.checkCanceled(); final int level = indents.pop(); int startLine = lines.pop(); if (level > 0) { boolean addDescriptor = effectiveCommentColumns.contains(startLine); // Indent started at comment if (!addDescriptor) { for (int i = startLine; i < line; i++) { if (level != lineIndents[i] && level != effectiveCommentColumns.get(i)) { addDescriptor = true; break; } } } if (addDescriptor) { descriptors.add(createDescriptor(level, startLine, line, chars)); } } } int prevLine = line - 1; int prevIndent = lineIndents[prevLine]; if (curIndent - prevIndent > 1) { lines.push(prevLine); indents.push(prevIndent); } } while (!indents.empty()) { ProgressManager.checkCanceled(); final int level = indents.pop(); int startLine = lines.pop(); if (level > 0) { descriptors.add(createDescriptor(level, startLine, myDocument.getLineCount(), chars)); } } return descriptors; } private IndentGuideDescriptor createDescriptor( int level, int startLine, int endLine, CharSequence chars) { while (startLine > 0 && isBlankLine(startLine, chars)) startLine--; return new IndentGuideDescriptor(level, startLine, endLine); } private boolean isBlankLine(int line, CharSequence chars) { Document document = myDocument; if (document == null) { return true; } int startOffset = document.getLineStartOffset(line); int endOffset = document.getLineEndOffset(line); return CharArrayUtil.shiftForward(chars, startOffset, endOffset, " \t") >= myDocument.getLineEndOffset(line); } /** * We want to treat comments specially in a way to skip comment prefix on line indent calculation. * * <p>Example: * * <pre> * if (true) { * int i1; * // int i2; * int i3; * } * </pre> * * We want to use 'int i2;' start offset as the third line indent (though it has non-white space * comment prefix (//) at the first column. * * <p>This method tries to parse comment prefix for the language implied by the given comment * type. It uses {@link #NO_COMMENT_INFO_MARKER} as an indicator that that information is * unavailable * * @param commentType target comment type * @return prefix of the comment denoted by the given type if any; {@link #NO_COMMENT_INFO_MARKER} * otherwise */ @NotNull private static String getCommentPrefix(@NotNull IElementType commentType) { Commenter c = LanguageCommenters.INSTANCE.forLanguage(commentType.getLanguage()); if (!(c instanceof CodeDocumentationAwareCommenter)) { COMMENT_PREFIXES.put(commentType, NO_COMMENT_INFO_MARKER); return NO_COMMENT_INFO_MARKER; } CodeDocumentationAwareCommenter commenter = (CodeDocumentationAwareCommenter) c; IElementType lineCommentType = commenter.getLineCommentTokenType(); String lineCommentPrefix = commenter.getLineCommentPrefix(); if (lineCommentType != null) { COMMENT_PREFIXES.put( lineCommentType, lineCommentPrefix == null ? NO_COMMENT_INFO_MARKER : lineCommentPrefix); } IElementType blockCommentType = commenter.getBlockCommentTokenType(); String blockCommentPrefix = commenter.getBlockCommentPrefix(); if (blockCommentType != null) { COMMENT_PREFIXES.put( blockCommentType, blockCommentPrefix == null ? NO_COMMENT_INFO_MARKER : blockCommentPrefix); } IElementType docCommentType = commenter.getDocumentationCommentTokenType(); String docCommentPrefix = commenter.getDocumentationCommentPrefix(); if (docCommentType != null) { COMMENT_PREFIXES.put( docCommentType, docCommentPrefix == null ? NO_COMMENT_INFO_MARKER : docCommentPrefix); } COMMENT_PREFIXES.putIfAbsent(commentType, NO_COMMENT_INFO_MARKER); return COMMENT_PREFIXES.get(commentType); } @NotNull private static RangeHighlighter createHighlighter(MarkupModel mm, TextRange range) { final RangeHighlighter highlighter = mm.addRangeHighlighter( range.getStartOffset(), range.getEndOffset(), 0, null, HighlighterTargetArea.EXACT_RANGE); highlighter.setCustomRenderer(RENDERER); return highlighter; } private static int compare(@NotNull TextRange r, @NotNull RangeHighlighter h) { int answer = r.getStartOffset() - h.getStartOffset(); return answer != 0 ? answer : r.getEndOffset() - h.getEndOffset(); } private class IndentsCalculator { @NotNull public final Map<Language, TokenSet> myComments = ContainerUtilRt.newHashMap(); /** * We need to treat specially commented lines. Consider a situation like below: * * <pre> * void test() { * if (true) { * int i; * // int j; * } * } * </pre> * * We don't want to show indent guide after 'int i;' line because un-commented line below ('int * j;') would have the same indent level. That's why we remember 'indents after un-comment' at * this collection. */ @NotNull public final TIntIntHashMap /* line -> indent column after un-comment */ indentAfterUncomment = new TIntIntHashMap(); @NotNull public final int[] lineIndents; @NotNull public final CharSequence myChars; IndentsCalculator() { assert myDocument != null; lineIndents = new int[myDocument.getLineCount()]; myChars = myDocument.getCharsSequence(); } /** Calculates line indents for the {@link #myDocument target document}. */ void calculate() { final FileType fileType = myFile.getFileType(); int prevLineIndent = -1; for (int line = 0; line < lineIndents.length; line++) { ProgressManager.checkCanceled(); int lineStart = myDocument.getLineStartOffset(line); int lineEnd = myDocument.getLineEndOffset(line); final int nonWhitespaceOffset = CharArrayUtil.shiftForward(myChars, lineStart, lineEnd, " \t"); if (nonWhitespaceOffset == lineEnd) { lineIndents[line] = -1; // Blank line marker } else { final int column = ((EditorImpl) myEditor).calcColumnNumber(nonWhitespaceOffset, line, true, myChars); if (prevLineIndent > 0 && prevLineIndent > column) { lineIndents[line] = calcIndent(line, nonWhitespaceOffset, lineEnd, column); } else { lineIndents[line] = column; } prevLineIndent = lineIndents[line]; } } int topIndent = 0; for (int line = 0; line < lineIndents.length; line++) { ProgressManager.checkCanceled(); if (lineIndents[line] >= 0) { topIndent = lineIndents[line]; } else { int startLine = line; while (line < lineIndents.length && lineIndents[line] < 0) { //noinspection AssignmentToForLoopParameter line++; } int bottomIndent = line < lineIndents.length ? lineIndents[line] : topIndent; int indent = Math.min(topIndent, bottomIndent); if (bottomIndent < topIndent) { int lineStart = myDocument.getLineStartOffset(line); int lineEnd = myDocument.getLineEndOffset(line); int nonWhitespaceOffset = CharArrayUtil.shiftForward(myChars, lineStart, lineEnd, " \t"); HighlighterIterator iterator = myEditor.getHighlighter().createIterator(nonWhitespaceOffset); if (BraceMatchingUtil.isRBraceToken(iterator, myChars, fileType)) { indent = topIndent; } } for (int blankLine = startLine; blankLine < line; blankLine++) { assert lineIndents[blankLine] == -1; lineIndents[blankLine] = Math.min(topIndent, indent); } //noinspection AssignmentToForLoopParameter line--; // will be incremented back at the end of the loop; } } } /** * Tries to calculate given line's indent column assuming that there might be a comment at the * given indent offset (see {@link #getCommentPrefix(IElementType)}). * * @param line target line * @param indentOffset start indent offset to use for the given line * @param lineEndOffset given line's end offset * @param fallbackColumn column to return if it's not possible to apply comment-specific indent * calculation rules * @return given line's indent column to use */ private int calcIndent(int line, int indentOffset, int lineEndOffset, int fallbackColumn) { final HighlighterIterator it = myEditor.getHighlighter().createIterator(indentOffset); IElementType tokenType = it.getTokenType(); Language language = tokenType.getLanguage(); TokenSet comments = myComments.get(language); if (comments == null) { ParserDefinition definition = LanguageParserDefinitions.INSTANCE.forLanguage(language); if (definition != null) { comments = definition.getCommentTokens(); } if (comments == null) { return fallbackColumn; } else { myComments.put(language, comments); } } if (comments.contains(tokenType) && indentOffset == it.getStart()) { String prefix = COMMENT_PREFIXES.get(tokenType); if (prefix == null) { prefix = getCommentPrefix(tokenType); } if (!NO_COMMENT_INFO_MARKER.equals(prefix)) { final int indentInsideCommentOffset = CharArrayUtil.shiftForward( myChars, indentOffset + prefix.length(), lineEndOffset, " \t"); if (indentInsideCommentOffset < lineEndOffset) { int indent = myEditor.calcColumnNumber(indentInsideCommentOffset, line); indentAfterUncomment.put(line, indent - prefix.length()); return indent; } } } return fallbackColumn; } } }
public class FoldingModelImpl implements FoldingModelEx, PrioritizedDocumentListener, Dumpable { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorFoldingModelImpl"); private static final Key<LogicalPosition> SAVED_CARET_POSITION = Key.create("saved.position.before.folding"); private final List<FoldingListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); private boolean myIsFoldingEnabled; private final EditorImpl myEditor; private final FoldRegionsTree myFoldTree; private TextAttributes myFoldTextAttributes; private boolean myIsBatchFoldingProcessing; private boolean myDoNotCollapseCaret; private boolean myFoldRegionsProcessed; private int mySavedCaretShift; private final MultiMap<FoldingGroup, FoldRegion> myGroups = new MultiMap<FoldingGroup, FoldRegion>(); private boolean myDocumentChangeProcessed = true; public FoldingModelImpl(EditorImpl editor) { myEditor = editor; myIsFoldingEnabled = true; myIsBatchFoldingProcessing = false; myDoNotCollapseCaret = false; myFoldTree = new FoldRegionsTree() { @Override protected boolean isFoldingEnabled() { return FoldingModelImpl.this.isFoldingEnabled(); } }; myFoldRegionsProcessed = false; refreshSettings(); } @NotNull public List<FoldRegion> getGroupedRegions(@NotNull FoldingGroup group) { return (List<FoldRegion>) myGroups.get(group); } @NotNull public FoldRegion getFirstRegion(@NotNull FoldingGroup group, FoldRegion child) { final List<FoldRegion> regions = getGroupedRegions(group); if (regions.isEmpty()) { final boolean inAll = Arrays.asList(getAllFoldRegions()).contains(child); throw new AssertionError( "Folding group without children; the known child is in all: " + inAll); } FoldRegion main = regions.get(0); for (int i = 1; i < regions.size(); i++) { FoldRegion region = regions.get(i); if (main.getStartOffset() > region.getStartOffset()) { main = region; } } return main; } public int getEndOffset(@NotNull FoldingGroup group) { final List<FoldRegion> regions = getGroupedRegions(group); int endOffset = 0; for (FoldRegion region : regions) { if (region.isValid()) { endOffset = Math.max(endOffset, region.getEndOffset()); } } return endOffset; } public void refreshSettings() { myFoldTextAttributes = myEditor.getColorsScheme().getAttributes(EditorColors.FOLDED_TEXT_ATTRIBUTES); } @Override public boolean isFoldingEnabled() { return myIsFoldingEnabled; } @Override public boolean isOffsetCollapsed(int offset) { assertReadAccess(); return getCollapsedRegionAtOffset(offset) != null; } private boolean isOffsetInsideCollapsedRegion(int offset) { assertReadAccess(); FoldRegion region = getCollapsedRegionAtOffset(offset); return region != null && region.getStartOffset() < offset; } private static void assertIsDispatchThreadForEditor() { ApplicationManagerEx.getApplicationEx().assertIsDispatchThread(); } private static void assertReadAccess() { ApplicationManagerEx.getApplicationEx().assertReadAccessAllowed(); } @Override public void setFoldingEnabled(boolean isEnabled) { assertIsDispatchThreadForEditor(); myIsFoldingEnabled = isEnabled; } @Override public FoldRegion addFoldRegion(int startOffset, int endOffset, @NotNull String placeholderText) { FoldRegion region = createFoldRegion(startOffset, endOffset, placeholderText, null, false); if (region == null) return null; if (!addFoldRegion(region)) { region.dispose(); return null; } return region; } @Override public boolean addFoldRegion(@NotNull final FoldRegion region) { assertIsDispatchThreadForEditor(); if (!isFoldingEnabled()) { return false; } if (!myIsBatchFoldingProcessing) { LOG.error("Fold regions must be added or removed inside batchFoldProcessing() only."); return false; } myFoldRegionsProcessed = true; if (myFoldTree.addRegion(region)) { final FoldingGroup group = region.getGroup(); if (group != null) { myGroups.putValue(group, region); } for (FoldingListener listener : myListeners) { listener.onFoldRegionStateChange(region); } return true; } return false; } @Override public void runBatchFoldingOperation(@NotNull Runnable operation) { runBatchFoldingOperation(operation, false, true); } @Override public void runBatchFoldingOperation(@NotNull Runnable operation, boolean moveCaret) { runBatchFoldingOperation(operation, false, moveCaret); } private void runBatchFoldingOperation( final Runnable operation, final boolean dontCollapseCaret, final boolean moveCaret) { assertIsDispatchThreadForEditor(); boolean oldDontCollapseCaret = myDoNotCollapseCaret; myDoNotCollapseCaret |= dontCollapseCaret; boolean oldBatchFlag = myIsBatchFoldingProcessing; if (!oldBatchFlag) { ((ScrollingModelImpl) myEditor.getScrollingModel()).finishAnimation(); mySavedCaretShift = myEditor.visibleLineToY(myEditor.getCaretModel().getVisualPosition().line) - myEditor.getScrollingModel().getVerticalScrollOffset(); } myIsBatchFoldingProcessing = true; try { operation.run(); } finally { if (!oldBatchFlag) { if (myFoldRegionsProcessed) { notifyBatchFoldingProcessingDone(moveCaret); myFoldRegionsProcessed = false; } myIsBatchFoldingProcessing = false; } myDoNotCollapseCaret = oldDontCollapseCaret; } } @Override public void runBatchFoldingOperationDoNotCollapseCaret(@NotNull final Runnable operation) { runBatchFoldingOperation(operation, true, true); } /** * Disables caret position adjustment after batch folding operation is finished. Should be called * from inside batch operation runnable. */ public void flushCaretShift() { mySavedCaretShift = -1; } @Override @NotNull public FoldRegion[] getAllFoldRegions() { assertReadAccess(); return myFoldTree.fetchAllRegions(); } @Override @Nullable public FoldRegion getCollapsedRegionAtOffset(int offset) { return myFoldTree.fetchOutermost(offset); } @Nullable @Override public FoldRegion getFoldRegion(int startOffset, int endOffset) { assertReadAccess(); return myFoldTree.getRegionAt(startOffset, endOffset); } @Override @Nullable public FoldRegion getFoldingPlaceholderAt(Point p) { assertReadAccess(); LogicalPosition pos = myEditor.xyToLogicalPosition(p); int line = pos.line; if (line >= myEditor.getDocument().getLineCount()) return null; int offset = myEditor.logicalPositionToOffset(pos); return myFoldTree.fetchOutermost(offset); } @Override public void removeFoldRegion(@NotNull final FoldRegion region) { assertIsDispatchThreadForEditor(); if (!myIsBatchFoldingProcessing) { LOG.error("Fold regions must be added or removed inside batchFoldProcessing() only."); } region.setExpanded(true); final FoldingGroup group = region.getGroup(); if (group != null) { myGroups.remove(group, region); } myFoldTree.removeRegion(region); myFoldRegionsProcessed = true; region.dispose(); } public void dispose() { doClearFoldRegions(); } @Override public void clearFoldRegions() { if (!myIsBatchFoldingProcessing) { LOG.error("Fold regions must be added or removed inside batchFoldProcessing() only."); return; } doClearFoldRegions(); } public void doClearFoldRegions() { myGroups.clear(); myFoldTree.clear(); } public void expandFoldRegion(FoldRegion region) { assertIsDispatchThreadForEditor(); if (region.isExpanded() || region.shouldNeverExpand()) return; if (!myIsBatchFoldingProcessing) { LOG.error("Fold regions must be collapsed or expanded inside batchFoldProcessing() only."); } for (Caret caret : myEditor.getCaretModel().getAllCarets()) { LogicalPosition savedPosition = caret.getUserData(SAVED_CARET_POSITION); if (savedPosition != null) { int savedOffset = myEditor.logicalPositionToOffset(savedPosition); FoldRegion[] allCollapsed = myFoldTree.fetchCollapsedAt(savedOffset); if (allCollapsed.length == 1 && allCollapsed[0] == region) { caret.moveToLogicalPosition(savedPosition); } } } myFoldRegionsProcessed = true; ((FoldRegionImpl) region).setExpandedInternal(true); notifyListenersOnFoldRegionStateChange(region); } public void collapseFoldRegion(FoldRegion region) { assertIsDispatchThreadForEditor(); if (!region.isExpanded()) return; if (!myIsBatchFoldingProcessing) { LOG.error("Fold regions must be collapsed or expanded inside batchFoldProcessing() only."); } List<Caret> carets = myEditor.getCaretModel().getAllCarets(); for (Caret caret : carets) { LogicalPosition caretPosition = caret.getLogicalPosition(); int caretOffset = myEditor.logicalPositionToOffset(caretPosition); if (FoldRegionsTree.contains(region, caretOffset)) { if (myDoNotCollapseCaret) return; } } for (Caret caret : carets) { LogicalPosition caretPosition = caret.getLogicalPosition(); int caretOffset = myEditor.logicalPositionToOffset(caretPosition); if (FoldRegionsTree.contains(region, caretOffset)) { if (caret.getUserData(SAVED_CARET_POSITION) == null) { caret.putUserData(SAVED_CARET_POSITION, caretPosition.withoutVisualPositionInfo()); } } } myFoldRegionsProcessed = true; ((FoldRegionImpl) region).setExpandedInternal(false); notifyListenersOnFoldRegionStateChange(region); } private void notifyBatchFoldingProcessingDone(final boolean moveCaretFromCollapsedRegion) { rebuild(); for (FoldingListener listener : myListeners) { listener.onFoldProcessingEnd(); } myEditor.updateCaretCursor(); myEditor.recalculateSizeAndRepaint(); myEditor.getGutterComponentEx().updateSize(); myEditor.getGutterComponentEx().repaint(); for (Caret caret : myEditor.getCaretModel().getAllCarets()) { // There is a possible case that caret position is already visual position aware. But visual // position depends on number of folded // logical lines as well, hence, we can't be sure that target logical position defines correct // visual position because fold // regions have just changed. Hence, we use 'raw' logical position instead. LogicalPosition caretPosition = caret.getLogicalPosition().withoutVisualPositionInfo(); int caretOffset = myEditor.logicalPositionToOffset(caretPosition); int selectionStart = caret.getSelectionStart(); int selectionEnd = caret.getSelectionEnd(); LogicalPosition positionToUse = null; int offsetToUse = -1; FoldRegion collapsed = myFoldTree.fetchOutermost(caretOffset); LogicalPosition savedPosition = caret.getUserData(SAVED_CARET_POSITION); if (savedPosition != null) { int savedOffset = myEditor.logicalPositionToOffset(savedPosition); FoldRegion collapsedAtSaved = myFoldTree.fetchOutermost(savedOffset); if (collapsedAtSaved == null) { positionToUse = savedPosition; } else { offsetToUse = collapsedAtSaved.getStartOffset(); } } if (collapsed != null && positionToUse == null) { positionToUse = myEditor.offsetToLogicalPosition(collapsed.getStartOffset()); } if (moveCaretFromCollapsedRegion && caret.isUpToDate()) { if (offsetToUse >= 0) { caret.moveToOffset(offsetToUse); } else if (positionToUse != null) { caret.moveToLogicalPosition(positionToUse); } else { caret.moveToLogicalPosition(caretPosition); } } caret.putUserData(SAVED_CARET_POSITION, savedPosition); if (isOffsetInsideCollapsedRegion(selectionStart) || isOffsetInsideCollapsedRegion(selectionEnd)) { caret.removeSelection(); } else if (selectionStart < myEditor.getDocument().getTextLength()) { caret.setSelection(selectionStart, selectionEnd); } } if (mySavedCaretShift > 0) { final ScrollingModel scrollingModel = myEditor.getScrollingModel(); scrollingModel.disableAnimation(); scrollingModel.scrollVertically( myEditor.visibleLineToY(myEditor.getCaretModel().getVisualPosition().line) - mySavedCaretShift); scrollingModel.enableAnimation(); } } @Override public void rebuild() { if (!myEditor.getDocument().isInBulkUpdate()) { myFoldTree.rebuild(); } } private void updateCachedOffsets() { myFoldTree.updateCachedOffsets(); } public int getFoldedLinesCountBefore(int offset) { if (!myDocumentChangeProcessed && myEditor.getDocument().isInEventsHandling()) { // There is a possible case that this method is called on document update before fold regions // are recalculated. // We return zero in such situations then. return 0; } return myFoldTree.getFoldedLinesCountBefore(offset); } @Override @Nullable public FoldRegion[] fetchTopLevel() { return myFoldTree.fetchTopLevel(); } @Override @Nullable public FoldRegion fetchOutermost(int offset) { return myFoldTree.fetchOutermost(offset); } public FoldRegion[] fetchCollapsedAt(int offset) { return myFoldTree.fetchCollapsedAt(offset); } @Override public boolean intersectsRegion(int startOffset, int endOffset) { return myFoldTree.intersectsRegion(startOffset, endOffset); } public FoldRegion[] fetchVisible() { return myFoldTree.fetchVisible(); } @Override public int getLastCollapsedRegionBefore(int offset) { return myFoldTree.getLastTopLevelIndexBefore(offset); } @Override public TextAttributes getPlaceholderAttributes() { return myFoldTextAttributes; } public void flushCaretPosition() { for (Caret caret : myEditor.getCaretModel().getAllCarets()) { caret.putUserData(SAVED_CARET_POSITION, null); } } void onBulkDocumentUpdateStarted() { myFoldTree.clearCachedValues(); } void onBulkDocumentUpdateFinished() { myFoldTree.rebuild(); } @Override public void beforeDocumentChange(DocumentEvent event) { myDocumentChangeProcessed = false; } @Override public void documentChanged(DocumentEvent event) { try { if (!((DocumentEx) event.getDocument()).isInBulkUpdate()) { updateCachedOffsets(); } } finally { myDocumentChangeProcessed = true; } } @Override public int getPriority() { return EditorDocumentPriorities.FOLD_MODEL; } @Override public FoldRegion createFoldRegion( int startOffset, int endOffset, @NotNull String placeholder, @Nullable FoldingGroup group, boolean neverExpands) { FoldRegionImpl region = new FoldRegionImpl(myEditor, startOffset, endOffset, placeholder, group, neverExpands); LOG.assertTrue(region.isValid()); return region; } @Override public void addListener( @NotNull final FoldingListener listener, @NotNull Disposable parentDisposable) { myListeners.add(listener); Disposer.register( parentDisposable, new Disposable() { @Override public void dispose() { myListeners.remove(listener); } }); } private void notifyListenersOnFoldRegionStateChange(@NotNull FoldRegion foldRegion) { for (FoldingListener listener : myListeners) { listener.onFoldRegionStateChange(foldRegion); } } @NotNull @Override public String dumpState() { return Arrays.toString(myFoldTree.fetchTopLevel()); } @Override public String toString() { return dumpState(); } }