diff --git a/.idea/artifacts/k2js_jar.xml b/.idea/artifacts/k2js_jar.xml index c80e2b9b132..0d9863a55f6 100644 --- a/.idea/artifacts/k2js_jar.xml +++ b/.idea/artifacts/k2js_jar.xml @@ -13,6 +13,8 @@ + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 3aad68d2896..363ff6ece3a 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -7,6 +7,7 @@ + diff --git a/examples/jetTests.iml b/examples/jetTests.iml index be9f52c5cfd..2c5057c81b3 100644 --- a/examples/jetTests.iml +++ b/examples/jetTests.iml @@ -5,7 +5,7 @@ - + diff --git a/idea_fix/idea_fix.iml b/idea_fix/idea_fix.iml new file mode 100644 index 00000000000..3fd75b7321c --- /dev/null +++ b/idea_fix/idea_fix.iml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/idea_fix/src/com/intellij/core/CoreJavaFileManager.java b/idea_fix/src/com/intellij/core/CoreJavaFileManager.java new file mode 100644 index 00000000000..c415fba3b91 --- /dev/null +++ b/idea_fix/src/com/intellij/core/CoreJavaFileManager.java @@ -0,0 +1,170 @@ +/* + * Copyright 2000-2011 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.core; + +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.impl.jar.CoreJarFileSystem; +import com.intellij.openapi.vfs.local.CoreLocalFileSystem; +import com.intellij.psi.*; +import com.intellij.psi.impl.file.PsiPackageImpl; +import com.intellij.psi.impl.file.impl.JavaFileManager; +import com.intellij.psi.search.GlobalSearchScope; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author yole + */ +public class CoreJavaFileManager implements JavaFileManager { + private final CoreLocalFileSystem myLocalFileSystem; + private final CoreJarFileSystem myJarFileSystem; + private final List myClasspath = new ArrayList(); + private final List myVirtualClasspath = new ArrayList(); + private final PsiManager myPsiManager; + + public CoreJavaFileManager(PsiManager psiManager, CoreLocalFileSystem localFileSystem, CoreJarFileSystem jarFileSystem) { + myPsiManager = psiManager; + myLocalFileSystem = localFileSystem; + myJarFileSystem = jarFileSystem; + addToClasspath("rt.jar"); + } + + @Override + public PsiPackage findPackage(@NotNull String packageName) { + String dirName = packageName.replace(".", "/"); + for (File file : myClasspath) { + VirtualFile classDir = findUnderClasspathEntry(file, dirName); + if (classDir != null) { + return new PsiPackageImpl(myPsiManager, packageName); + } + } + + for (String str : myVirtualClasspath) { + VirtualFile file = myJarFileSystem.findFileByPath(str + "!/" + dirName); + if (file != null) { + return new PsiPackageImpl(myPsiManager, packageName); + } + } + return null; + } + + @Nullable + private VirtualFile findUnderClasspathEntry(File classpathEntry, String relativeName) { + if (classpathEntry.isFile()) { + return myJarFileSystem.findFileByPath(classpathEntry.getPath() + "!/" + relativeName); + } else { + return myLocalFileSystem.findFileByPath(new File(classpathEntry, relativeName).getPath()); + } + } + + @Nullable + private PsiClass findInVirtualJar(String classpathEntry, String relativeName) { + String fileName = relativeName.replace(".", "/") + ".class"; + VirtualFile file = myJarFileSystem.findFileByPath(classpathEntry + "!/" + fileName); + if (file != null) { + PsiFile psiFile = myPsiManager.findFile(file); + if (!(psiFile instanceof PsiJavaFile)) { + /*ErrorWriter.ERROR_WRITER.writeException(ErrorWriter.getExceptionForLog( + "UNKNOWN", new UnsupportedOperationException("no java file for .class"), classpathEntry + "!/" + relativeName + )); + return null;*/ + throw new UnsupportedOperationException("no java file for .class " + classpathEntry + "!/" + relativeName); + } + final PsiClass[] classes = ((PsiJavaFile) psiFile).getClasses(); + if (classes.length == 1) { + return classes[0]; + } + } + return null; + } + + @Override + public PsiClass findClass(@NotNull String qName, @NotNull GlobalSearchScope scope) { + for (File file : myClasspath) { + final PsiClass psiClass = findClassInClasspathEntry(qName, file); + if (psiClass != null) { + return psiClass; + } + } + for (String str : myVirtualClasspath) { + final PsiClass psiClass = findInVirtualJar(str, qName); + if (psiClass != null) { + return psiClass; + } + } + return null; + } + + @Nullable + private PsiClass findClassInClasspathEntry(String qName, File file) { + String fileName = qName.replace(".", "/") + ".class"; + VirtualFile classFile = findUnderClasspathEntry(file, fileName); + + if (classFile != null) { + PsiFile psiFile = myPsiManager.findFile(classFile); + if (!(psiFile instanceof PsiJavaFile)) { + throw new UnsupportedOperationException("no java file for .class"); + } + final PsiClass[] classes = ((PsiJavaFile) psiFile).getClasses(); + if (classes.length == 1) { + return classes[0]; + } + } + return null; + } + + @Override + public PsiClass[] findClasses(@NotNull String qName, @NotNull GlobalSearchScope scope) { + List result = new ArrayList(); + for (File file : myClasspath) { + final PsiClass psiClass = findClassInClasspathEntry(qName, file); + if (psiClass != null) { + result.add(psiClass); + } + } + for (String str : myVirtualClasspath) { + final PsiClass psiClass = findInVirtualJar(str, qName); + if (psiClass != null) { + result.add(psiClass); + } + } + return result.toArray(new PsiClass[result.size()]); + } + + @Override + public Collection getNonTrivialPackagePrefixes() { + return Collections.emptyList(); + } + + @Override + public void initialize() { + } + + public void addToClasspath(File path) { + myClasspath.add(path); + } + + public void addToClasspath(String path) { + myVirtualClasspath.add(path); + } + +} diff --git a/idea_fix/src/com/intellij/lang/impl/PsiBuilderImpl.java b/idea_fix/src/com/intellij/lang/impl/PsiBuilderImpl.java new file mode 100644 index 00000000000..e4f84e71c51 --- /dev/null +++ b/idea_fix/src/com/intellij/lang/impl/PsiBuilderImpl.java @@ -0,0 +1,1629 @@ +/* + * Copyright 2000-2011 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.intellij.lang.impl; + +import com.intellij.lang.*; +import com.intellij.lexer.Lexer; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressIndicatorProvider; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.UserDataHolderBase; +import com.intellij.psi.PsiErrorElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.TokenType; +import com.intellij.psi.impl.source.CharTableImpl; +import com.intellij.psi.impl.source.resolve.FileContextUtil; +import com.intellij.psi.impl.source.text.BlockSupportImpl; +import com.intellij.psi.impl.source.text.DiffLog; +import com.intellij.psi.impl.source.tree.*; +import com.intellij.psi.text.BlockSupport; +import com.intellij.psi.tree.*; +import com.intellij.util.CharTable; +import com.intellij.util.ThreeState; +import com.intellij.util.TripleFunction; +import com.intellij.util.containers.CollectionFactory; +import com.intellij.util.containers.Convertor; +import com.intellij.util.containers.LimitedPool; +import com.intellij.util.containers.Stack; +import com.intellij.util.diff.DiffTreeChangeBuilder; +import com.intellij.util.diff.FlyweightCapableTreeStructure; +import com.intellij.util.diff.ShallowNodeComparator; +import com.intellij.util.text.CharArrayUtil; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @author max + */ +public class PsiBuilderImpl extends UserDataHolderBase implements PsiBuilder, ASTNodeBuilder { + private static final Logger LOG = Logger.getInstance("#com.intellij.lang.impl.PsiBuilderImpl"); + + // function stored in PsiBuilderImpl' user data which called during reparse when merge algorithm is not sure what to merge + public static final Key, ThreeState>> + CUSTOM_COMPARATOR = Key.create("CUSTOM_COMPARATOR"); + + private final Project myProject; + private PsiFile myFile; + + private int[] myLexStarts; + private IElementType[] myLexTypes; + private int myCurrentLexeme; + + private final MyList myProduction = new MyList(); + + private final Lexer myLexer; + private final TokenSet myWhitespaces; + private TokenSet myComments; + + private CharTable myCharTable; + private final CharSequence myText; + private final char[] myTextArray; + private boolean myDebugMode = false; + private int myLexemeCount = 0; + private boolean myTokenTypeChecked; + private ITokenTypeRemapper myRemapper; + private WhitespaceSkippedCallback myWhitespaceSkippedCallback; + + private final ASTNode myOriginalTree; + private final MyTreeStructure myParentLightTree; + + private static TokenSet ourAnyLanguageWhitespaceTokens = TokenSet.EMPTY; + + private Map myUserData = null; + + private final LimitedPool START_MARKERS = new LimitedPool(2000, new LimitedPool.ObjectFactory() { + @Override + public StartMarker create() { + return new StartMarker(); + } + + @Override + public void cleanup(final StartMarker startMarker) { + startMarker.clean(); + } + }); + + private final LimitedPool DONE_MARKERS = new LimitedPool(2000, new LimitedPool.ObjectFactory() { + @Override + public DoneMarker create() { + return new DoneMarker(); + } + + @Override + public void cleanup(final DoneMarker doneMarker) { + doneMarker.clean(); + } + }); + + private static final WhitespacesAndCommentsBinder DEFAULT_LEFT_EDGE_TOKEN_BINDER = new WhitespacesAndCommentsBinder() { + @Override + public int getEdgePosition(final List tokens, final boolean atStreamEdge, final TokenTextGetter getter) { + return tokens.size(); + } + }; + + private static final WhitespacesAndCommentsBinder DEFAULT_RIGHT_EDGE_TOKEN_BINDER = new WhitespacesAndCommentsBinder() { + @Override + public int getEdgePosition(final List tokens, final boolean atStreamEdge, final TokenTextGetter getter) { + return 0; + } + }; + + public static void registerWhitespaceToken(IElementType type) { + ourAnyLanguageWhitespaceTokens = TokenSet.orSet(ourAnyLanguageWhitespaceTokens, TokenSet.create(type)); + } + + public PsiBuilderImpl(@NotNull Project project, + PsiFile containingFile, + @NotNull ParserDefinition parserDefinition, + @NotNull Lexer lexer, + CharTable charTable, + @NotNull final CharSequence text, + @Nullable ASTNode originalTree, + @Nullable MyTreeStructure parentLightTree) { + this(project, containingFile, parserDefinition.getWhitespaceTokens(), parserDefinition.getCommentTokens(), lexer, charTable, text, + originalTree, parentLightTree); + } + + public PsiBuilderImpl(Project project, + PsiFile containingFile, + @NotNull TokenSet whiteSpaces, + @NotNull TokenSet comments, + @NotNull Lexer lexer, + CharTable charTable, + @NotNull final CharSequence text, + @Nullable ASTNode originalTree, + @Nullable MyTreeStructure parentLightTree) { + myProject = project; + myFile = containingFile; + + myText = text; + myTextArray = CharArrayUtil.fromSequenceWithoutCopying(text); + myLexer = lexer; + + myWhitespaces = whiteSpaces; + myComments = comments; + myCharTable = charTable; + myOriginalTree = originalTree; + myParentLightTree = parentLightTree; + + cacheLexemes(); + } + + public PsiBuilderImpl(@NotNull final Project project, + @NotNull final ParserDefinition parserDefinition, + @NotNull final Lexer lexer, + @NotNull final ASTNode chameleon, + @NotNull final CharSequence text) { + this(project, SharedImplUtil.getContainingFile(chameleon), parserDefinition, lexer, SharedImplUtil.findCharTableByTree(chameleon), text, + chameleon.getUserData(BlockSupport.TREE_TO_BE_REPARSED), null); + } + + public PsiBuilderImpl(@NotNull final Project project, + @NotNull final ParserDefinition parserDefinition, + @NotNull final Lexer lexer, + @NotNull final LighterLazyParseableNode chameleon, + @NotNull final CharSequence text) { + this(project, chameleon.getContainingFile(), parserDefinition, lexer, chameleon.getCharTable(), text, + null, ((LazyParseableToken) chameleon).myParent); + } + + private void cacheLexemes() { + int approxLexCount = Math.max(10, myText.length() / 5); + + myLexStarts = new int[approxLexCount]; + myLexTypes = new IElementType[approxLexCount]; + + myLexer.start(myText); + int i = 0; + int offset = 0; + while (true) { + ProgressIndicatorProvider.checkCanceled(); + IElementType type = myLexer.getTokenType(); + if (type == null) break; + + if (i >= myLexTypes.length - 1) { + resizeLexemes(i * 3 / 2); + } + int tokenStart = myLexer.getTokenStart(); + if (tokenStart < offset) { + final StringBuilder sb = new StringBuilder(); + final IElementType tokenType = myLexer.getTokenType(); + sb.append("Token sequence broken") + .append("\n this: '").append(myLexer.getTokenText()).append("' (").append(tokenType).append(':') + .append(tokenType != null ? tokenType.getLanguage() : null).append(") ").append(tokenStart).append(":").append(myLexer.getTokenEnd()); + if (i > 0) { + final int prevStart = myLexStarts[i - 1]; + sb.append("\n prev: '").append(myText.subSequence(prevStart, offset)).append("' (").append(myLexTypes[i - 1]).append(':') + .append(myLexTypes[i - 1].getLanguage()).append(") ").append(prevStart).append(":").append(offset); + } + final int quoteStart = Math.max(tokenStart - 256, 0); + final int quoteEnd = Math.min(tokenStart + 256, myText.length()); + sb.append("\n quote: [").append(quoteStart).append(':').append(quoteEnd) + .append("] '").append(myText.subSequence(quoteStart, quoteEnd)).append('\''); + LOG.error(sb); + } + myLexStarts[i] = offset = tokenStart; + myLexTypes[i] = type; + i++; + myLexer.advance(); + } + + myLexStarts[i] = myText.length(); + + myLexemeCount = i; + } + + @Override + public Project getProject() { + return myProject; + } + + @Override + public void enforceCommentTokens(TokenSet tokens) { + myComments = tokens; + } + + @Override + @Nullable + public LighterASTNode getLatestDoneMarker() { + int index = myProduction.size() - 1; + while (index >= 0) { + ProductionMarker marker = myProduction.get(index); + if (marker instanceof DoneMarker) return ((DoneMarker) marker).myStart; + --index; + } + return null; + } + + private abstract static class Node implements LighterASTNode { + public abstract int hc(); + } + + @Override + public IElementType getElementType(int lexemIndex) { + return lexemIndex < myLexemeCount && lexemIndex >= 0 ? myLexTypes[lexemIndex] : null; + } + + public abstract static class ProductionMarker extends Node { + protected int myLexemeIndex; + protected WhitespacesAndCommentsBinder myEdgeTokenBinder; + protected ProductionMarker myParent; + protected ProductionMarker myNext; + + public void clean() { + myLexemeIndex = 0; + myParent = myNext = null; + } + } + + private static class StartMarker extends ProductionMarker implements Marker { + private PsiBuilderImpl myBuilder; + private IElementType myType; + private DoneMarker myDoneMarker; + private Throwable myDebugAllocationPosition; + private ProductionMarker myFirstChild; + private ProductionMarker myLastChild; + private int myHC = -1; + + private StartMarker() { + myEdgeTokenBinder = DEFAULT_LEFT_EDGE_TOKEN_BINDER; + } + + @Override + public void clean() { + super.clean(); + myBuilder = null; + myType = null; + myDoneMarker = null; + myDebugAllocationPosition = null; + myFirstChild = myLastChild = null; + myHC = -1; + myEdgeTokenBinder = DEFAULT_LEFT_EDGE_TOKEN_BINDER; + } + + @Override + public int hc() { + if (myHC == -1) { + PsiBuilderImpl builder = myBuilder; + int hc = 0; + final CharSequence buf = builder.myText; + final char[] bufArray = builder.myTextArray; + ProductionMarker child = myFirstChild; + int lexIdx = myLexemeIndex; + + while (child != null) { + int lastLeaf = child.myLexemeIndex; + for (int i = builder.myLexStarts[lexIdx]; i < builder.myLexStarts[lastLeaf]; i++) { + hc += bufArray != null ? bufArray[i] : buf.charAt(i); + } + lexIdx = lastLeaf; + hc += child.hc(); + if (child instanceof StartMarker) { + lexIdx = ((StartMarker) child).myDoneMarker.myLexemeIndex; + } + child = child.myNext; + } + + for (int i = builder.myLexStarts[lexIdx]; i < builder.myLexStarts[myDoneMarker.myLexemeIndex]; i++) { + hc += bufArray != null ? bufArray[i] : buf.charAt(i); + } + + myHC = hc; + } + + return myHC; + } + + @Override + public int getStartOffset() { + return myBuilder.myLexStarts[myLexemeIndex]; + } + + @Override + public int getEndOffset() { + return myBuilder.myLexStarts[myDoneMarker.myLexemeIndex]; + } + + public void addChild(ProductionMarker node) { + if (myFirstChild == null) { + myFirstChild = node; + myLastChild = node; + } else { + myLastChild.myNext = node; + myLastChild = node; + } + } + + @Override + public Marker precede() { + return myBuilder.precede(this); + } + + @Override + public void drop() { + myBuilder.drop(this); + } + + @Override + public void rollbackTo() { + myBuilder.rollbackTo(this); + } + + @Override + public void done(IElementType type) { + myType = type; + myBuilder.done(this); + } + + @Override + public void collapse(IElementType type) { + myType = type; + myBuilder.collapse(this); + } + + @Override + public void doneBefore(IElementType type, Marker before) { + myType = type; + myBuilder.doneBefore(this, before); + } + + @Override + public void doneBefore(final IElementType type, final Marker before, final String errorMessage) { + final StartMarker marker = (StartMarker) before; + myBuilder.myProduction.add(myBuilder.myProduction.lastIndexOf(marker), + new ErrorItem(myBuilder, errorMessage, marker.myLexemeIndex)); + doneBefore(type, before); + } + + @Override + public void error(String message) { + myType = TokenType.ERROR_ELEMENT; + myBuilder.error(this, message); + } + + @Override + public void errorBefore(final String message, final Marker before) { + myType = TokenType.ERROR_ELEMENT; + myBuilder.errorBefore(this, message, before); + } + + @Override + public IElementType getTokenType() { + return myType; + } + + @Override + public void setCustomEdgeTokenBinders(final WhitespacesAndCommentsBinder left, final WhitespacesAndCommentsBinder right) { + if (left != null) { + myEdgeTokenBinder = left; + } + + if (right != null) { + if (myDoneMarker == null) + throw new IllegalArgumentException("Cannot set right-edge processor for unclosed marker"); + myDoneMarker.myEdgeTokenBinder = right; + } + } + } + + private Marker precede(final StartMarker marker) { + int idx = myProduction.lastIndexOf(marker); + if (idx < 0) { + LOG.error("Cannot precede dropped or rolled-back marker"); + } + StartMarker pre = createMarker(marker.myLexemeIndex); + myProduction.add(idx, pre); + return pre; + } + + private abstract static class Token extends Node { + protected PsiBuilderImpl myBuilder; + private IElementType myTokenType; + private int myTokenStart; + private int myTokenEnd; + private int myHC = -1; + + public void clean() { + myBuilder = null; + myHC = -1; + } + + @Override + public int hc() { + if (myHC == -1) { + int hc = 0; + if (myTokenType instanceof TokenWrapper) { + final String value = ((TokenWrapper) myTokenType).getValue(); + for (int i = 0; i < value.length(); i++) { + hc += value.charAt(i); + } + } else { + final int start = myTokenStart; + final int end = myTokenEnd; + final CharSequence buf = myBuilder.myText; + final char[] bufArray = myBuilder.myTextArray; + + for (int i = start; i < end; i++) { + hc += bufArray != null ? bufArray[i] : buf.charAt(i); + } + } + + myHC = hc; + } + + return myHC; + } + + @Override + public int getEndOffset() { + return myTokenEnd; + } + + @Override + public int getStartOffset() { + return myTokenStart; + } + + public CharSequence getText() { + if (myTokenType instanceof TokenWrapper) { + return ((TokenWrapper) myTokenType).getValue(); + } + + return myBuilder.myText.subSequence(myTokenStart, myTokenEnd); + } + + @Override + public IElementType getTokenType() { + return myTokenType; + } + } + + private static class TokenNode extends Token implements LighterASTTokenNode { + } + + private static class LazyParseableToken extends Token implements LighterLazyParseableNode, ASTUnparsedNodeMarker { + private MyTreeStructure myParent; + private FlyweightCapableTreeStructure myParsed; + private int myStartIndex; + private int myEndIndex; + + @Override + public void clean() { + super.clean(); + myParent = null; + myParsed = null; + } + + @Override + public PsiFile getContainingFile() { + return myBuilder.myFile; + } + + @Override + public CharTable getCharTable() { + return myBuilder.myCharTable; + } + + public FlyweightCapableTreeStructure parseContents() { + if (myParsed == null) { + myParsed = ((ILightLazyParseableElementType) getTokenType()).parseContents(this); + } + return myParsed; + } + + public ASTNodeBuilder getBuilder() { + return myBuilder; + } + + @Override + public int getStartLexemIndex() { + return myStartIndex; + } + + @Override + public int getEndLexemIndex() { + return myEndIndex; + } + + } + + private static class DoneMarker extends ProductionMarker { + private StartMarker myStart; + private boolean myCollapse = false; + + public DoneMarker() { + myEdgeTokenBinder = DEFAULT_RIGHT_EDGE_TOKEN_BINDER; + } + + public DoneMarker(final StartMarker marker, final int currentLexeme) { + this(); + myLexemeIndex = currentLexeme; + myStart = marker; + } + + @Override + public void clean() { + super.clean(); + myStart = null; + myEdgeTokenBinder = DEFAULT_RIGHT_EDGE_TOKEN_BINDER; + } + + @Override + public int hc() { + throw new UnsupportedOperationException("Shall not be called on this kind of markers"); + } + + @Override + public IElementType getTokenType() { + throw new UnsupportedOperationException("Shall not be called on this kind of markers"); + } + + @Override + public int getEndOffset() { + throw new UnsupportedOperationException("Shall not be called on this kind of markers"); + } + + @Override + public int getStartOffset() { + throw new UnsupportedOperationException("Shall not be called on this kind of markers"); + } + } + + private static class DoneWithErrorMarker extends DoneMarker { + private String myMessage; + + public DoneWithErrorMarker(final StartMarker marker, final int currentLexeme, final String message) { + super(marker, currentLexeme); + myMessage = message; + } + + @Override + public void clean() { + super.clean(); + myMessage = null; + } + } + + private static class ErrorItem extends ProductionMarker { + private final PsiBuilderImpl myBuilder; + private String myMessage; + + public ErrorItem(final PsiBuilderImpl builder, final String message, final int idx) { + myBuilder = builder; + myMessage = message; + myLexemeIndex = idx; + myEdgeTokenBinder = DEFAULT_RIGHT_EDGE_TOKEN_BINDER; + } + + @Override + public void clean() { + super.clean(); + myMessage = null; + } + + @Override + public int hc() { + return 0; + } + + @Override + public int getEndOffset() { + return myBuilder.myLexStarts[myLexemeIndex]; + } + + @Override + public int getStartOffset() { + return myBuilder.myLexStarts[myLexemeIndex]; + } + + @Override + public IElementType getTokenType() { + return TokenType.ERROR_ELEMENT; + } + } + + @Override + public CharSequence getOriginalText() { + return myText; + } + + @Override + public IElementType getTokenType() { + if (eof()) return null; + + if (myRemapper != null) { + IElementType type = myLexTypes[myCurrentLexeme]; + type = myRemapper.filter(type, myLexStarts[myCurrentLexeme], myLexStarts[myCurrentLexeme + 1], myLexer.getBufferSequence()); + myLexTypes[myCurrentLexeme] = type; // filter may have changed the type + return type; + } + return myLexTypes[myCurrentLexeme]; + } + + @Override + public void setTokenTypeRemapper(final ITokenTypeRemapper remapper) { + myRemapper = remapper; + } + + @Override + public void remapCurrentToken(IElementType type) { + myLexTypes[myCurrentLexeme] = type; + } + + @Nullable + @Override + public IElementType lookAhead(int steps) { + if (eof()) { // ensure we skip over whitespace if it's needed + return null; + } + int cur = myCurrentLexeme; + + while (steps > 0) { + ++cur; + while (cur < myLexemeCount && whitespaceOrComment(myLexTypes[cur])) { + cur++; + } + + steps--; + } + + return cur < myLexemeCount ? myLexTypes[cur] : null; + } + + @Override + public IElementType rawLookup(int steps) { + int cur = myCurrentLexeme + steps; + return cur < myLexemeCount && cur >= 0 ? myLexTypes[cur] : null; + } + + @Override + public int rawTokenTypeStart(int steps) { + int cur = myCurrentLexeme + steps; + if (cur < 0) return -1; + if (cur >= myLexemeCount) return getOriginalText().length(); + return myLexStarts[cur]; + } + + @Override + public void setWhitespaceSkippedCallback(WhitespaceSkippedCallback callback) { + myWhitespaceSkippedCallback = callback; + } + + @Override + public void advanceLexer() { + if (eof()) return; + + if (!myTokenTypeChecked) { + LOG.assertTrue(eof(), "Probably a bug: eating token without its type checking"); + } + myTokenTypeChecked = false; + myCurrentLexeme++; + ProgressIndicatorProvider.checkCanceled(); + } + + private void skipWhitespace() { + while (myCurrentLexeme < myLexemeCount && whitespaceOrComment(myLexTypes[myCurrentLexeme])) { + onSkip(myLexTypes[myCurrentLexeme], myLexStarts[myCurrentLexeme], myCurrentLexeme + 1 < myLexemeCount ? myLexStarts[myCurrentLexeme + 1] : myText.length()); + myCurrentLexeme++; + } + } + + private void onSkip(IElementType type, int start, int end) { + if (myWhitespaceSkippedCallback != null) { + myWhitespaceSkippedCallback.onSkip(type, start, end); + } + } + + @Override + public int getCurrentOffset() { + if (eof()) return getOriginalText().length(); + return myLexStarts[myCurrentLexeme]; + } + + @Override + @Nullable + public String getTokenText() { + if (eof()) return null; + final IElementType type = getTokenType(); + if (type instanceof TokenWrapper) { + return ((TokenWrapper) type).getValue(); + } + return myText.subSequence(myLexStarts[myCurrentLexeme], myLexStarts[myCurrentLexeme + 1]).toString(); + } + + private void resizeLexemes(final int newSize) { + int count = Math.min(newSize, myLexTypes.length); + int[] newStarts = new int[newSize + 1]; + System.arraycopy(myLexStarts, 0, newStarts, 0, count); + myLexStarts = newStarts; + + IElementType[] newTypes = new IElementType[newSize]; + System.arraycopy(myLexTypes, 0, newTypes, 0, count); + myLexTypes = newTypes; + } + + private boolean whitespaceOrComment(IElementType token) { + return myWhitespaces.contains(token) || myComments.contains(token); + } + + @Override + public Marker mark() { + if (!myProduction.isEmpty()) { + skipWhitespace(); + } + StartMarker marker = createMarker(myCurrentLexeme); + + myProduction.add(marker); + return marker; + } + + private StartMarker createMarker(final int lexemeIndex) { + StartMarker marker = START_MARKERS.alloc(); + marker.myLexemeIndex = lexemeIndex; + marker.myBuilder = this; + + if (myDebugMode) { + marker.myDebugAllocationPosition = new Throwable("Created at the following trace."); + } + return marker; + } + + @Override + public final boolean eof() { + if (!markTokenTypeChecked()) { + skipWhitespace(); + } + return myCurrentLexeme >= myLexemeCount; + } + + public boolean markTokenTypeChecked() { + if (!myTokenTypeChecked) { + myTokenTypeChecked = true; + return false; + } + return true; + } + + @SuppressWarnings({"SuspiciousMethodCalls"}) + private void rollbackTo(Marker marker) { + myCurrentLexeme = ((StartMarker) marker).myLexemeIndex; + markTokenTypeChecked(); + int idx = myProduction.lastIndexOf(marker); + if (idx < 0) { + LOG.error("The marker must be added before rolled back to."); + } + myProduction.removeRange(idx, myProduction.size()); + START_MARKERS.recycle((StartMarker) marker); + } + + @SuppressWarnings({"SuspiciousMethodCalls"}) + public void drop(Marker marker) { + final DoneMarker doneMarker = ((StartMarker) marker).myDoneMarker; + if (doneMarker != null) { + myProduction.remove(myProduction.lastIndexOf(doneMarker)); + DONE_MARKERS.recycle(doneMarker); + } + final boolean removed = myProduction.remove(myProduction.lastIndexOf(marker)) == marker; + if (!removed) { + LOG.error("The marker must be added before it is dropped."); + } + START_MARKERS.recycle((StartMarker) marker); + } + + public void error(Marker marker, String message) { + doValidityChecks(marker, null); + + DoneWithErrorMarker doneMarker = new DoneWithErrorMarker((StartMarker) marker, myCurrentLexeme, message); + boolean tieToTheLeft = isEmpty(((StartMarker) marker).myLexemeIndex, myCurrentLexeme); + if (tieToTheLeft) ((StartMarker) marker).myEdgeTokenBinder = DEFAULT_RIGHT_EDGE_TOKEN_BINDER; + + ((StartMarker) marker).myDoneMarker = doneMarker; + myProduction.add(doneMarker); + } + + @SuppressWarnings({"SuspiciousMethodCalls"}) + public void errorBefore(Marker marker, String message, Marker before) { + doValidityChecks(marker, before); + + int beforeIndex = myProduction.lastIndexOf(before); + + DoneWithErrorMarker doneMarker = new DoneWithErrorMarker((StartMarker) marker, ((StartMarker) before).myLexemeIndex, message); + boolean tieToTheLeft = isEmpty(((StartMarker) marker).myLexemeIndex, ((StartMarker) before).myLexemeIndex); + if (tieToTheLeft) ((StartMarker) marker).myEdgeTokenBinder = DEFAULT_RIGHT_EDGE_TOKEN_BINDER; + + ((StartMarker) marker).myDoneMarker = doneMarker; + myProduction.add(beforeIndex, doneMarker); + } + + public void done(final Marker marker) { + doValidityChecks(marker, null); + + DoneMarker doneMarker = DONE_MARKERS.alloc(); + doneMarker.myStart = (StartMarker) marker; + doneMarker.myLexemeIndex = myCurrentLexeme; + boolean tieToTheLeft = doneMarker.myStart.myType.isLeftBound() && + isEmpty(((StartMarker) marker).myLexemeIndex, myCurrentLexeme); + if (tieToTheLeft) ((StartMarker) marker).myEdgeTokenBinder = DEFAULT_RIGHT_EDGE_TOKEN_BINDER; + + ((StartMarker) marker).myDoneMarker = doneMarker; + myProduction.add(doneMarker); + } + + @SuppressWarnings({"SuspiciousMethodCalls"}) + public void doneBefore(Marker marker, Marker before) { + doValidityChecks(marker, before); + + int beforeIndex = myProduction.lastIndexOf(before); + + DoneMarker doneMarker = DONE_MARKERS.alloc(); + doneMarker.myLexemeIndex = ((StartMarker) before).myLexemeIndex; + doneMarker.myStart = (StartMarker) marker; + boolean tieToTheLeft = doneMarker.myStart.myType.isLeftBound() && + isEmpty(((StartMarker) marker).myLexemeIndex, ((StartMarker) before).myLexemeIndex); + if (tieToTheLeft) ((StartMarker) marker).myEdgeTokenBinder = DEFAULT_RIGHT_EDGE_TOKEN_BINDER; + + ((StartMarker) marker).myDoneMarker = doneMarker; + myProduction.add(beforeIndex, doneMarker); + } + + private boolean isEmpty(final int startIdx, final int endIdx) { + for (int i = startIdx; i < endIdx; i++) { + final IElementType token = myLexTypes[i]; + if (!whitespaceOrComment(token)) return false; + } + return true; + } + + public void collapse(final Marker marker) { + done(marker); + ((StartMarker) marker).myDoneMarker.myCollapse = true; + } + + @SuppressWarnings({"UseOfSystemOutOrSystemErr", "SuspiciousMethodCalls"}) + private void doValidityChecks(final Marker marker, @Nullable final Marker before) { + final DoneMarker doneMarker = ((StartMarker) marker).myDoneMarker; + if (doneMarker != null) { + LOG.error("Marker already done."); + } + + if (!myDebugMode) return; + + int idx = myProduction.lastIndexOf(marker); + if (idx < 0) { + LOG.error("Marker has never been added."); + } + + int endIdx = myProduction.size(); + if (before != null) { + endIdx = myProduction.lastIndexOf(before); + if (endIdx < 0) { + LOG.error("'Before' marker has never been added."); + } + if (idx > endIdx) { + LOG.error("'Before' marker precedes this one."); + } + } + + for (int i = endIdx - 1; i > idx; i--) { + Object item = myProduction.get(i); + if (item instanceof StartMarker) { + StartMarker otherMarker = (StartMarker) item; + if (otherMarker.myDoneMarker == null) { + final Throwable debugAllocOther = otherMarker.myDebugAllocationPosition; + final Throwable debugAllocThis = ((StartMarker) marker).myDebugAllocationPosition; + if (debugAllocOther != null) { + debugAllocThis.printStackTrace(System.err); + debugAllocOther.printStackTrace(System.err); + } + LOG.error("Another not done marker added after this one. Must be done before this."); + } + } + } + } + + @Override + public void error(String messageText) { + final ProductionMarker lastMarker = myProduction.get(myProduction.size() - 1); + if (lastMarker instanceof ErrorItem && lastMarker.myLexemeIndex == myCurrentLexeme) { + return; + } + myProduction.add(new ErrorItem(this, messageText, myCurrentLexeme)); + } + + @Override + public ASTNode getTreeBuilt() { + try { + return buildTree(); + } finally { + for (int i = 0, myProductionSize = myProduction.size(); i < myProductionSize; i++) { + ProductionMarker marker = myProduction.get(i); + if (marker instanceof StartMarker) { + START_MARKERS.recycle((StartMarker) marker); + } else if (marker instanceof DoneMarker) { + DONE_MARKERS.recycle((DoneMarker) marker); + } + } + } + } + + private ASTNode buildTree() { + final StartMarker rootMarker = prepareLightTree(); + final boolean isTooDeep = myFile != null && BlockSupport.isTooDeep(myFile.getOriginalFile()); + + if (myOriginalTree != null && !isTooDeep) { + DiffLog diffLog = merge(myOriginalTree, rootMarker); + throw new BlockSupport.ReparsedSuccessfullyException(diffLog); + } + + final ASTNode rootNode = createRootAST(rootMarker); + bind(rootMarker, (CompositeElement) rootNode); + + if (isTooDeep && !(rootNode instanceof FileElement)) { + final ASTNode childNode = rootNode.getFirstChildNode(); + childNode.putUserData(BlockSupport.TREE_DEPTH_LIMIT_EXCEEDED, Boolean.TRUE); + } + + return rootNode; + } + + @Override + public FlyweightCapableTreeStructure getLightTree() { + final StartMarker rootMarker = prepareLightTree(); + return new MyTreeStructure(rootMarker, myParentLightTree); + } + + private ASTNode createRootAST(final StartMarker rootMarker) { + final IElementType type = rootMarker.getTokenType(); + final ASTNode rootNode = type instanceof ILazyParseableElementType ? + ASTFactory.lazy((ILazyParseableElementType) type, null) : createComposite(rootMarker); + if (myCharTable == null) { + myCharTable = rootNode instanceof FileElement ? ((FileElement) rootNode).getCharTable() : new CharTableImpl(); + } + if (!(rootNode instanceof FileElement)) { + rootNode.putUserData(CharTable.CHAR_TABLE_KEY, myCharTable); + } + return rootNode; + } + + private static class ConvertFromTokensToASTBuilder implements DiffTreeChangeBuilder { + private final DiffTreeChangeBuilder myDelegate; + private final ASTConverter myConverter; + + public ConvertFromTokensToASTBuilder(StartMarker rootNode, DiffTreeChangeBuilder delegate) { + myDelegate = delegate; + myConverter = new ASTConverter(rootNode); + } + + @Override + public void nodeDeleted(@NotNull final ASTNode oldParent, @NotNull final ASTNode oldNode) { + myDelegate.nodeDeleted(oldParent, oldNode); + } + + @Override + public void nodeInserted(@NotNull final ASTNode oldParent, @NotNull final LighterASTNode newNode, final int pos) { + myDelegate.nodeInserted(oldParent, myConverter.convert((Node) newNode), pos); + } + + @Override + public void nodeReplaced(@NotNull final ASTNode oldChild, @NotNull final LighterASTNode newChild) { + ASTNode converted = myConverter.convert((Node) newChild); + myDelegate.nodeReplaced(oldChild, converted); + } + } + + @NonNls + private static final String UNBALANCED_MESSAGE = + "Unbalanced tree. Most probably caused by unbalanced markers. " + + "Try calling setDebugMode(true) against PsiBuilder passed to identify exact location of the problem"; + + @NotNull + private DiffLog merge(@NotNull final ASTNode oldRoot, @NotNull StartMarker newRoot) { + DiffLog diffLog = new DiffLog(); + final ConvertFromTokensToASTBuilder builder = new ConvertFromTokensToASTBuilder(newRoot, diffLog); + final MyTreeStructure treeStructure = new MyTreeStructure(newRoot, null); + final MyComparator comparator = new MyComparator(getUserDataUnprotected(CUSTOM_COMPARATOR), treeStructure); + + final ProgressIndicatorProvider provider = ProgressIndicatorProvider.getInstance(); + final ProgressIndicator indicator = provider != null ? provider.getProgressIndicator() : null; + BlockSupportImpl.diffTrees(oldRoot, builder, comparator, treeStructure, indicator); + return diffLog; + } + + @NotNull + private StartMarker prepareLightTree() { + markTokenTypeChecked(); + balanceWhiteSpaces(); + + LOG.assertTrue(!myProduction.isEmpty(), "Parser produced no markers. Text:\n" + myText); + + final StartMarker rootMarker = (StartMarker) myProduction.get(0); + rootMarker.myParent = rootMarker.myFirstChild = rootMarker.myLastChild = rootMarker.myNext = null; + StartMarker curNode = rootMarker; + final Stack nodes = new Stack(); + nodes.push(rootMarker); + + @SuppressWarnings({"MultipleVariablesInDeclaration"}) int lastErrorIndex = -1, maxDepth = 0, curDepth = 0; + for (int i = 1; i < myProduction.size(); i++) { + final ProductionMarker item = myProduction.get(i); + + if (curNode == null) LOG.error("Unexpected end of the production"); + + item.myParent = curNode; + if (item instanceof StartMarker) { + final StartMarker marker = (StartMarker) item; + marker.myFirstChild = marker.myLastChild = marker.myNext = null; + curNode.addChild(marker); + nodes.push(curNode); + curNode = marker; + curDepth++; + if (curDepth > maxDepth) maxDepth = curDepth; + } else if (item instanceof DoneMarker) { + if (((DoneMarker) item).myStart != curNode) LOG.error(UNBALANCED_MESSAGE); + curNode = nodes.pop(); + curDepth--; + } else if (item instanceof ErrorItem) { + int curToken = item.myLexemeIndex; + if (curToken == lastErrorIndex) continue; + lastErrorIndex = curToken; + curNode.addChild(item); + } + } + + if (myCurrentLexeme < myLexemeCount) { + final List missed = CollectionFactory.arrayList(myLexTypes, myCurrentLexeme, myLexemeCount); + LOG.error("Tokens " + missed + " were not inserted into the tree. Text:\n" + myText); + } + + if (rootMarker.myDoneMarker.myLexemeIndex < myLexemeCount) { + final List missed = CollectionFactory.arrayList(myLexTypes, rootMarker.myDoneMarker.myLexemeIndex, myLexemeCount); + LOG.error("Tokens " + missed + " are outside of root element \"" + rootMarker.myType + "\". Text:\n" + myText); + } + + if (myLexStarts.length <= myCurrentLexeme + 1) { + resizeLexemes(myCurrentLexeme + 1); + } + + myLexStarts[myCurrentLexeme] = myText.length(); // $ terminating token.; + myLexStarts[myCurrentLexeme + 1] = 0; + myLexTypes[myCurrentLexeme] = null; + + LOG.assertTrue(curNode == rootMarker, UNBALANCED_MESSAGE); + + checkTreeDepth(maxDepth, rootMarker.getTokenType() instanceof IFileElementType); + + return rootMarker; + } + + private void balanceWhiteSpaces() { + for (int i = 1; i < myProduction.size() - 1; i++) { + final ProductionMarker item = myProduction.get(i); + + if (item instanceof StartMarker && ((StartMarker) item).myDoneMarker == null) { + LOG.error(UNBALANCED_MESSAGE); + } + + final int prevProductionLexIndex = myProduction.get(i - 1).myLexemeIndex; + int idx = item.myLexemeIndex; + while (idx > prevProductionLexIndex && whitespaceOrComment(myLexTypes[idx - 1])) idx--; + final int wsStartIndex = idx; + + int wsEndIndex = item.myLexemeIndex; + while (wsEndIndex < myLexemeCount && whitespaceOrComment(myLexTypes[wsEndIndex])) wsEndIndex++; + + final List wsTokens = CollectionFactory.arrayList(myLexTypes, wsStartIndex, wsEndIndex); + final boolean atEnd = wsStartIndex == 0 || wsEndIndex == myLexemeCount; + final WhitespacesAndCommentsBinder.TokenTextGetter getter = new WhitespacesAndCommentsBinder.TokenTextGetter() { + @Override + public CharSequence get(final int i) { + return myText.subSequence(myLexStarts[wsStartIndex + i], myLexStarts[wsStartIndex + i + 1]); + } + }; + item.myLexemeIndex = wsStartIndex + item.myEdgeTokenBinder.getEdgePosition(wsTokens, atEnd, getter); + } + } + + private void checkTreeDepth(final int maxDepth, final boolean isFileRoot) { + if (myFile == null) return; + final PsiFile file = myFile.getOriginalFile(); + final Boolean flag = file.getUserData(BlockSupport.TREE_DEPTH_LIMIT_EXCEEDED); + if (maxDepth > BlockSupport.INCREMENTAL_REPARSE_DEPTH_LIMIT) { + if (!Boolean.TRUE.equals(flag)) { + file.putUserData(BlockSupport.TREE_DEPTH_LIMIT_EXCEEDED, Boolean.TRUE); + } + } else if (isFileRoot && flag != null) { + file.putUserData(BlockSupport.TREE_DEPTH_LIMIT_EXCEEDED, null); + } + } + + private void bind(final StartMarker rootMarker, final CompositeElement rootNode) { + StartMarker curMarker = rootMarker; + CompositeElement curNode = rootNode; + + int lexIndex = rootMarker.myLexemeIndex; + ProductionMarker item = rootMarker.myFirstChild != null ? rootMarker.myFirstChild : rootMarker.myDoneMarker; + while (true) { + lexIndex = insertLeaves(lexIndex, item.myLexemeIndex, curNode); + + if (item == rootMarker.myDoneMarker) break; + + if (item instanceof StartMarker) { + final StartMarker marker = (StartMarker) item; + if (!marker.myDoneMarker.myCollapse) { + curMarker = marker; + + final CompositeElement childNode = createComposite(marker); + curNode.rawAddChildrenWithoutNotifications(childNode); + curNode = childNode; + + item = marker.myFirstChild != null ? marker.myFirstChild : marker.myDoneMarker; + continue; + } else { + lexIndex = collapseLeaves(curNode, marker); + } + } else if (item instanceof ErrorItem) { + final CompositeElement errorElement = Factory.createErrorElement(((ErrorItem) item).myMessage); + curNode.rawAddChildrenWithoutNotifications(errorElement); + } else if (item instanceof DoneMarker) { + curMarker = (StartMarker) ((DoneMarker) item).myStart.myParent; + curNode = curNode.getTreeParent(); + item = ((DoneMarker) item).myStart; + } + + item = item.myNext != null ? item.myNext : curMarker.myDoneMarker; + } + } + + private int insertLeaves(int curToken, int lastIdx, final CompositeElement curNode) { + lastIdx = Math.min(lastIdx, myLexemeCount); + while (curToken < lastIdx) { + ProgressIndicatorProvider.checkCanceled(); + final int start = myLexStarts[curToken]; + final int end = myLexStarts[curToken + 1]; + if (start < end || myLexTypes[curToken] instanceof ILeafElementType) { // Empty token. Most probably a parser directive like indent/dedent in Python + final IElementType type = myLexTypes[curToken]; + final TreeElement leaf = createLeaf(type, start, end); + curNode.rawAddChildrenWithoutNotifications(leaf); + } + curToken++; + } + + return curToken; + } + + private int collapseLeaves(final CompositeElement ast, final StartMarker startMarker) { + final int start = myLexStarts[startMarker.myLexemeIndex]; + final int end = myLexStarts[startMarker.myDoneMarker.myLexemeIndex]; + final TreeElement leaf = createLeaf(startMarker.myType, start, end); + ast.rawAddChildrenWithoutNotifications(leaf); + return startMarker.myDoneMarker.myLexemeIndex; + } + + private static CompositeElement createComposite(final StartMarker marker) { + final IElementType type = marker.myType; + if (type == TokenType.ERROR_ELEMENT) { + String message = marker.myDoneMarker instanceof DoneWithErrorMarker ? ((DoneWithErrorMarker) marker.myDoneMarker).myMessage : null; + return Factory.createErrorElement(message); + } + + if (type == null) { + throw new RuntimeException(UNBALANCED_MESSAGE); + } + + return ASTFactory.composite(type); + } + + @Nullable + public static String getErrorMessage(final LighterASTNode node) { + if (node instanceof ErrorItem) return ((ErrorItem) node).myMessage; + if (node instanceof StartMarker) { + final StartMarker marker = (StartMarker) node; + if (marker.myType == TokenType.ERROR_ELEMENT && marker.myDoneMarker instanceof DoneWithErrorMarker) { + return ((DoneWithErrorMarker) marker.myDoneMarker).myMessage; + } + } + + return null; + } + + private static class MyComparator implements ShallowNodeComparator { + private final TripleFunction, ThreeState> custom; + private final MyTreeStructure myTreeStructure; + + private MyComparator(TripleFunction, ThreeState> custom, + MyTreeStructure treeStructure) { + this.custom = custom; + myTreeStructure = treeStructure; + } + + @Override + public ThreeState deepEqual(final ASTNode oldNode, final LighterASTNode newNode) { + ProgressIndicatorProvider.checkCanceled(); + + boolean oldIsErrorElement = oldNode instanceof PsiErrorElement; + boolean newIsErrorElement = newNode.getTokenType() == TokenType.ERROR_ELEMENT; + if (oldIsErrorElement != newIsErrorElement) return ThreeState.NO; + if (oldIsErrorElement && newIsErrorElement) { + final PsiErrorElement e1 = (PsiErrorElement) oldNode; + return Comparing.equal(e1.getErrorDescription(), getErrorMessage(newNode)) ? ThreeState.UNSURE : ThreeState.NO; + } + + if (newNode instanceof Token) { + final IElementType type = newNode.getTokenType(); + final Token token = (Token) newNode; + + if (oldNode instanceof ForeignLeafPsiElement) { + return type instanceof ForeignLeafType && ((ForeignLeafType) type).getValue().equals(oldNode.getText()) + ? ThreeState.YES + : ThreeState.NO; + } + + if (oldNode instanceof LeafElement) { + if (type instanceof ForeignLeafType) return ThreeState.NO; + + return ((LeafElement) oldNode).textMatches(token.getText()) + ? ThreeState.YES + : ThreeState.NO; + } + + if (type instanceof ILightLazyParseableElementType) { + return ((TreeElement) oldNode).textMatches(token.getText()) + ? ThreeState.YES + : TreeUtil.isCollapsedChameleon(oldNode) + ? ThreeState.NO // do not dive into collapsed nodes + : ThreeState.UNSURE; + } + + if (oldNode.getElementType() instanceof ILazyParseableElementType && type instanceof ILazyParseableElementType || + oldNode.getElementType() instanceof CustomParsingType && type instanceof CustomParsingType) { + return ((TreeElement) oldNode).textMatches(token.getText()) + ? ThreeState.YES + : ThreeState.NO; + } + } + if (custom != null) { + return custom.fun(oldNode, newNode, myTreeStructure); + } + + return ThreeState.UNSURE; + } + + @Override + public boolean typesEqual(final ASTNode n1, final LighterASTNode n2) { + if (n1 instanceof PsiWhiteSpaceImpl) { + return ourAnyLanguageWhitespaceTokens.contains(n2.getTokenType()) || + n2 instanceof Token && ((Token) n2).myBuilder.myWhitespaces.contains(n2.getTokenType()); + } + + return derefToken(n1.getElementType()) == derefToken(n2.getTokenType()); + } + + public static IElementType derefToken(IElementType probablyWrapper) { + if (probablyWrapper instanceof TokenWrapper) { + return derefToken(((TokenWrapper) probablyWrapper).getDelegate()); + } + return probablyWrapper; + } + + @Override + public boolean hashCodesEqual(final ASTNode n1, final LighterASTNode n2) { + if (n1 instanceof LeafElement && n2 instanceof Token) { + boolean isForeign1 = n1 instanceof ForeignLeafPsiElement; + boolean isForeign2 = n2.getTokenType() instanceof ForeignLeafType; + if (isForeign1 != isForeign2) return false; + + if (isForeign1 && isForeign2) { + return n1.getText().equals(((ForeignLeafType) n2.getTokenType()).getValue()); + } + + return ((LeafElement) n1).textMatches(((Token) n2).getText()); + } + + if (n1 instanceof PsiErrorElement && n2.getTokenType() == TokenType.ERROR_ELEMENT) { + final PsiErrorElement e1 = (PsiErrorElement) n1; + if (!Comparing.equal(e1.getErrorDescription(), getErrorMessage(n2))) return false; + } + + return ((TreeElement) n1).hc() == ((Node) n2).hc(); + } + } + + private static class MyTreeStructure implements FlyweightCapableTreeStructure { + private final LimitedPool myPool; + private final LimitedPool myLazyPool; + private final StartMarker myRoot; + + public MyTreeStructure(@NotNull StartMarker root, @Nullable final MyTreeStructure parentTree) { + if (parentTree == null) { + myPool = new LimitedPool(1000, new LimitedPool.ObjectFactory() { + @Override + public void cleanup(final Token token) { + token.clean(); + } + + @Override + public Token create() { + return new TokenNode(); + } + }); + myLazyPool = new LimitedPool(200, new LimitedPool.ObjectFactory() { + @Override + public void cleanup(final LazyParseableToken token) { + token.clean(); + } + + @Override + public LazyParseableToken create() { + return new LazyParseableToken(); + } + }); + } else { + myPool = parentTree.myPool; + myLazyPool = parentTree.myLazyPool; + } + myRoot = root; + } + + @Override + @NotNull + public LighterASTNode getRoot() { + return myRoot; + } + + @Override + public LighterASTNode getParent(@NotNull final LighterASTNode node) { + if (node instanceof StartMarker) { + return ((StartMarker) node).myParent; + } + throw new UnsupportedOperationException("Unknown node type: " + node); + } + + @Override + @NotNull + public LighterASTNode prepareForGetChildren(@NotNull final LighterASTNode node) { + return node; + } + + private int count; + + @Override + public int getChildren(@NotNull final LighterASTNode item, @NotNull final Ref into) { + if (item instanceof LazyParseableToken) { + final FlyweightCapableTreeStructure tree = ((LazyParseableToken) item).parseContents(); + final LighterASTNode root = tree.getRoot(); + return tree.getChildren(tree.prepareForGetChildren(root), into); // todo: set offset shift for kids? + } + + if (item instanceof Token || item instanceof ErrorItem) return 0; + StartMarker marker = (StartMarker) item; + + count = 0; + ProductionMarker child = marker.myFirstChild; + int lexIndex = marker.myLexemeIndex; + while (child != null) { + lexIndex = insertLeaves(lexIndex, child.myLexemeIndex, into, marker.myBuilder); + + if (child instanceof StartMarker && ((StartMarker) child).myDoneMarker.myCollapse) { + int lastIndex = ((StartMarker) child).myDoneMarker.myLexemeIndex; + insertLeaf(into, child.getTokenType(), marker.myBuilder, child.myLexemeIndex, lastIndex); + } else { + ensureCapacity(into); + into.get()[count++] = child; + } + + if (child instanceof StartMarker) { + lexIndex = ((StartMarker) child).myDoneMarker.myLexemeIndex; + } + child = child.myNext; + } + + insertLeaves(lexIndex, marker.myDoneMarker.myLexemeIndex, into, marker.myBuilder); + + return count; + } + + @Override + public void disposeChildren(final LighterASTNode[] nodes, final int count) { + if (nodes == null) return; + for (int i = 0; i < count; i++) { + final LighterASTNode node = nodes[i]; + if (node instanceof LazyParseableToken) { + myLazyPool.recycle((LazyParseableToken) node); + } else if (node instanceof Token) { + myPool.recycle((Token) node); + } + } + } + + private void ensureCapacity(final Ref into) { + LighterASTNode[] old = into.get(); + if (old == null) { + old = new LighterASTNode[10]; + into.set(old); + } else if (count >= old.length) { + LighterASTNode[] newStore = new LighterASTNode[count * 3 / 2]; + System.arraycopy(old, 0, newStore, 0, count); + into.set(newStore); + } + } + + private int insertLeaves(int curToken, int lastIdx, Ref into, PsiBuilderImpl builder) { + lastIdx = Math.min(lastIdx, builder.myLexemeCount); + while (curToken < lastIdx) { + insertLeaf(into, builder.myLexTypes[curToken], builder, curToken, curToken + 1); + + curToken++; + } + return curToken; + } + + private void insertLeaf(Ref into, IElementType type, PsiBuilderImpl builder, int startLexemIndex, int endLexemIndex) { + final int start = builder.myLexStarts[startLexemIndex]; + final int end = builder.myLexStarts[endLexemIndex]; + if (start > end || ((start == end) && !(type instanceof ILeafElementType))) return; + + final Token lexeme; + if (type instanceof ILightLazyParseableElementType) { + lexeme = myLazyPool.alloc(); + LazyParseableToken lazyParseableToken = (LazyParseableToken) lexeme; + lazyParseableToken.myParent = this; + lazyParseableToken.myStartIndex = startLexemIndex; + lazyParseableToken.myEndIndex = endLexemIndex; + } else { + lexeme = myPool.alloc(); + } + lexeme.myBuilder = builder; + lexeme.myTokenType = type; + lexeme.myTokenStart = start; + lexeme.myTokenEnd = end; + ensureCapacity(into); + into.get()[count++] = lexeme; + } + } + + private static class ASTConverter implements Convertor { + private final StartMarker myRoot; + + public ASTConverter(final StartMarker root) { + myRoot = root; + } + + @Override + public ASTNode convert(final Node n) { + if (n instanceof Token) { + final Token token = (Token) n; + return token.myBuilder.createLeaf(token.getTokenType(), token.myTokenStart, token.myTokenEnd); + } else if (n instanceof ErrorItem) { + return Factory.createErrorElement(((ErrorItem) n).myMessage); + } else { + final StartMarker startMarker = (StartMarker) n; + final CompositeElement composite = n == myRoot ? (CompositeElement) myRoot.myBuilder.createRootAST(myRoot) + : createComposite(startMarker); + startMarker.myBuilder.bind(startMarker, composite); + return composite; + } + } + } + + @Override + public void setDebugMode(boolean dbgMode) { + myDebugMode = dbgMode; + } + + public Lexer getLexer() { + return myLexer; + } + + @NotNull + private TreeElement createLeaf(final IElementType type, final int start, final int end) { + CharSequence text = myCharTable.intern(myText, start, end); + if (myWhitespaces.contains(type)) { + return new PsiWhiteSpaceImpl(text); + } + + if (type instanceof CustomParsingType) { + return (TreeElement) ((CustomParsingType) type).parse(text, myCharTable); + } + + if (type instanceof ILazyParseableElementType) { + return ASTFactory.lazy((ILazyParseableElementType) type, text); + } + + return ASTFactory.leaf(type, text); + } + + /** + * just to make removeRange method available. + */ + private static class MyList extends ArrayList { + private static final Field ourElementDataField; + + static { + Field f = null; + /*try { + f = ArrayList.class.getDeclaredField("elementData"); + f.setAccessible(true); + } + catch(NoSuchFieldException e) { + // IBM J9 does not have the field + f = null; + + }*/ + ourElementDataField = f; + } + + private Object[] cachedElementData; + + @Override + public void removeRange(final int fromIndex, final int toIndex) { + super.removeRange(fromIndex, toIndex); + } + + MyList() { + super(256); + } + + @Override + public int lastIndexOf(final Object o) { + if (cachedElementData == null) return super.lastIndexOf(o); + for (int i = size() - 1; i >= 0; i--) { + if (cachedElementData[i] == o) return i; + } + return -1; + } + + @Override + public void ensureCapacity(final int minCapacity) { + if (cachedElementData == null || minCapacity >= cachedElementData.length) { + super.ensureCapacity(minCapacity); + initCachedField(); + } + } + + private void initCachedField() { + if (ourElementDataField == null) return; + try { + cachedElementData = (Object[]) ourElementDataField.get(this); + } catch (Exception e) { + LOG.error(e); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public T getUserDataUnprotected(@NotNull final Key key) { + if (key == FileContextUtil.CONTAINING_FILE_KEY) return (T) myFile; + return myUserData != null ? (T) myUserData.get(key) : null; + } + + @Override + public void putUserDataUnprotected(@NotNull final Key key, @Nullable final T value) { + if (myUserData == null) myUserData = CollectionFactory.hashMap(); + myUserData.put(key, value); + } +} diff --git a/idea_fix/src/com/intellij/openapi/editor/impl/CharArray.java b/idea_fix/src/com/intellij/openapi/editor/impl/CharArray.java new file mode 100644 index 00000000000..533162bb0b0 --- /dev/null +++ b/idea_fix/src/com/intellij/openapi/editor/impl/CharArray.java @@ -0,0 +1,668 @@ +/* + * Copyright 2000-2011 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.editor.impl; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.editor.impl.event.DocumentEventImpl; +import com.intellij.util.LocalTimeCounter; +import com.intellij.util.text.CharArrayCharSequence; +import com.intellij.util.text.CharArrayUtil; +import com.intellij.util.text.CharSequenceBackedByArray; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author cdr + */ +abstract class CharArray implements CharSequenceBackedByArray { + + @SuppressWarnings("UseOfArchaicSystemPropertyAccessors") + private static final boolean DISABLE_DEFERRED_PROCESSING = false; + @SuppressWarnings("UseOfArchaicSystemPropertyAccessors") + private static final boolean DEBUG_DEFERRED_PROCESSING = false; + + private static final Logger LOG = Logger.getInstance("#" + CharArray.class.getName()); + + /** + * We can't exclude possibility of situation when 'defer changes' state is {@link #setDeferredChangeMode(boolean) entered} + * but not exited, hence, we want to perform automatic flushing if necessary in order to avoid memory leaks. This constant holds + * a value that defines that 'automatic flushing' criteria, i.e. every time number of stored deferred changes exceeds this value, + * they are automatically flushed. + */ + private static final int MAX_DEFERRED_CHANGES_NUMBER = 10000; + + private final AtomicReference myDeferredChangesStorage = new AtomicReference(); + + private int myStart; + /** + * This class implements {@link #subSequence(int, int)} by creating object of the same class that partially shares the same + * data as the object on which the method is called. So, this field may define interested end offset (if it's non-negative). + */ + private int myEnd = -1; + private int myCount = 0; + + private CharSequence myOriginalSequence; + private char[] myArray; + private SoftReference myStringRef; // buffers String value - for not to generate it every time + private int myBufferSize; + private int myDeferredShift; + private boolean myDeferredChangeMode; + + // We had a problems with bulk document text processing, hence, debug facilities were introduced. The fields group below work with them. + // The main idea is to hold all history of bulk processing iteration in order to be able to retrieve it from client and reproduce the + // problem. + + /** + * Flag the identifies if current char array should debug bulk processing. + */ + private final boolean myDebugDeferredProcessing; + + /** + * Duplicate instance of the current char array that is used during debug processing as follows - apply every text change + * from the bulk changes group to this instance immediately in order to be able to check if the current 'deferred change-aware' + * instance functionally behaves at the same way as 'straightforward' one. + */ + private CharArray myDebugArray; + + /** + * Holds deferred changes create during the current bulk processing iteration. + */ + private List myDebugDeferredChanges; + + /** + * Document text on bulk processing start. + */ + private String myDebugTextOnBatchUpdateStart; + + // max chars to hold, bufferSize == 0 means unbounded + CharArray(int bufferSize) { + this(bufferSize, new TextChangesStorage(), null, -1, -1); + } + + private CharArray(final int bufferSize, @NotNull TextChangesStorage deferredChangesStorage, @Nullable char[] data, int start, int end) { + this(bufferSize, deferredChangesStorage, data, start, end, DEBUG_DEFERRED_PROCESSING); + } + + private CharArray(final int bufferSize, @NotNull TextChangesStorage deferredChangesStorage, @Nullable char[] data, int start, int end, + boolean debugDeferredProcessing) { + myBufferSize = bufferSize; + myDeferredChangesStorage.set(deferredChangesStorage); + if (data == null) { + myOriginalSequence = ""; + } else { + myArray = data; + myCount = end - start; + } + if (start >= 0 && end >= 0) { + myStart = start; + myEnd = end; + } + + myDebugDeferredProcessing = debugDeferredProcessing; + if (myDebugDeferredProcessing) { + + myDebugArray = new CharArray(bufferSize, new TextChangesStorage(), data == null ? null : Arrays.copyOf(data, data.length), + start, end, false) { + @NotNull + @Override + protected DocumentEvent beforeChangedUpdate(DocumentImpl subj, + int offset, + CharSequence oldString, + CharSequence newString, + boolean wholeTextReplaced) { + return new DocumentEventImpl(subj, offset, oldString, newString, -1, wholeTextReplaced); + } + + @Override + protected void afterChangedUpdate(@NotNull DocumentEvent event, long newModificationStamp) { + } + }; + myDebugDeferredChanges = new ArrayList(); + } + } + + public void setBufferSize(int bufferSize) { + myBufferSize = bufferSize; + } + + @NotNull + protected abstract DocumentEvent beforeChangedUpdate(DocumentImpl subj, + int offset, + @Nullable CharSequence oldString, + @Nullable CharSequence newString, + boolean wholeTextReplaced); + + protected abstract void afterChangedUpdate(@NotNull DocumentEvent event, long newModificationStamp); + + public void setText(@Nullable final DocumentImpl subj, final CharSequence chars) { + myOriginalSequence = chars; + myArray = null; + myCount = chars.length(); + myStringRef = null; + TextChangesStorage storage = myDeferredChangesStorage.get(); + storage.getLock().lock(); + try { + if (isSubSequence()) { + myDeferredChangesStorage.set(new TextChangesStorage()); + myStart = 0; + myEnd = -1; + } else { + storage.clear(); + } + } finally { + storage.getLock().unlock(); + } + + if (subj != null) { + trimToSize(subj); + } + + if (myDebugDeferredProcessing) { + myDebugArray.setText(subj, chars); + myDebugDeferredChanges.clear(); + } + } + + public void replace(DocumentImpl subj, + int startOffset, int endOffset, CharSequence toDelete, CharSequence newString, long newModificationStamp, + boolean wholeTextReplaced) { + final DocumentEvent event = beforeChangedUpdate(subj, startOffset, toDelete, newString, wholeTextReplaced); + startOffset += myStart; + endOffset += myStart; + doReplace(startOffset, endOffset, newString); + afterChangedUpdate(event, newModificationStamp); + } + + private void doReplace(int startOffset, int endOffset, CharSequence newString) { + prepareForModification(); + + if (isDeferredChangeMode()) { + storeChange(new TextChangeImpl(newString, startOffset, endOffset)); + if (myDebugDeferredProcessing) { + myDebugArray.doReplace(startOffset, endOffset, newString); + } + return; + } + + int newLength = newString.length(); + int oldLength = endOffset - startOffset; + + CharArrayUtil.getChars(newString, myArray, startOffset, Math.min(newLength, oldLength)); + + if (newLength > oldLength) { + doInsert(newString.subSequence(oldLength, newLength), endOffset); + } else if (newLength < oldLength) { + doRemove(startOffset + newLength, startOffset + oldLength); + } + } + + public void remove(DocumentImpl subj, int startIndex, int endIndex, CharSequence toDelete) { + DocumentEvent event = beforeChangedUpdate(subj, startIndex, toDelete, null, false); + startIndex += myStart; + endIndex += myStart; + doRemove(startIndex, endIndex); + afterChangedUpdate(event, LocalTimeCounter.currentTime()); + } + + private void doRemove(final int startIndex, final int endIndex) { + if (startIndex == endIndex) { + return; + } + prepareForModification(); + + if (isDeferredChangeMode()) { + storeChange(new TextChangeImpl("", startIndex, endIndex)); + if (myDebugDeferredProcessing) { + myDebugArray.doRemove(startIndex, endIndex); + } + return; + } + + if (endIndex < myCount) { + System.arraycopy(myArray, endIndex, myArray, startIndex, myCount - endIndex); + } + myCount -= endIndex - startIndex; + } + + public void insert(DocumentImpl subj, CharSequence s, int startIndex) { + DocumentEvent event = beforeChangedUpdate(subj, startIndex, null, s, false); + startIndex += myStart; + doInsert(s, startIndex); + + afterChangedUpdate(event, LocalTimeCounter.currentTime()); + trimToSize(subj); + } + + private void doInsert(final CharSequence s, final int startIndex) { + prepareForModification(); + + if (isDeferredChangeMode()) { + storeChange(new TextChangeImpl(s, startIndex)); + if (myDebugDeferredProcessing) { + myDebugArray.doInsert(s, startIndex); + } + return; + } + + int insertLength = s.length(); + myArray = relocateArray(myArray, myCount + insertLength); + if (startIndex < myCount) { + System.arraycopy(myArray, startIndex, myArray, startIndex + insertLength, myCount - startIndex); + } + + CharArrayUtil.getChars(s, myArray, startIndex); + myCount += insertLength; + } + + /** + * Stores given change at collection of deferred changes (merging it with others if necessary) and updates current object + * state ({@link #length() length} etc). + * + * @param change new change to store + */ + private void storeChange(@NotNull TextChangeImpl change) { + if (!change.isWithinBounds(length())) { + LOG.error(String.format( + "Invalid change attempt detected - given change bounds are not within the current char array. Change: %d:%d-%d", + change.getText().length(), change.getStart(), change.getEnd() + ), dumpState()); + return; + } + TextChangesStorage storage = myDeferredChangesStorage.get(); + storage.getLock().lock(); + try { + doStoreChange(change); + } finally { + storage.getLock().unlock(); + } + } + + private void doStoreChange(@NotNull TextChangeImpl change) { + TextChangesStorage storage = myDeferredChangesStorage.get(); + if (storage.size() >= MAX_DEFERRED_CHANGES_NUMBER) { + flushDeferredChanged(storage); + } + storage.store(change); + myDeferredShift += change.getDiff(); + + if (myDebugDeferredProcessing) { + myDebugDeferredChanges.add(change); + } + } + + private void prepareForModification() { + if (myOriginalSequence != null) { + myArray = new char[myOriginalSequence.length()]; + CharArrayUtil.getChars(myOriginalSequence, myArray, 0); + myOriginalSequence = null; + } + myStringRef = null; + } + + public CharSequence getCharArray() { + if (myOriginalSequence != null) return myOriginalSequence; + return this; + } + + public String toString() { + String str = myStringRef != null ? myStringRef.get() : null; + if (str == null) { + if (myOriginalSequence != null) { + str = myOriginalSequence.toString(); + } else if (!hasDeferredChanges()) { + str = new String(myArray, myStart, myCount); + } else { + str = substring(0, length()).toString(); + } + myStringRef = new SoftReference(str); + } + if (myDebugDeferredProcessing && isDeferredChangeMode()) { + String expected = myDebugArray.toString(); + checkStrings("toString()", expected, str); + } + return str; + } + + @Override + public final int length() { + final int result = myCount + myDeferredShift; + if (myDebugDeferredProcessing && isDeferredChangeMode()) { + int expected = myDebugArray.length(); + if (expected != result) { + dumpDebugInfo(String.format("Incorrect length() processing. Expected: '%s', actual: '%s'", expected, result)); + } + } + return result; + } + + @Override + public final char charAt(int i) { + if (i < 0 || i >= length()) { + throw new IndexOutOfBoundsException("Wrong offset: " + i + "; count:" + length()); + } + i += myStart; + if (myOriginalSequence != null) return myOriginalSequence.charAt(i); + final char result; + if (hasDeferredChanges()) { + TextChangesStorage storage = myDeferredChangesStorage.get(); + storage.getLock().lock(); + try { + result = storage.charAt(myArray, i); + } finally { + storage.getLock().unlock(); + } + } else { + result = myArray[i]; + } + + if (myDebugDeferredProcessing && isDeferredChangeMode()) { + char expected = myDebugArray.charAt(i); + if (expected != result) { + dumpDebugInfo( + String.format("Incorrect charAt() processing for index %d. Expected: '%c', actual: '%c'", i, expected, result) + ); + } + } + return result; + } + + @Override + public CharSequence subSequence(final int start, final int end) { + if (start == 0 && end == length()) return this; + if (myOriginalSequence != null) { + return myOriginalSequence.subSequence(start, end); + } + if (hasDeferredChanges()) { + return new CharArray(myBufferSize, myDeferredChangesStorage.get(), myArray, myStart + start, myStart + end) { + @NotNull + @Override + protected DocumentEvent beforeChangedUpdate(DocumentImpl subj, + int offset, + CharSequence oldString, + CharSequence newString, + boolean wholeTextReplaced) { + return new DocumentEventImpl(subj, offset, oldString, newString, LocalTimeCounter.currentTime(), wholeTextReplaced); + } + + @Override + protected void afterChangedUpdate(@NotNull DocumentEvent event, long newModificationStamp) { + } + + @Override + public char[] getChars() { + char[] chars = CharArray.this.getChars(); + char[] result = new char[end - start]; + System.arraycopy(chars, start, result, 0, result.length); + return result; + } + }; + } else { + // We don't use the same approach as with 'defer changes' mode because the former is the new experimental one and this one + // is rather mature, hence, we just minimizes the risks that something is wrong within the new approach. + return new CharArrayCharSequence(myArray, start, end); + } + } + + private boolean isSubSequence() { + return myEnd >= 0; + } + + @Override + public char[] getChars() { + if (myOriginalSequence != null) { + if (myArray == null) { + myArray = CharArrayUtil.fromSequence(myOriginalSequence); + } + } + flushDeferredChanged(myDeferredChangesStorage.get()); + if (myDebugDeferredProcessing && isDeferredChangeMode()) { + char[] expected = myDebugArray.getChars(); + for (int i = 0, max = length(); i < max; i++) { + if (myArray[i] != expected[i]) { + dumpDebugInfo(String.format("getChars(). Index: %d, expected: %c, actual: %c", i, expected[i], myArray[i])); + break; + } + } + } + return myArray; + } + + @Override + public void getChars(final char[] dst, final int dstOffset) { + flushDeferredChanged(myDeferredChangesStorage.get()); + if (myOriginalSequence != null) { + CharArrayUtil.getChars(myOriginalSequence, dst, dstOffset); + } else { + System.arraycopy(myArray, myStart, dst, dstOffset, length()); + } + + if (myDebugDeferredProcessing && isDeferredChangeMode()) { + char[] expected = new char[dst.length]; + myDebugArray.getChars(expected, dstOffset); + for (int i = dstOffset, j = myStart; i < dst.length && j < myArray.length; i++, j++) { + if (expected[i] != myArray[j]) { + dumpDebugInfo(String.format("getChars(char[], int). Given array of length %d, offset %d. Found char '%c' at index %d, " + + "expected to find '%c'", dst.length, dstOffset, myArray[j], i, expected[i])); + break; + } + } + } + } + + public CharSequence substring(final int start, final int end) { + if (start == end) return ""; + final CharSequence result; + if (myOriginalSequence == null) { + TextChangesStorage storage = myDeferredChangesStorage.get(); + storage.getLock().lock(); + try { + result = storage.substring(myArray, start + myStart, end + myStart); + } finally { + storage.getLock().unlock(); + } + } else { + result = myOriginalSequence.subSequence(start, end); + } + + if (myDebugDeferredProcessing && isDeferredChangeMode()) { + String expected = myDebugArray.substring(start, end).toString(); + checkStrings(String.format("substring(%d, %d)", start, end), expected, result.toString()); + } + return result; + } + + private static char[] relocateArray(char[] array, int index) { + if (index < array.length) { + return array; + } + + int newArraySize = array.length; + if (newArraySize == 0) { + newArraySize = 16; + } + while (newArraySize <= index) { + newArraySize = newArraySize * 12 / 10 + 1; + } + char[] newArray = new char[newArraySize]; + System.arraycopy(array, 0, newArray, 0, array.length); + return newArray; + } + + private void trimToSize(DocumentImpl subj) { + if (myBufferSize != 0 && length() > myBufferSize) { + flushDeferredChanged(myDeferredChangesStorage.get()); + // make a copy + remove(subj, 0, myCount - myBufferSize, getCharArray().subSequence(0, myCount - myBufferSize).toString()); + } + } + + /** + * @return true if this object is at {@link #setDeferredChangeMode(boolean) defer changes} mode; + * false otherwise + */ + public boolean isDeferredChangeMode() { + return !DISABLE_DEFERRED_PROCESSING && myDeferredChangeMode; + } + + public boolean hasDeferredChanges() { + return !myDeferredChangesStorage.get().isEmpty(); + } + + /** + * There is a possible case that client of this class wants to perform great number of modifications in a short amount of time + * (e.g. end-user performs formatting of the document backed by the object of the current class). It may result in significant + * performance degradation is the changes are performed one by one (every time the change is applied tail content is shifted to + * the left or right). So, we may want to optimize that by avoiding actual array modification until information about + * all target changes is provided and perform array data moves only after that. + *

+ * This method allows to define that 'defer changes' mode usages, i.e. expected usage pattern is as follows: + *

+     * 
    + *
  1. + * Client of this class enters 'defer changes' mode (calls this method with 'true' argument). + * That means that all subsequent changes will not actually modify backed array data and will be stored separately; + *
  2. + *
  3. + * Number of target changes are applied to the current object via standard API + * ({@link #insert(com.intellij.openapi.editor.impl.DocumentImpl, CharSequence, int) insert}, + * {@link #remove(com.intellij.openapi.editor.impl.DocumentImpl, int, int, CharSequence) remove} and + * {@link #replace(com.intellij.openapi.editor.impl.DocumentImpl, int, int, CharSequence, CharSequence, long, boolean) replace}); + *
  4. + *
  5. + * Client of this class indicates that 'massive change time' is over by calling this method with 'false' + * argument. That flushes all deferred changes (if any) to the backed data array and makes every subsequent change to + * be immediate flushed to the backed array; + *
  6. + *
+ *
+ *

+ * Note: we can't exclude possibility that 'defer changes' mode is started but inadvertently not ended + * (due to programming error, unexpected exception etc). Hence, this class is free to automatically end + * 'defer changes' mode when necessary in order to avoid memory leak with infinite deferred changes storing. + * + * @param deferredChangeMode flag that defines if 'defer changes' mode should be used by the current object + */ + public void setDeferredChangeMode(boolean deferredChangeMode) { + if (deferredChangeMode && myDebugDeferredProcessing) { + myDebugArray.setText(null, myDebugTextOnBatchUpdateStart = toString()); + myDebugDeferredChanges.clear(); + } + myDeferredChangeMode = deferredChangeMode; + if (!deferredChangeMode) { + flushDeferredChanged(myDeferredChangesStorage.get()); + } + } + + private void flushDeferredChanged(@NotNull TextChangesStorage storage) { + storage.getLock().lock(); + try { + doFlushDeferredChanged(); + } finally { + storage.getLock().unlock(); + } + } + + private void doFlushDeferredChanged() { + TextChangesStorage storage = myDeferredChangesStorage.get(); + List changes = storage.getChanges(); + if (changes.isEmpty()) { + return; + } + + char[] beforeMerge = null; + final boolean inPlace; + if (myDebugDeferredProcessing) { + beforeMerge = new char[myArray.length]; + System.arraycopy(myArray, 0, beforeMerge, 0, myArray.length); + } + + BulkChangesMerger changesMerger = BulkChangesMerger.INSTANCE; + if (myArray.length < length()) { + myArray = changesMerger.mergeToCharArray(myArray, myCount, changes); + inPlace = false; + } else { + changesMerger.mergeInPlace(myArray, myCount, changes); + inPlace = true; + } + + if (myDebugDeferredProcessing) { + for (int i = 0, max = length(); i < max; i++) { + if (myArray[i] != myDebugArray.myArray[i]) { + dumpDebugInfo(String.format( + "flushDeferredChanged(). Index %d, expected: '%c', actual '%c'. Text before merge: '%s', merge inplace: %b", + i, myDebugArray.myArray[i], myArray[i], Arrays.toString(beforeMerge), inPlace)); + break; + } + } + } + + myCount += myDeferredShift; + myDeferredShift = 0; + storage.clear(); + myDeferredChangeMode = false; + } + + @NotNull + public String dumpState() { + return String.format( + "deferred changes mode: %b, length: %d (data array length: %d, deferred shift: %d); view offsets: [%d; %d]; deferred changes: %s", + isDeferredChangeMode(), length(), myCount, myDeferredShift, myStart, myEnd, myDeferredChangesStorage + ); + } + + private void checkStrings(@NotNull String operation, @NotNull String expected, @NotNull String actual) { + if (expected.equals(actual)) { + return; + } + for (int i = 0, max = Math.min(expected.length(), actual.length()); i < max; i++) { + if (actual.charAt(i) != expected.charAt(i)) { + dumpDebugInfo(String.format( + "Incorrect %s processing. Expected length: %d, actual length: %d. Unmatched symbol at %d - expected: '%c', " + + "actual: '%c', expected document: '%s', actual document: '%s'", + operation, expected.length(), actual.length(), i, expected.charAt(i), actual.charAt(i), expected, actual + )); + return; + } + } + dumpDebugInfo(String.format( + "Incorrect %s processing. Expected length: %d, actual length: %d, expected: '%s', actual: '%s'", + operation, expected.length(), actual.length(), expected, actual + )); + } + + private void dumpDebugInfo(@NotNull String problem) { + //LOG.error(String.format( + // "/***********************************************************\n" + + // " * Please email idea.log to Denis.Zhdanov@jetbrains.com\n" + + // " ***********************************************************/\n" + + // "Incorrect CharArray processing detected: '%s'. Start: %d, end: %d, text on batch update start: '%s', deferred changes history: %s, " + // + "current deferred changes: %s", + // problem, myStart, myEnd, myDebugTextOnBatchUpdateStart, myDebugDeferredChanges, myDeferredChangesStorage + //)); + LOG.error(String.format( + "Incorrect CharArray processing detected: '%s'. Start: %d, end: %d, text on batch update start: '%s', deferred changes history: %s, " + + "current deferred changes: %s", + problem, myStart, myEnd, myDebugTextOnBatchUpdateStart, myDebugDeferredChanges, myDeferredChangesStorage + )); + } +} diff --git a/idea_fix/src/com/intellij/openapi/fileEditor/impl/LoadTextUtil.java b/idea_fix/src/com/intellij/openapi/fileEditor/impl/LoadTextUtil.java new file mode 100644 index 00000000000..35114b8b85e --- /dev/null +++ b/idea_fix/src/com/intellij/openapi/fileEditor/impl/LoadTextUtil.java @@ -0,0 +1,418 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.fileEditor.impl; + +import com.intellij.lang.properties.charset.Native2AsciiCharset; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.fileTypes.BinaryFileDecompiler; +import com.intellij.openapi.fileTypes.BinaryFileTypeDecompilers; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.Trinity; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.CharsetToolkit; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.encoding.EncodingRegistry; +import com.intellij.testFramework.LightVirtualFile; +import com.intellij.util.ArrayUtil; +import com.intellij.util.text.CharArrayUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; + +public final class LoadTextUtil { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.fileEditor.impl.LoadTextUtil"); + private static final Key DETECTED_LINE_SEPARATOR_KEY = Key.create("DETECTED_LINE_SEPARATOR_KEY"); + + private LoadTextUtil() { + } + + @NotNull + private static Pair 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': + buffer.put(dst++, '\n'); + crCount++; + break; + case '\n': + if (prev == '\r') { + crCount--; + crlfCount++; + } else { + buffer.put(dst++, '\n'); + lfCount++; + } + break; + default: + 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 = buffer.length() == dst ? buffer : buffer.subSequence(0, dst); + return Pair.create(result, detectedLineSeparator); + } + + private static Charset detectCharset(@NotNull VirtualFile virtualFile, @NotNull byte[] content) { + if (virtualFile.isCharsetSet()) return virtualFile.getCharset(); + + Charset charset = doDetectCharset(virtualFile, content); + charset = charset == null ? EncodingRegistry.getInstance().getDefaultCharset() : charset; + if (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).getFirst(); + } + + @NotNull + private static Pair doDetectCharsetAndSetBOM(@NotNull VirtualFile virtualFile, @NotNull byte[] content) { + Charset charset = detectCharset(virtualFile, content); + Pair bomAndCharset = getBOMAndCharset(content, charset); + final byte[] bom = bomAndCharset.second; + if (bom.length != 0) { + virtualFile.setBOM(bom); + } + return bomAndCharset; + } + + private static Charset doDetectCharset(@NotNull VirtualFile virtualFile, @NotNull byte[] content) { + Trinity guessed = guessFromContent(virtualFile, content, content.length); + if (guessed != null && guessed.first != null) return guessed.first; + + FileType fileType = virtualFile.getFileType(); + String charsetName = fileType.getCharset(virtualFile, content); + + if (charsetName == null) { + Charset saved = EncodingRegistry.getInstance().getEncoding(virtualFile, true); + if (saved != null) return saved; + } + return CharsetToolkit.forName(charsetName); + } + + @Nullable("null means no luck, otherwise it's tuple(guessed encoding, hint about content if was unable to guess, BOM)") + public static Trinity guessFromContent(VirtualFile virtualFile, byte[] content, int length) { + EncodingRegistry settings = EncodingRegistry.getInstance(); + boolean shouldGuess = settings != null && settings.isUseUTFGuessing(virtualFile); + CharsetToolkit toolkit = shouldGuess ? new CharsetToolkit(content, EncodingRegistry.getInstance().getDefaultCharset()) : null; + setCharsetWasDetectedFromBytes(virtualFile, false); + if (shouldGuess) { + toolkit.setEnforce8Bit(true); + Charset charset = toolkit.guessFromBOM(); + if (charset != null) { + setCharsetWasDetectedFromBytes(virtualFile, true); + byte[] bom = CharsetToolkit.getBom(charset); + if (bom == null) bom = CharsetToolkit.UTF8_BOM; + return Trinity.create(charset, null, bom); + } + CharsetToolkit.GuessedEncoding guessed = toolkit.guessFromContent(length); + if (guessed == CharsetToolkit.GuessedEncoding.VALID_UTF8) { + setCharsetWasDetectedFromBytes(virtualFile, true); + return Trinity.create(CharsetToolkit.UTF8_CHARSET, null, null); //UTF detected, ignore all directives + } + return Trinity.create(null, guessed, null); + } + return null; + } + + @NotNull + private static Pair getBOMAndCharset(@NotNull byte[] content, final Charset charset) { + if (charset != null && charset.name().contains(CharsetToolkit.UTF8) && CharsetToolkit.hasUTF8Bom(content)) { + return Pair.create(charset, CharsetToolkit.UTF8_BOM); + } + try { + if (CharsetToolkit.hasUTF16LEBom(content)) { + return Pair.create(CharsetToolkit.UTF_16LE_CHARSET, CharsetToolkit.UTF16LE_BOM); + } + if (CharsetToolkit.hasUTF16BEBom(content)) { + return Pair.create(CharsetToolkit.UTF_16BE_CHARSET, CharsetToolkit.UTF16BE_BOM); + } + } catch (UnsupportedCharsetException ignore) { + } + if (charset == null) { + return Pair.create(Charset.defaultCharset(), CharsetToolkit.UTF8_BOM); + } + return Pair.create(charset, ArrayUtil.EMPTY_BYTE_ARRAY); + } + + /** + * Gets the Writer for this file and sets modification stamp and time stamp to the specified values + * after closing the Writer.

+ *

+ * Normally you should not use this method. + * + * @param project + * @param virtualFile + * @param requestor any object to control who called this method. Note that + * it is considered to be an external change if requestor is null. + * See {@link com.intellij.openapi.vfs.VirtualFileEvent#getRequestor} + * @param text + * @param newModificationStamp new modification stamp or -1 if no special value should be set @return Writer + * @throws java.io.IOException if an I/O error occurs + * @see com.intellij.openapi.vfs.VirtualFile#getModificationStamp() + */ + @SuppressWarnings({"IOResourceOpenedButNotSafelyClosed"}) + public static Writer getWriter(@Nullable Project project, @NotNull VirtualFile virtualFile, Object requestor, @NotNull String text, final long newModificationStamp) + throws IOException { + Charset existing = virtualFile.getCharset(); + Charset specified = extractCharsetFromFileContent(project, virtualFile, text); + Charset charset = chooseMostlyHarmlessCharset(existing, specified, text); + if (charset != null) { + if (!charset.equals(existing)) { + virtualFile.setCharset(charset); + } + setDetectedFromBytesFlagBack(virtualFile, charset, text); + } + + // in c ase 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) charset = fromBom; + + OutputStream outputStream = virtualFile.getOutputStream(requestor, newModificationStamp, -1); + OutputStreamWriter writer = charset == null ? new OutputStreamWriter(outputStream) : new OutputStreamWriter(outputStream, charset); + // no need to buffer ByteArrayOutputStream + return outputStream instanceof ByteArrayOutputStream ? writer : new BufferedWriter(writer); + } + + private static void setDetectedFromBytesFlagBack(@NotNull VirtualFile virtualFile, @NotNull Charset charset, @NotNull String text) { + if (virtualFile.getBOM() != null) { + // prevent file to be reloaded in other encoding after save with BOM + setCharsetWasDetectedFromBytes(virtualFile, true); + return; + } + + byte[] content = text.getBytes(charset); + CharsetToolkit.GuessedEncoding guessedEncoding = new CharsetToolkit(content).guessFromContent(content.length); + if (guessedEncoding == CharsetToolkit.GuessedEncoding.VALID_UTF8) { + setCharsetWasDetectedFromBytes(virtualFile, true); + } + } + + private static Charset chooseMostlyHarmlessCharset(Charset existing, Charset specified, String text) { + if (existing == null) return specified; + if (specified == null) return existing; + if (specified.equals(existing)) return specified; + if (isSupported(specified, text)) return specified; //if explicitly specified encoding is safe, return it + if (isSupported(existing, text)) return existing; //otherwise stick to the old encoding if it's ok + return specified; //if both are bad there is no difference + } + + private static boolean isSupported(@NotNull Charset charset, @NotNull String str) { + if (!charset.canEncode()) return false; + ByteBuffer out = charset.encode(str); + CharBuffer buffer = charset.decode(out); + return str.equals(buffer.toString()); + } + + public static Charset extractCharsetFromFileContent(@Nullable Project project, @NotNull VirtualFile virtualFile, @NotNull String text) { + Charset charset = charsetFromContentOrNull(project, virtualFile, text); + if (charset == null) charset = virtualFile.getCharset(); + return charset; + } + + @Nullable("returns null if cannot determine from content") + public static Charset charsetFromContentOrNull(@Nullable Project project, @NotNull VirtualFile virtualFile, @NotNull String text) { + FileType fileType = virtualFile.getFileType(); + if (fileType instanceof LanguageFileType) { + return ((LanguageFileType) fileType).extractCharsetFromFileContent(project, virtualFile, text); + } + return null; + } + + public static CharSequence loadText(@NotNull VirtualFile file) { + if (file instanceof LightVirtualFile) { + CharSequence content = ((LightVirtualFile) file).getContent(); + if (StringUtil.indexOf(content, '\r') == -1) return content; + + CharBuffer buffer = CharBuffer.allocate(content.length()); + buffer.append(content); + buffer.rewind(); + return convertLineSeparators(buffer).first; + } + + assert !file.isDirectory() : "'" + file.getPresentableUrl() + "' is directory"; + final FileType fileType = file.getFileType(); + + if (fileType.isBinary()) { + final BinaryFileDecompiler decompiler = BinaryFileTypeDecompilers.INSTANCE.forFileType(fileType); + if (decompiler != null) { + CharSequence text = decompiler.decompile(file); + StringUtil.assertValidSeparators(text); + return text; + } + + throw new IllegalArgumentException("Attempt to load text for binary file, that doesn't have decompiler plugged in: " + file.getPresentableUrl()); + } + + 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); + } + + @NotNull + public static CharSequence getTextByBinaryPresentation(@NotNull byte[] bytes, @NotNull VirtualFile virtualFile, final boolean rememberDetectedSeparators) { + Pair pair = doDetectCharsetAndSetBOM(virtualFile, bytes); + final Charset charset = pair.getFirst(); + byte[] bom = pair.getSecond(); + int offset = bom == null ? 0 : bom.length; + + final Pair result = convertBytes(bytes, charset, offset); + if (rememberDetectedSeparators) { + virtualFile.putUserData(DETECTED_LINE_SEPARATOR_KEY, 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.getUserData(DETECTED_LINE_SEPARATOR_KEY); + } + + /** + * Change line separator for the file to the specified value (assumes that the documents were saved) + * + * @param project the project instance + * @param requestor the requestor for the operation + * @param file the file to convert + * @param newLineSeparator the new line separator for the file + * @throws java.io.IOException in the case of IO problem + */ + public static void changeLineSeparator(@Nullable Project project, + @Nullable Object requestor, + @NotNull VirtualFile file, + @NotNull String newLineSeparator) throws IOException { + String lineSeparator = getDetectedLineSeparator(file); + if (lineSeparator != null && lineSeparator.equals(newLineSeparator)) { + return; + } + CharSequence cs = getTextByBinaryPresentation(file.contentsToByteArray(), file); + lineSeparator = getDetectedLineSeparator(file); + if (lineSeparator == null || lineSeparator.equals(newLineSeparator)) { + return; + } + if (!newLineSeparator.equals("\n")) { + cs = StringUtil.convertLineSeparators(cs.toString(), newLineSeparator); + } + String text = cs.toString(); + file.putUserData(DETECTED_LINE_SEPARATOR_KEY, newLineSeparator); + Writer w = getWriter(project, file, requestor, text, System.currentTimeMillis()); + try { + w.write(text); + } finally { + w.close(); + } + } + + @NotNull + public static CharSequence getTextByBinaryPresentation(@NotNull byte[] bytes, Charset charset) { + Pair pair = getBOMAndCharset(bytes, charset); + byte[] bom = pair.getSecond(); + int offset = bom == null ? 0 : bom.length; + + final Pair result = convertBytes(bytes, charset, offset); + return result.getFirst(); + } + + // do not need to think about BOM here. it is processed outside + @NotNull + private static Pair convertBytes(@NotNull byte[] bytes, Charset charset, final int startOffset) { + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes, startOffset, bytes.length - startOffset); + + if (charset == null) { + charset = CharsetToolkit.getDefaultSystemCharset(); + } + if (charset == null) { + //noinspection HardCodedStringLiteral + charset = Charset.forName("ISO-8859-1"); + } + CharBuffer charBuffer = charset.decode(byteBuffer); + return convertLineSeparators(charBuffer); + } + + private static final Key CHARSET_WAS_DETECTED_FROM_BYTES = new Key("CHARSET_WAS_DETECTED_FROM_BYTES"); + + public static boolean wasCharsetDetectedFromBytes(@NotNull VirtualFile virtualFile) { + return virtualFile.getUserData(CHARSET_WAS_DETECTED_FROM_BYTES) != null; + } + + public static void setCharsetWasDetectedFromBytes(@NotNull VirtualFile virtualFile, boolean flag) { + virtualFile.putUserData(CHARSET_WAS_DETECTED_FROM_BYTES, flag ? Boolean.TRUE : null); + } +} diff --git a/idea_fix/src/com/intellij/openapi/util/IconLoader.java b/idea_fix/src/com/intellij/openapi/util/IconLoader.java new file mode 100644 index 00000000000..70c739fed80 --- /dev/null +++ b/idea_fix/src/com/intellij/openapi/util/IconLoader.java @@ -0,0 +1,351 @@ +/* + * Copyright 2000-2011 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.util; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.reference.SoftReference; +import com.intellij.util.ImageLoader; +import com.intellij.util.containers.ConcurrentHashMap; +import com.intellij.util.containers.WeakHashMap; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.lang.ref.Reference; +import java.net.URL; +import java.util.Map; + +//import sun.reflect.Reflection; + +public final class IconLoader { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.IconLoader"); + + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private static final ConcurrentHashMap ourIconsCache = new ConcurrentHashMap(100, 0.9f, 2); + + /** + * This cache contains mapping between icons and disabled icons. + */ + private static final Map ourIcon2DisabledIcon = new WeakHashMap(200); + + private static final ImageIcon EMPTY_ICON = new ImageIcon(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR)) { + @NonNls + public String toString() { + return "Empty icon " + super.toString(); + } + }; + + private static boolean ourIsActivated = false; + + private IconLoader() { + } + + @Deprecated + public static Icon getIcon(@NotNull final Image image) { + return new MyImageIcon(image); + } + + @NotNull + public static Icon getIcon(@NonNls final String path) { + int stackFrameCount = 2; + return IconLoader.EMPTY_ICON; + /*Class callerClass = Reflection.getCallerClass(stackFrameCount); + while (callerClass != null && callerClass.getClassLoader() == null) { // looks like a system class + callerClass = Reflection.getCallerClass(++stackFrameCount); + } + if (callerClass == null) { + callerClass = Reflection.getCallerClass(1); + } + return getIcon(path, callerClass);*/ + } + + @Nullable + /** + * Might return null if icon was not found. + * Use only if you expected null return value, otherwise see {@link com.intellij.openapi.util.IconLoader#getIcon(String)} + */ + public static Icon findIcon(@NonNls final String path) { + int stackFrameCount = 2; + return IconLoader.EMPTY_ICON; + /*Class callerClass = Reflection.getCallerClass(stackFrameCount); + while (callerClass != null && callerClass.getClassLoader() == null) { // looks like a system class + callerClass = Reflection.getCallerClass(++stackFrameCount); + } + if (callerClass == null) { + callerClass = Reflection.getCallerClass(1); + } + return findIcon(path, callerClass);*/ + } + + @NotNull + public static Icon getIcon(@NotNull String path, @NotNull final Class aClass) { + final Icon icon = findIcon(path, aClass); + if (icon == null) { + LOG.error("Icon cannot be found in '" + path + "', aClass='" + aClass + "'"); + } + return icon; + } + + public static void activate() { + ourIsActivated = true; + } + + private static boolean isLoaderDisabled() { + return !ourIsActivated; + } + + /** + * Might return null if icon was not found. + * Use only if you expected null return value, otherwise see {@link com.intellij.openapi.util.IconLoader#getIcon(String, Class)} + */ + @Nullable + public static Icon findIcon(@NotNull final String path, @NotNull final Class aClass) { + return findIcon(path, aClass, false); + } + + public static Icon findIcon(@NotNull final String path, @NotNull final Class aClass, boolean computeNow) { + /*final ByClass icon = new ByClass(aClass, path); + + if (computeNow || !Registry.is("ide.lazyIconLoading", true)) { + return icon.getOrComputeIcon(); + } + + return icon;*/ + return IconLoader.EMPTY_ICON; + } + + @Nullable + private static Icon findIcon(URL url) { + if (url == null) return null; + Icon icon = ourIconsCache.get(url); + if (icon == null) { + icon = new CachedImageIcon(url); + icon = ourIconsCache.cacheOrGet(url, icon); + } + return icon; + } + + @Nullable + public static Icon findIcon(final String path, final ClassLoader aClassLoader) { + if (!path.startsWith("/")) return null; + + final URL url = aClassLoader.getResource(path.substring(1)); + return findIcon(url); + } + + @Nullable + private static Icon checkIcon(final Image image, URL url) { + if (image == null || image.getHeight(LabelHolder.ourFakeComponent) < 1) { // image wasn't loaded or broken + return null; + } + + final Icon icon = getIcon(image); + if (icon != null && !isGoodSize(icon)) { + LOG.error("Invalid icon: " + url); // # 22481 + return EMPTY_ICON; + } + return icon; + } + + public static boolean isGoodSize(@NotNull final Icon icon) { + return icon.getIconWidth() > 0 && icon.getIconHeight() > 0; + } + + /** + * Gets (creates if necessary) disabled icon based on the passed one. + * + * @param icon + * @return ImageIcon constructed from disabled image of passed icon. + */ + @Nullable + public static Icon getDisabledIcon(final Icon icon) { + if (icon == null) { + return null; + } + Icon disabledIcon = ourIcon2DisabledIcon.get(icon); + if (disabledIcon == null) { + if (!isGoodSize(icon)) { + LOG.error(icon); // # 22481 + return EMPTY_ICON; + } + final BufferedImage image = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); + final Graphics2D graphics = image.createGraphics(); + + graphics.setColor(UIUtil.TRANSPARENT_COLOR); + graphics.fillRect(0, 0, icon.getIconWidth(), icon.getIconHeight()); + icon.paintIcon(LabelHolder.ourFakeComponent, graphics, 0, 0); + + graphics.dispose(); + + disabledIcon = new MyImageIcon(GrayFilter.createDisabledImage(image)); + ourIcon2DisabledIcon.put(icon, disabledIcon); + } + return disabledIcon; + } + + public static Icon getTransparentIcon(final Icon icon) { + return getTransparentIcon(icon, 0.5f); + } + + public static Icon getTransparentIcon(final Icon icon, final float alpha) { + return new Icon() { + public int getIconHeight() { + return icon.getIconHeight(); + } + + public int getIconWidth() { + return icon.getIconWidth(); + } + + public void paintIcon(final Component c, final Graphics g, final int x, final int y) { + final Graphics2D g2 = (Graphics2D) g; + final Composite saveComposite = g2.getComposite(); + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); + icon.paintIcon(c, g2, x, y); + g2.setComposite(saveComposite); + } + }; + } + + private static final class CachedImageIcon implements Icon { + private Object myRealIcon; + private final URL myUrl; + + public CachedImageIcon(URL url) { + myUrl = url; + } + + private synchronized Icon getRealIcon() { + if (isLoaderDisabled()) return EMPTY_ICON; + + if (myRealIcon instanceof Icon) return (Icon) myRealIcon; + + Icon icon; + if (myRealIcon instanceof Reference) { + icon = ((Reference) myRealIcon).get(); + if (icon != null) return icon; + } + + Image image = ImageLoader.loadFromUrl(myUrl); + icon = checkIcon(image, myUrl); + + if (icon != null) { + if (icon.getIconWidth() < 50 && icon.getIconHeight() < 50) { + myRealIcon = icon; + } else { + myRealIcon = new SoftReference(icon); + } + } + + return icon != null ? icon : EMPTY_ICON; + } + + public void paintIcon(Component c, Graphics g, int x, int y) { + getRealIcon().paintIcon(c, g, x, y); + } + + public int getIconWidth() { + return getRealIcon().getIconWidth(); + } + + public int getIconHeight() { + return getRealIcon().getIconHeight(); + } + } + + private static final class MyImageIcon extends ImageIcon { + public MyImageIcon(final Image image) { + super(image); + } + + public final synchronized void paintIcon(final Component c, final Graphics g, final int x, final int y) { + super.paintIcon(null, g, x, y); + } + } + + public abstract static class LazyIcon implements Icon { + private boolean myWasComputed; + private Icon myIcon; + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + final Icon icon = getOrComputeIcon(); + if (icon != null) { + icon.paintIcon(c, g, x, y); + } + } + + @Override + public int getIconWidth() { + final Icon icon = getOrComputeIcon(); + return icon != null ? icon.getIconWidth() : 0; + } + + @Override + public int getIconHeight() { + final Icon icon = getOrComputeIcon(); + return icon != null ? icon.getIconHeight() : 0; + } + + protected synchronized final Icon getOrComputeIcon() { + if (!myWasComputed) { + myWasComputed = true; + myIcon = compute(); + } + + return myIcon; + } + + public final void load() { + getIconWidth(); + } + + protected abstract Icon compute(); + } + + private static class ByClass extends LazyIcon { + private final Class myCallerClass; + private final String myPath; + + public ByClass(Class aClass, String path) { + myCallerClass = aClass; + myPath = path; + } + + @Override + protected Icon compute() { + URL url = myCallerClass.getResource(myPath); + return findIcon(url); + } + + @Override + public String toString() { + return "icon path=" + myPath + " class=" + myCallerClass; + } + } + + private static class LabelHolder { + /** + * To get disabled icon with paint it into the image. Some icons require + * not null component to paint. + */ + private static final JComponent ourFakeComponent = new JLabel(); + } +} diff --git a/idea_fix/src/com/intellij/openapi/util/UserDataHolderBase.java b/idea_fix/src/com/intellij/openapi/util/UserDataHolderBase.java new file mode 100644 index 00000000000..7c7b4fa3c56 --- /dev/null +++ b/idea_fix/src/com/intellij/openapi/util/UserDataHolderBase.java @@ -0,0 +1,265 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.util; + + +//import com.intellij.util.concurrency.AtomicFieldUpdater; + +import com.intellij.util.containers.StripedLockConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +import java.util.ConcurrentModificationException; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; + +public class UserDataHolderBase implements UserDataHolderEx, Cloneable { + private static final Key> COPYABLE_USER_MAP_KEY = Key.create("COPYABLE_USER_MAP_KEY"); + + /** + * Concurrent writes to this field are via CASes only, using the {@link #} + * When map becomes empty, this field set to null atomically + *

+ * Basic state transitions are as follows: + *

+ * (adding keyvalue) (putUserData(key,value)) + * [myUserMap=null] -> [myUserMap=(key->value)] + *

+ * (adding another) (putUserData(key2,value2)) + * [myUserMap=(key->value)] -> [myUserMap=(key->value, key2->value2)] + *

+ * (removing keyvalue) (putUserData(k2,null)) + * [myUserMap=(key->value, k2->v2)] -> [myUserMap=(key->value)] + *

+ * (removing last entry) (putUserData(key,null)) + * [myUserMap=(key->value)] -> [myUserMap=null] + */ + private volatile ConcurrentMap myUserMap = null; + + protected Object clone() { + try { + UserDataHolderBase clone = (UserDataHolderBase) super.clone(); + clone.myUserMap = null; + copyCopyableDataTo(clone); + return clone; + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + + } + } + + @TestOnly + public String getUserDataString() { + final ConcurrentMap userMap = myUserMap; + if (userMap == null) { + return ""; + } + final Map copyableMap = getUserData(COPYABLE_USER_MAP_KEY); + return userMap.toString() + (copyableMap == null ? "" : copyableMap.toString()); + } + + public void copyUserDataTo(UserDataHolderBase other) { + ConcurrentMap map = myUserMap; + if (map == null) { + other.myUserMap = null; + } else { + ConcurrentMap fresh = createDataMap(map.size()); + fresh.putAll(map); + other.myUserMap = fresh; + } + } + + public T getUserData(@NotNull Key key) { + final Map map = myUserMap; + //noinspection unchecked + return map == null ? null : (T) map.get(key); + } + + public void putUserData(@NotNull Key key, @Nullable T value) { + while (true) { + try { + if (value == null) { + ConcurrentMap map = myUserMap; + if (map == null) break; + @SuppressWarnings("unchecked") + T previous = (T) map.remove(key); + boolean removed = previous != null; + if (removed) { + nullifyMapFieldIfEmpty(); + } + } else { + getOrCreateMap().put(key, value); + } + break; + } catch (ConcurrentModificationException ignored) { + } + } + } + + private static ConcurrentMap createDataMap(int initialCapacity) { + return new StripedLockConcurrentHashMap(initialCapacity); + } + + public T getCopyableUserData(Key key) { + return getCopyableUserDataImpl(key); + } + + protected final T getCopyableUserDataImpl(Key key) { + Map map = getUserData(COPYABLE_USER_MAP_KEY); + //noinspection unchecked + return map == null ? null : (T) map.get(key); + } + + public void putCopyableUserData(Key key, T value) { + putCopyableUserDataImpl(key, value); + } + + private Map getOrCreateCopyableMap(boolean create) { + Map copyMap = getUserData(COPYABLE_USER_MAP_KEY); + if (copyMap == null && create) { + copyMap = createDataMap(1); + copyMap = putUserDataIfAbsent(COPYABLE_USER_MAP_KEY, copyMap); + } + + return copyMap; + } + + protected final void putCopyableUserDataImpl(Key key, T value) { + while (true) { + try { + Map copyMap = getOrCreateCopyableMap(value != null); + if (copyMap == null) break; + + if (value == null) { + copyMap.remove(key); + if (copyMap.isEmpty()) { + ((StripedLockConcurrentHashMap) copyMap).blockModification(); + ConcurrentMap newCopyMap; + if (copyMap.isEmpty()) { + newCopyMap = null; + } else { + newCopyMap = createDataMap(copyMap.size()); + newCopyMap.putAll(copyMap); + } + boolean replaced = replace(COPYABLE_USER_MAP_KEY, copyMap, newCopyMap); + if (!replaced) continue; + } + } else { + copyMap.put(key, value); + } + break; + } catch (ConcurrentModificationException ignored) { + // someone blocked modification, retry + } + } + } + + private ConcurrentMap getOrCreateMap() { + while (true) { + ConcurrentMap map = myUserMap; + if (map != null) return map; + map = createDataMap(2); + boolean updated = true; + //throw new UnsupportedOperationException("Exception in (my) UserDataHolderBase"); +// boolean updated = updater.compareAndSet(this, null, map); + if (updated) { + return map; + } + } + } + + public boolean replace(@NotNull Key key, @Nullable T oldValue, @Nullable T newValue) { + while (true) { + try { + ConcurrentMap map = getOrCreateMap(); + if (oldValue == null) { + return newValue == null || map.putIfAbsent(key, newValue) == null; + } + if (newValue == null) { + boolean removed = map.remove(key, oldValue); + if (removed) { + nullifyMapFieldIfEmpty(); + } + return removed; + } + return map.replace(key, oldValue, newValue); + } catch (ConcurrentModificationException ignored) { + // someone blocked modification, retry + } + } + } + + @NotNull + public T putUserDataIfAbsent(@NotNull final Key key, @NotNull final T value) { + Object v = getOrCreateMap().get(key); + if (v != null) { + //noinspection unchecked + return (T) v; + } + while (true) { + try { + @SuppressWarnings("unchecked") + T prev = (T) getOrCreateMap().putIfAbsent(key, value); + return prev == null ? value : prev; + } catch (ConcurrentModificationException ignored) { + // someone blocked modification, retry + } + } + } + + public void copyCopyableDataTo(@NotNull UserDataHolderBase clone) { + Map copyableMap = getUserData(COPYABLE_USER_MAP_KEY); + if (copyableMap != null) { + ConcurrentMap copy = createDataMap(copyableMap.size()); + copy.putAll(copyableMap); + copyableMap = copy; + } + clone.putUserData(COPYABLE_USER_MAP_KEY, copyableMap); + } + + protected void clearUserData() { + myUserMap = null; + } + +// private static final AtomicFieldUpdater updater = AtomicFieldUpdater.forFieldOfType(UserDataHolderBase.class, ConcurrentMap.class); + + private void nullifyMapFieldIfEmpty() { + try { + while (true) { + StripedLockConcurrentHashMap map = (StripedLockConcurrentHashMap) myUserMap; + if (map == null || !map.isEmpty()) break; + map.blockModification(); // we block the map and either replace it with null or fail with replace, in both cases the map is thrown away + ConcurrentMap newMap; + if (map.isEmpty()) { + newMap = null; + } else { + // someone managed to add something in the meantime + // atomically replace the blocked map with newly created map filled with the data sneaked in + newMap = createDataMap(map.size()); + newMap.putAll(map); + } + boolean replaced = true; + //throw new UnsupportedOperationException("Exception in (my) UserDataHolderBase"); + //boolean replaced = updater.compareAndSet(this, map, newMap); + if (replaced) break; + // else someone has replaced map already and pushing back the changes is his responsibility + } + } catch (ConcurrentModificationException ignored) { + // somebody has already blocked the map, back off + } + } +} diff --git a/idea_fix/src/com/intellij/openapi/vfs/impl/jar/CoreJarHandler.java b/idea_fix/src/com/intellij/openapi/vfs/impl/jar/CoreJarHandler.java new file mode 100644 index 00000000000..3362a67f7be --- /dev/null +++ b/idea_fix/src/com/intellij/openapi/vfs/impl/jar/CoreJarHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2011 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.vfs.impl.jar; + +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author yole + */ +public class CoreJarHandler extends JarHandlerBase { + private final Map myFileMap = new HashMap(); + private final CoreJarFileSystem myFileSystem; + + public CoreJarHandler(CoreJarFileSystem fileSystem, String path) { + super(fileSystem, path); + myFileSystem = fileSystem; + } + + @Nullable + public VirtualFile findFileByPath(String pathInJar) { + if (getZip() == null) { + return null; + } + VirtualFile file = myFileMap.get(pathInJar); + if (file == null) { + if (pathInJar.length() > 0) { + EntryInfo entryInfo = getEntryInfo(pathInJar); + if (entryInfo == null) { + return null; + } + } + file = new CoreJarVirtualFile(myFileSystem, this, pathInJar); + myFileMap.put(pathInJar, file); + } + return file; + } +} diff --git a/idea_fix/src/com/intellij/openapi/vfs/impl/jar/JarHandlerBase.java b/idea_fix/src/com/intellij/openapi/vfs/impl/jar/JarHandlerBase.java new file mode 100644 index 00000000000..d87e1d322d5 --- /dev/null +++ b/idea_fix/src/com/intellij/openapi/vfs/impl/jar/JarHandlerBase.java @@ -0,0 +1,266 @@ +/* + * Copyright 2000-2011 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.vfs.impl.jar; + +import com.intellij.openapi.util.io.BufferExposingByteArrayInputStream; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ArrayUtil; +import com.intellij.util.TimedReference; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class JarHandlerBase { + protected final TimedReference myZipFile = new TimedReference(null); + protected SoftReference> myRelPathsToEntries = new SoftReference>(null); + protected final Object lock = new Object(); + private InputStream inputStream; + + protected final String myBasePath; + + protected static class EntryInfo { + public EntryInfo(final String shortName, final EntryInfo parent, final boolean directory, byte[] content) { + this.shortName = new String(shortName); + this.parent = parent; + isDirectory = directory; + this.content = content; + + } + + final boolean isDirectory; + final byte[] content; + protected final String shortName; + final EntryInfo parent; + } + + public JarHandlerBase(CoreJarFileSystem myFileSystem, String path) { + if (inputStream == null) { + try { + inputStream = new VirtualJarFile(myFileSystem, path).getInputStream(); + } catch (IOException e) { + e.printStackTrace(); + } + } + myBasePath = path; + } + + @NotNull + protected Map initEntries() { + synchronized (lock) { + Map map = myRelPathsToEntries.get(); + if (map == null) { + final ZipInputStream zip = getZip(); + + map = new HashMap(); + if (zip != null) { + map.put("", new EntryInfo("", null, true, new byte[0])); + try { + ZipEntry entry = zip.getNextEntry(); + while (entry != null) { + final String name = entry.getName(); + final boolean isDirectory = name.endsWith("/"); + if (entry.getExtra() == null) { + byte[] cont = new byte[(int) entry.getSize()]; + ByteArrayOutputStream byteArray = new ByteArrayOutputStream(); + InputStream stream = getZip(); + if (stream != null) { + int tmp; + if ((tmp = stream.read(cont)) == entry.getSize()) { + byteArray.write(cont, 0, tmp); + entry.setExtra(byteArray.toByteArray()); + } else { + int readFromIS = tmp; + if (tmp < entry.getSize()) { + byteArray.write(cont, 0, tmp); + while (((tmp = stream.read(cont)) != -1) && (tmp + readFromIS <= entry.getSize())) { + byteArray.write(cont, 0, tmp); + readFromIS += tmp; + } + entry.setExtra(byteArray.toByteArray()); + } + } + } + } + getOrCreate(isDirectory ? name.substring(0, name.length() - 1) : name, isDirectory, map, entry.getExtra()); + + entry = zip.getNextEntry(); + } + } catch (IOException e) { + e.printStackTrace(); + } + myRelPathsToEntries = new SoftReference>(map); + } + } + return map; + } + } + + public File getMirrorFile(File originalFile) { + return originalFile; + } + + @Nullable + public ZipInputStream getZip() { + ZipInputStream zip = myZipFile.get(); + if (zip == null) { + if (inputStream == null) { + throw new IllegalArgumentException("Input Stream is null"); + } else { + zip = new ZipInputStream(inputStream); + } + myZipFile.set(zip); + } + return zip; + } + + protected InputStream getOriginalFile() { + + return inputStream; + } + + private static EntryInfo getOrCreate(String entryName, boolean isDirectory, Map map, byte[] content) { + EntryInfo info = map.get(entryName); + if (info == null) { + int idx = entryName.lastIndexOf('/'); + final String parentEntryName = idx > 0 ? entryName.substring(0, idx) : ""; + String shortName = idx > 0 ? entryName.substring(idx + 1) : entryName; + if (".".equals(shortName)) return getOrCreate(parentEntryName, true, map, content); + + info = new EntryInfo(shortName, getOrCreate(parentEntryName, true, map, content), isDirectory, content); + map.put(entryName, info); + } + + return info; + } + + @NotNull + public String[] list(@NotNull final VirtualFile file) { + synchronized (lock) { + EntryInfo parentEntry = getEntryInfo(file); + + Set names = new HashSet(); + for (EntryInfo info : getEntriesMap().values()) { + if (info.parent == parentEntry) { + names.add(info.shortName); + } + } + + return ArrayUtil.toStringArray(names); + } + } + + protected EntryInfo getEntryInfo(final VirtualFile file) { + synchronized (lock) { + String parentPath = getRelativePath(file); + return getEntryInfo(parentPath); + } + } + + public EntryInfo getEntryInfo(String parentPath) { + return getEntriesMap().get(parentPath); + } + + protected Map getEntriesMap() { + return initEntries(); + } + + private String getRelativePath(final VirtualFile file) { +// throw new UnsupportedOperationException(file.getPath()); + final String path = file.getPath().substring(myBasePath.length() + 1); + return path.startsWith("/") ? path.substring(1) : path; + } + + @Nullable + private ZipEntry convertToEntry(VirtualFile file) { + String path = getRelativePath(file); + final ZipInputStream zip = getZip(); + return null; + } + + @Nullable + private EntryInfo convertToISEntry(VirtualFile file) { + String path = getRelativePath(file); + final ZipInputStream zip = getZip(); + return myRelPathsToEntries.get().get(path); + } + + public long getLength(@NotNull final VirtualFile file) { + synchronized (lock) { + final ZipEntry entry = convertToEntry(file); + return entry != null ? entry.getSize() : 0; + } + } + + @NotNull + public InputStream getInputStream(@NotNull final VirtualFile file) throws IOException { + return new BufferExposingByteArrayInputStream(contentsToByteArray(file)); + } + + @NotNull + public byte[] contentsToByteArray(@NotNull final VirtualFile file) throws IOException { + synchronized (lock) { + EntryInfo info = convertToISEntry(file); + if (info == null) { + return new byte[0]; + } + byte[] content = info.content; + if (content == null) { + return new byte[0]; + } + return content; + + } + } + + public long getTimeStamp(@NotNull final VirtualFile file) { + if (file.getParent() == null) return file.getTimeStamp(); // Optimization +// if (file.getParent() == null) return getOriginalFile().lastModified(); // Optimization + synchronized (lock) { + final ZipEntry entry = convertToEntry(file); + return entry != null ? entry.getTime() : -1L; + } + } + + public boolean isDirectory(@NotNull final VirtualFile file) { + if (file.getParent() == null) return true; // Optimization + synchronized (lock) { + final String path = getRelativePath(file); + final EntryInfo info = getEntryInfo(path); + return info == null || info.isDirectory; + } + } + + public boolean exists(@NotNull final VirtualFile fileOrDirectory) { + if (fileOrDirectory.getParent() == null) { + // Optimization. Do not build entries if asked for jar root existence. + return myZipFile.get() != null; +// return myZipFile.get() != null || getOriginalFile().exists(); + } + + return getEntryInfo(fileOrDirectory) != null; + } +} diff --git a/idea_fix/src/com/intellij/openapi/vfs/impl/jar/VirtualJarFile.java b/idea_fix/src/com/intellij/openapi/vfs/impl/jar/VirtualJarFile.java new file mode 100644 index 00000000000..4021bb413f1 --- /dev/null +++ b/idea_fix/src/com/intellij/openapi/vfs/impl/jar/VirtualJarFile.java @@ -0,0 +1,117 @@ +package com.intellij.openapi.vfs.impl.jar; + +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileSystem; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Created by IntelliJ IDEA. + * User: Natalia.Ukhorskaya + * Date: 12/1/11 + * Time: 5:06 PM + */ + +public class VirtualJarFile extends VirtualFile { + private byte[] byteArray; + private final CoreJarFileSystem myFileSystem; + private final String name; + + public VirtualJarFile(CoreJarFileSystem myFileSystem, String pathInJar) { + this.myFileSystem = myFileSystem; + this.name = pathInJar; + } + + @NotNull + @Override + public String getName() { + return name; + } + + @NotNull + @Override + public VirtualFileSystem getFileSystem() { + return myFileSystem; + } + + @Override + public String getPath() { + return name; + } + + @Override + public boolean isWritable() { + return false; + } + + @Override + public boolean isDirectory() { + return false; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public VirtualFile getParent() { + return null; + } + + @Override + public VirtualFile[] getChildren() { + return VirtualFile.EMPTY_ARRAY; + } + + @NotNull + @Override + public OutputStream getOutputStream(Object requestor, long newModificationStamp, long newTimeStamp) throws IOException { + return System.out; + } + + @NotNull + @Override + public byte[] contentsToByteArray() throws IOException { + if (byteArray.length <= 0) { + int length; + byte[] tmp = new byte[1024]; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + InputStream rtJar = VirtualJarFile.class.getResourceAsStream("/" + name); + try { + while ((length = rtJar.read(tmp)) >= 0) { + out.write(tmp, 0, length); + } + } catch (IOException e) { + e.printStackTrace(); + return new byte[0]; + } + byteArray = out.toByteArray(); + } + return byteArray; + } + + @Override + public long getTimeStamp() { + return 0; + } + + @Override + public long getLength() { + return byteArray.length; + } + + @Override + public void refresh(boolean asynchronous, boolean recursive, Runnable postRunnable) { + + } + + @Override + public InputStream getInputStream() throws IOException { + return VirtualJarFile.class.getResourceAsStream("/" + name); + } +} diff --git a/idea_fix/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSConstants.java b/idea_fix/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSConstants.java new file mode 100644 index 00000000000..da2c8391363 --- /dev/null +++ b/idea_fix/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSConstants.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2011 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.vfs.newvfs.persistent; + +public class PersistentFSConstants { + public static final long FILE_LENGTH_TO_CACHE_THRESHOLD = 20 * 1024 * 1024; // 20 megabytes + /** + * always in range [0, PersistentFS.FILE_LENGTH_TO_CACHE_THRESHOLD] + */ + public static final int MAX_INTELLISENSE_FILESIZE = maxIntellisenseFileSize(); +// @NonNls private static final String MAX_INTELLISENSE_SIZE_PROPERTY = "idea.max.intellisense.filesize"; + + private PersistentFSConstants() { + } + + private static int maxIntellisenseFileSize() { + final int maxLimitBytes = (int) FILE_LENGTH_TO_CACHE_THRESHOLD; + final String userLimitKb = "100"; +// final String userLimitKb = System.getProperty(MAX_INTELLISENSE_SIZE_PROPERTY); + try { + return userLimitKb != null ? Math.min(Integer.parseInt(userLimitKb) * 1024, maxLimitBytes) : maxLimitBytes; + } catch (NumberFormatException ignored) { + return maxLimitBytes; + } + } +} diff --git a/idea_fix/src/com/intellij/psi/SingleRootFileViewProvider.java b/idea_fix/src/com/intellij/psi/SingleRootFileViewProvider.java new file mode 100644 index 00000000000..64f2802d665 --- /dev/null +++ b/idea_fix/src/com/intellij/psi/SingleRootFileViewProvider.java @@ -0,0 +1,535 @@ +/* + * Copyright 2000-2011 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.psi; + +import com.intellij.lang.Language; +import com.intellij.lang.LanguageParserDefinitions; +import com.intellij.lang.ParserDefinition; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.undo.UndoConstants; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.extensions.Extensions; +import com.intellij.openapi.file.exclude.ProjectFileExclusionManager; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileEditor.impl.LoadTextUtil; +import com.intellij.openapi.fileTypes.*; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.UserDataHolderBase; +import com.intellij.openapi.vfs.NonPhysicalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.newvfs.persistent.PersistentFSConstants; +import com.intellij.psi.impl.PsiFileEx; +import com.intellij.psi.impl.PsiManagerEx; +import com.intellij.psi.impl.PsiManagerImpl; +import com.intellij.psi.impl.compiled.ClsFileImpl; +import com.intellij.psi.impl.file.PsiBinaryFileImpl; +import com.intellij.psi.impl.source.PsiFileImpl; +import com.intellij.psi.impl.source.PsiPlainTextFileImpl; +import com.intellij.psi.impl.source.tree.FileElement; +import com.intellij.testFramework.LightVirtualFile; +import com.intellij.util.LocalTimeCounter; +import com.intellij.util.ReflectionCache; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.SoftReference; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +public class SingleRootFileViewProvider extends UserDataHolderBase implements FileViewProvider { + private static final Key OUR_NO_SIZE_LIMIT_KEY = Key.create("no.size.limit"); + private static final Logger LOG = Logger.getInstance("#" + SingleRootFileViewProvider.class.getCanonicalName()); + private final PsiManager myManager; + private final VirtualFile myVirtualFile; + private final boolean myEventSystemEnabled; + private final boolean myPhysical; + private final AtomicReference myPsiFile = new AtomicReference(); + private volatile Content myContent; + private volatile SoftReference myDocument; + private final Language myBaseLanguage; + private final ProjectFileExclusionManager myExclusionManager; + + public SingleRootFileViewProvider(@NotNull PsiManager manager, @NotNull VirtualFile file) { + this(manager, file, true); + } + + public SingleRootFileViewProvider(@NotNull PsiManager manager, @NotNull VirtualFile virtualFile, final boolean physical) { + this(manager, virtualFile, physical, calcBaseLanguage(virtualFile, manager.getProject())); + } + + protected SingleRootFileViewProvider(@NotNull PsiManager manager, @NotNull VirtualFile virtualFile, final boolean physical, @NotNull Language language) { + myManager = manager; + myVirtualFile = virtualFile; + myEventSystemEnabled = physical; + myBaseLanguage = language; + setContent(new VirtualFileContent()); + myPhysical = isEventSystemEnabled() && + !(virtualFile instanceof LightVirtualFile) && + !(virtualFile.getFileSystem() instanceof NonPhysicalFileSystem); + myExclusionManager = ProjectFileExclusionManager.SERVICE.getInstance(manager.getProject()); + } + + @Override + @NotNull + public Language getBaseLanguage() { + return myBaseLanguage; + } + + private static Language calcBaseLanguage(@NotNull VirtualFile file, @NotNull Project project) { + if (file instanceof LightVirtualFile) { + final Language language = ((LightVirtualFile) file).getLanguage(); + if (language != null) { + return language; + } + } + + FileType fileType = file.getFileType(); + // Do not load content + if (fileType == UnknownFileType.INSTANCE) { + fileType = FileTypeRegistry.getInstance().detectFileTypeFromContent(file); + } + if (fileType.isBinary()) return Language.ANY; + if (isTooLarge(file)) return PlainTextLanguage.INSTANCE; + + if (fileType instanceof LanguageFileType) { + return LanguageSubstitutors.INSTANCE.substituteLanguage(((LanguageFileType) fileType).getLanguage(), file, project); + } + + final ContentBasedFileSubstitutor[] processors = Extensions.getExtensions(ContentBasedFileSubstitutor.EP_NAME); + for (ContentBasedFileSubstitutor processor : processors) { + Language language = processor.obtainLanguageForFile(file); + if (language != null) return language; + } + + return PlainTextLanguage.INSTANCE; + } + + @Override + @NotNull + public Set getLanguages() { + return Collections.singleton(getBaseLanguage()); + } + + @Override + @Nullable + public final PsiFile getPsi(@NotNull Language target) { + /*if (!isPhysical()) { + ((PsiManagerEx) myManager).getFileManager().setViewProvider(getVirtualFile(), this); + }*/ + return getPsiInner(target); + } + + @Override + @NotNull + public List getAllFiles() { + return Collections.singletonList(getPsi(getBaseLanguage())); + } + + @Nullable + protected PsiFile getPsiInner(final Language target) { + if (target != getBaseLanguage()) { + return null; + } + PsiFile psiFile = myPsiFile.get(); + if (psiFile == null) { + psiFile = createFile(); + boolean set = myPsiFile.compareAndSet(null, psiFile); + if (!set) { + psiFile = myPsiFile.get(); + } + } + return psiFile; + } + + @Override + public void beforeContentsSynchronized() { + unsetPsiContent(); + } + + @Override + public void contentsSynchronized() { + unsetPsiContent(); + } + + private void unsetPsiContent() { + if (!(myContent instanceof PsiFileContent)) return; + final Document cachedDocument = getCachedDocument(); + setContent(cachedDocument == null ? new VirtualFileContent() : new DocumentContent()); + } + + public void beforeDocumentChanged() { + final PsiFileImpl psiFile = (PsiFileImpl) getCachedPsi(getBaseLanguage()); + if (psiFile != null && psiFile.isContentsLoaded() && getContent() instanceof DocumentContent) { + setContent(new PsiFileContent(psiFile, getModificationStamp())); + } + } + + @Override + public void rootChanged(PsiFile psiFile) { + if (((PsiFileEx) psiFile).isContentsLoaded()) { + setContent(new PsiFileContent((PsiFileImpl) psiFile, LocalTimeCounter.currentTime())); + } + } + + @Override + public boolean isEventSystemEnabled() { + return myEventSystemEnabled; + } + + @Override + public boolean isPhysical() { + return myPhysical; + } + + @Override + public long getModificationStamp() { + return getContent().getModificationStamp(); + } + + @Override + public boolean supportsIncrementalReparse(final Language rootLanguage) { + return true; + } + + + public PsiFile getCachedPsi(Language target) { + return myPsiFile.get(); + } + + public FileElement[] getKnownTreeRoots() { + PsiFile psiFile = myPsiFile.get(); + if (psiFile == null || !(psiFile instanceof PsiFileImpl)) return new FileElement[0]; + if (((PsiFileImpl) psiFile).getTreeElement() == null) return new FileElement[0]; + return new FileElement[]{(FileElement) psiFile.getNode()}; + } + + private PsiFile createFile() { + try { + final VirtualFile vFile = getVirtualFile(); + if (vFile.isDirectory()) return null; + if (isIgnored()) return null; + + final Project project = myManager.getProject(); + if (isPhysical()) { // check directories consistency + final VirtualFile parent = vFile.getParent(); + if (parent == null) return null; + final PsiDirectory psiDir = getManager().findDirectory(parent); + if (psiDir == null) return null; + } + + FileType fileType = vFile.getFileType(); + PsiFile file = null; + if (fileType.isBinary() || vFile.isSpecialFile()) { + //TODO check why ClsFileImpl doesn't created automatically with create method + file = new ClsFileImpl((PsiManagerImpl) getManager(), this); + //file = new PsiBinaryFileImpl((PsiManagerImpl) getManager(), this); + } else { + if (!isTooLarge(vFile)) { + final PsiFile psiFile = createFile(getBaseLanguage()); + if (psiFile != null) file = psiFile; + } else { + file = new PsiPlainTextFileImpl(this); + } + } + return file; + } catch (ProcessCanceledException e) { + e.printStackTrace(); + throw e; + + } catch (Throwable e) { + e.printStackTrace(); + LOG.error(e); + return null; + } + } + + protected boolean isIgnored() { + final VirtualFile file = getVirtualFile(); + if (file instanceof LightVirtualFile) return false; + if (myExclusionManager != null && myExclusionManager.isExcluded(file)) return true; + return FileTypeRegistry.getInstance().isFileIgnored(file); + } + + @Nullable + protected PsiFile createFile(@NotNull Project project, @NotNull VirtualFile file, @NotNull FileType fileType) { + if (fileType.isBinary() || file.isSpecialFile()) { + return new PsiBinaryFileImpl((PsiManagerImpl) getManager(), this); + } + if (!isTooLarge(file)) { + final PsiFile psiFile = createFile(getBaseLanguage()); + if (psiFile != null) return psiFile; + } + + return new PsiPlainTextFileImpl(this); + } + + public static boolean isTooLarge(@NotNull VirtualFile vFile) { + if (!checkFileSizeLimit(vFile)) return false; + return fileSizeIsGreaterThan(vFile, PersistentFSConstants.MAX_INTELLISENSE_FILESIZE); + } + + private static boolean checkFileSizeLimit(@NotNull VirtualFile vFile) { + return !Boolean.TRUE.equals(vFile.getUserData(OUR_NO_SIZE_LIMIT_KEY)); + } + + public static void doNotCheckFileSizeLimit(@NotNull VirtualFile vFile) { + vFile.putUserData(OUR_NO_SIZE_LIMIT_KEY, Boolean.TRUE); + } + + public static boolean isTooLarge(@NotNull VirtualFile vFile, final long contentSize) { + if (!checkFileSizeLimit(vFile)) return false; + return contentSize > PersistentFSConstants.MAX_INTELLISENSE_FILESIZE; + } + + private static boolean fileSizeIsGreaterThan(@NotNull VirtualFile vFile, final long maxBytes) { + if (vFile instanceof LightVirtualFile) { + // This is optimization in order to avoid conversion of [large] file contents to bytes + final int lengthInChars = ((LightVirtualFile) vFile).getContent().length(); + if (lengthInChars < maxBytes / 2) return false; + if (lengthInChars > maxBytes) return true; + } + + return vFile.getLength() > maxBytes; + } + + @Nullable + protected PsiFile createFile(Language lang) { + if (lang != getBaseLanguage()) return null; + final ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(lang); + if (parserDefinition != null) { + return parserDefinition.createFile(this); + } + return null; + } + + @Override + @NotNull + public PsiManager getManager() { + return myManager; + } + + @Override + @NotNull + public CharSequence getContents() { + return getContent().getText(); + } + + @Override + @NotNull + public VirtualFile getVirtualFile() { + return myVirtualFile; + } + + @Nullable + private Document getCachedDocument() { + final Document document = myDocument != null ? myDocument.get() : null; + if (document != null) return document; + return FileDocumentManager.getInstance().getCachedDocument(getVirtualFile()); + } + + @Override + public Document getDocument() { + Document document = myDocument != null ? myDocument.get() : null; + if (document == null/* TODO[ik] make this change && isEventSystemEnabled()*/) { + document = FileDocumentManager.getInstance().getDocument(getVirtualFile()); + myDocument = new SoftReference(document); + } + if (document != null && getContent() instanceof VirtualFileContent) { + setContent(new DocumentContent()); + } + return document; + } + + @Override + public FileViewProvider clone() { + final VirtualFile origFile = getVirtualFile(); + LightVirtualFile copy = new LightVirtualFile(origFile.getName(), origFile.getFileType(), getContents(), origFile.getCharset(), getModificationStamp()); + copy.setOriginalFile(origFile); + copy.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE); + copy.setCharset(origFile.getCharset()); + return createCopy(copy); + } + + @NotNull + @Override + public SingleRootFileViewProvider createCopy(final VirtualFile copy) { + return new SingleRootFileViewProvider(getManager(), copy, false, myBaseLanguage); + } + + @Override + public PsiReference findReferenceAt(final int offset) { + final PsiFileImpl psiFile = (PsiFileImpl) getPsi(getBaseLanguage()); + return findReferenceAt(psiFile, offset); + } + + @Override + public PsiElement findElementAt(final int offset, final Language language) { + final PsiFile psiFile = getPsi(language); + return psiFile != null ? findElementAt(psiFile, offset) : null; + } + + @Override + @Nullable + public PsiReference findReferenceAt(final int offset, @NotNull final Language language) { + final PsiFile psiFile = getPsi(language); + return psiFile != null ? findReferenceAt(psiFile, offset) : null; + } + + @Nullable + private static PsiReference findReferenceAt(final PsiFile psiFile, final int offset) { + if (psiFile == null) return null; + int offsetInElement = offset; + PsiElement child = psiFile.getFirstChild(); + while (child != null) { + final int length = child.getTextLength(); + if (length <= offsetInElement) { + offsetInElement -= length; + child = child.getNextSibling(); + continue; + } + return child.findReferenceAt(offsetInElement); + } + return null; + } + + @Override + public PsiElement findElementAt(final int offset) { + return findElementAt(getPsi(getBaseLanguage()), offset); + } + + + @Override + public PsiElement findElementAt(int offset, Class lang) { + if (!ReflectionCache.isAssignable(lang, getBaseLanguage().getClass())) return null; + return findElementAt(offset); + } + + @Nullable + protected static PsiElement findElementAt(final PsiElement psiFile, final int offset) { + if (psiFile == null) return null; + int offsetInElement = offset; + PsiElement child = psiFile.getFirstChild(); + while (child != null) { + final int length = child.getTextLength(); + if (length <= offsetInElement) { + offsetInElement -= length; + child = child.getNextSibling(); + continue; + } + return child.findElementAt(offsetInElement); + } + return null; + } + + public void forceCachedPsi(final PsiFile psiFile) { + myPsiFile.set(psiFile); + ((PsiManagerEx) myManager).getFileManager().setViewProvider(getVirtualFile(), this); + } + + private Content getContent() { + return myContent; + } + + private void setContent(final Content content) { + myContent = content; + } + + private interface Content { + CharSequence getText(); + + long getModificationStamp(); + } + + private class VirtualFileContent implements Content { + @Override + public CharSequence getText() { + final VirtualFile virtualFile = getVirtualFile(); + if (virtualFile instanceof LightVirtualFile) { + Document doc = getCachedDocument(); + if (doc != null) return doc.getCharsSequence(); + return ((LightVirtualFile) virtualFile).getContent(); + } + + final Document document = getDocument(); + if (document == null) { + return LoadTextUtil.loadText(virtualFile); + } else { + return document.getCharsSequence(); + } + } + + @Override + public long getModificationStamp() { + return getVirtualFile().getModificationStamp(); + } + } + + private class DocumentContent implements Content { + @Override + public CharSequence getText() { + final Document document = getDocument(); + assert document != null; + return document.getCharsSequence(); + } + + @Override + public long getModificationStamp() { + Document document = myDocument == null ? null : myDocument.get(); + if (document != null) return document.getModificationStamp(); + return myVirtualFile.getModificationStamp(); + } + } + + private class PsiFileContent implements Content { + private final PsiFileImpl myFile; + private CharSequence myContent = null; + private final long myModificationStamp; + + private PsiFileContent(final PsiFileImpl file, final long modificationStamp) { + myFile = file; + myModificationStamp = modificationStamp; + } + + @Override + public CharSequence getText() { + if (!myFile.isContentsLoaded()) { + unsetPsiContent(); + return getContents(); + } + if (myContent != null) return myContent; + return myContent = ApplicationManager.getApplication().runReadAction(new Computable() { + public CharSequence compute() { + return myFile.calcTreeElement().getText(); + } + }); + } + + @Override + public long getModificationStamp() { + if (!myFile.isContentsLoaded()) { + unsetPsiContent(); + return SingleRootFileViewProvider.this.getModificationStamp(); + } + return myModificationStamp; + } + } +} diff --git a/idea_fix/src/com/intellij/psi/text/BlockSupport.java b/idea_fix/src/com/intellij/psi/text/BlockSupport.java new file mode 100644 index 00000000000..45a0be4fc74 --- /dev/null +++ b/idea_fix/src/com/intellij/psi/text/BlockSupport.java @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.intellij.psi.text; + +import com.intellij.lang.ASTNode; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.UserDataHolder; +import com.intellij.psi.PsiFile; +import com.intellij.psi.impl.source.text.DiffLog; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +public abstract class BlockSupport { + public static BlockSupport getInstance(Project project) { + return ServiceManager.getService(project, BlockSupport.class); + } + + public abstract void reparseRange(PsiFile file, int startOffset, int endOffset, @NonNls CharSequence newText) throws IncorrectOperationException; + + @NotNull + public abstract DiffLog reparseRange(@NotNull PsiFile file, + int startOffset, + int endOffset, + int lengthShift, + @NotNull CharSequence newText, + @NotNull ProgressIndicator progressIndicator) throws IncorrectOperationException; + + public static final Key DO_NOT_REPARSE_INCREMENTALLY = Key.create("DO_NOT_REPARSE_INCREMENTALLY"); + public static final Key TREE_TO_BE_REPARSED = Key.create("TREE_TO_BE_REPARSED"); + + public static class ReparsedSuccessfullyException extends RuntimeException { + private final DiffLog myDiffLog; + + public ReparsedSuccessfullyException(@NotNull DiffLog diffLog) { + myDiffLog = diffLog; + } + + @NotNull + public DiffLog getDiffLog() { + return myDiffLog; + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + } + + // maximal tree depth for which incremental reparse is allowed + // if tree is deeper then it will be replaced completely - to avoid SOEs +// public static final int INCREMENTAL_REPARSE_DEPTH_LIMIT = Registry.intValue("psi.incremental.reparse.depth.limit", 1000); + public static final int INCREMENTAL_REPARSE_DEPTH_LIMIT = 1000; + + public static final Key TREE_DEPTH_LIMIT_EXCEEDED = Key.create("TREE_IS_TOO_DEEP"); + + public static boolean isTooDeep(final UserDataHolder element) { + return element != null && Boolean.TRUE.equals(element.getUserData(TREE_DEPTH_LIMIT_EXCEEDED)); + } +} diff --git a/idea_fix/src/com/intellij/util/ReflectionUtil.java b/idea_fix/src/com/intellij/util/ReflectionUtil.java new file mode 100644 index 00000000000..cc9c72caafb --- /dev/null +++ b/idea_fix/src/com/intellij/util/ReflectionUtil.java @@ -0,0 +1,288 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.intellij.util; + +import com.intellij.openapi.diagnostic.Logger; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.*; +import java.util.ArrayList; +import java.util.Arrays; + +public class ReflectionUtil { + private static final Logger LOG = Logger.getInstance("#com.intellij.util.ReflectionUtil"); + + private ReflectionUtil() { + } + + @Nullable + public static Type resolveVariable(TypeVariable variable, final Class classType) { + return resolveVariable(variable, classType, true); + } + + @Nullable + public static Type resolveVariable(TypeVariable variable, final Class classType, boolean resolveInInterfacesOnly) { + final Class aClass = getRawType(classType); + int index = ArrayUtil.find(ReflectionCache.getTypeParameters(aClass), variable); + if (index >= 0) { + return variable; + } + + final Class[] classes = ReflectionCache.getInterfaces(aClass); + final Type[] genericInterfaces = ReflectionCache.getGenericInterfaces(aClass); + for (int i = 0; i <= classes.length; i++) { + Class anInterface; + if (i < classes.length) { + anInterface = classes[i]; + } else { + anInterface = ReflectionCache.getSuperClass(aClass); + if (resolveInInterfacesOnly || anInterface == null) { + continue; + } + } + final Type resolved = resolveVariable(variable, anInterface); + if (resolved instanceof Class || resolved instanceof ParameterizedType) { + return resolved; + } + if (resolved instanceof TypeVariable) { + final TypeVariable typeVariable = (TypeVariable) resolved; + index = ArrayUtil.find(ReflectionCache.getTypeParameters(anInterface), typeVariable); + if (index < 0) { + LOG.error("Cannot resolve type variable:\n" + "typeVariable = " + typeVariable + "\n" + "genericDeclaration = " + + declarationToString(typeVariable.getGenericDeclaration()) + "\n" + "searching in " + declarationToString(anInterface)); + } + final Type type = i < genericInterfaces.length ? genericInterfaces[i] : aClass.getGenericSuperclass(); + if (type instanceof Class) { + return Object.class; + } + if (type instanceof ParameterizedType) { + return getActualTypeArguments(((ParameterizedType) type))[index]; + } + throw new AssertionError("Invalid type: " + type); + } + } + return null; + } + + public static String declarationToString(final GenericDeclaration anInterface) { + return anInterface.toString() + + Arrays.asList(anInterface.getTypeParameters()) + + " loaded by " + ((Class) anInterface).getClassLoader(); + } + + public static Class getRawType(Type type) { + if (type instanceof Class) { + return (Class) type; + } + if (type instanceof ParameterizedType) { + return getRawType(((ParameterizedType) type).getRawType()); + } + if (type instanceof GenericArrayType) { + //todo[peter] don't create new instance each time + return Array.newInstance(getRawType(((GenericArrayType) type).getGenericComponentType()), 0).getClass(); + } + assert false : type; + return null; + } + + public static Type[] getActualTypeArguments(final ParameterizedType parameterizedType) { + return ReflectionCache.getActualTypeArguments(parameterizedType); + } + + @Nullable + public static Class substituteGenericType(final Type genericType, final Type classType) { + if (genericType instanceof TypeVariable) { + final Class aClass = getRawType(classType); + final Type type = resolveVariable((TypeVariable) genericType, aClass); + if (type instanceof Class) { + return (Class) type; + } + if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getRawType(); + } + if (type instanceof TypeVariable && classType instanceof ParameterizedType) { + final int index = ArrayUtil.find(ReflectionCache.getTypeParameters(aClass), type); + if (index >= 0) { + return getRawType(getActualTypeArguments(((ParameterizedType) classType))[index]); + } + } + } else { + return getRawType(genericType); + } + return null; + } + + public static ArrayList collectFields(Class clazz) { + ArrayList result = new ArrayList(); + collectFields(clazz, result); + return result; + } + + public static Field findField(Class clazz, @Nullable Class type, String name) throws NoSuchFieldException { + final ArrayList fields = collectFields(clazz); + for (Field each : fields) { + if (name.equals(each.getName()) && (type == null || each.getType().equals(type))) return each; + } + + throw new NoSuchFieldException("Class: " + clazz + " name: " + name + " type: " + type); + } + + public static Field findAssignableField(Class clazz, Class type, String name) throws NoSuchFieldException { + final ArrayList fields = collectFields(clazz); + for (Field each : fields) { + if (name.equals(each.getName()) && type.isAssignableFrom(each.getType())) return each; + } + + throw new NoSuchFieldException("Class: " + clazz + " name: " + name + " type: " + type); + } + + private static void collectFields(final Class clazz, final ArrayList result) { + final Field[] fields = clazz.getDeclaredFields(); + result.addAll(Arrays.asList(fields)); + final Class superClass = clazz.getSuperclass(); + if (superClass != null) { + collectFields(superClass, result); + } + final Class[] interfaces = clazz.getInterfaces(); + for (Class each : interfaces) { + collectFields(each, result); + } + } + + public static void resetField(Class clazz, Class type, String name) { + try { + resetField(null, findField(clazz, type, name)); + } catch (NoSuchFieldException e) { + LOG.info(e); + } + } + + public static void resetField(Object object, Class type, String name) { + try { + resetField(object, findField(object.getClass(), type, name)); + } catch (NoSuchFieldException e) { + LOG.info(e); + } + } + + public static void resetField(Object object, String name) { + try { + resetField(object, findField(object.getClass(), null, name)); + } catch (NoSuchFieldException e) { + LOG.info(e); + } + } + + public static void resetField(@Nullable final Object object, final Field field) { +// field.setAccessible(true); + Class type = field.getType(); + try { + if (type.isPrimitive()) { + if (boolean.class.equals(type)) { + field.set(object, Boolean.FALSE); + } else if (int.class.equals(type)) { + field.set(object, new Integer(0)); + } else if (double.class.equals(type)) { + field.set(object, new Double(0)); + } else if (float.class.equals(type)) { + field.set(object, new Float(0)); + } + } else { + field.set(object, null); + } + } catch (IllegalAccessException e) { + LOG.info(e); + } + } + + @Nullable + public static Method findMethod(Method[] methods, @NonNls @NotNull String name, Class... parameters) { + for (final Method method : methods) { + if (name.equals(method.getName()) && Arrays.equals(parameters, method.getParameterTypes())) return method; + } + return null; + } + + @Nullable + public static Method getMethod(@NotNull Class aClass, @NonNls @NotNull String name, Class... parameters) { + return findMethod(ReflectionCache.getMethods(aClass), name, parameters); + } + + @Nullable + public static Method getDeclaredMethod(@NotNull Class aClass, @NonNls @NotNull String name, Class... parameters) { + return findMethod(aClass.getDeclaredMethods(), name, parameters); + } + + public static Object getField(Class objectClass, Object object, Class type, @NonNls String name) { + try { + final Field field = findAssignableField(objectClass, type, name); +// field.setAccessible(true); + return field.get(object); + } catch (NoSuchFieldException e) { + LOG.debug(e); + return null; + } catch (IllegalAccessException e) { + LOG.debug(e); + return null; + } + } + + public static Type resolveVariableInHierarchy(final TypeVariable variable, final Class aClass) { + Type type; + Class current = aClass; + while ((type = resolveVariable(variable, current, false)) == null) { + current = ReflectionCache.getSuperClass(current); + if (current == null) { + return null; + } + } + if (type instanceof TypeVariable) { + return resolveVariableInHierarchy((TypeVariable) type, aClass); + } + return type; + } + + @NotNull + public static Constructor getDefaultConstructor(final Class aClass) { + try { + final Constructor constructor = aClass.getConstructor(); +// constructor.setAccessible(true); + return constructor; + } catch (NoSuchMethodException e) { + LOG.error("No default constructor in " + aClass, e); + return null; + } + } + + @NotNull + public static T createInstance(final Constructor constructor, final Object... args) { + try { + return constructor.newInstance(args); + } catch (InstantiationException e) { + LOG.error(e); + return null; + } catch (IllegalAccessException e) { + LOG.error(e); + return null; + } catch (InvocationTargetException e) { + LOG.error(e); + return null; + } + } +} diff --git a/js/js.iml b/js/js.iml index ce521ee9bd4..0110b0e9407 100644 --- a/js/js.iml +++ b/js/js.iml @@ -5,9 +5,9 @@ - + - + diff --git a/translator/src/org/jetbrains/k2js/K2JSTranslator.java b/translator/src/org/jetbrains/k2js/K2JSTranslator.java index 4fd6e7facf8..08511ce2e7d 100644 --- a/translator/src/org/jetbrains/k2js/K2JSTranslator.java +++ b/translator/src/org/jetbrains/k2js/K2JSTranslator.java @@ -11,6 +11,7 @@ import com.intellij.psi.impl.PsiFileFactoryImpl; import com.intellij.testFramework.LightVirtualFile; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.jet.JetCoreEnvironment; import org.jetbrains.jet.compiler.CompileEnvironment; import org.jetbrains.jet.lang.cfg.pseudocode.JetControlFlowDataTraceFactory; @@ -20,56 +21,74 @@ import org.jetbrains.jet.lang.resolve.BindingContext; import org.jetbrains.jet.plugin.JetLanguage; import org.jetbrains.k2js.generate.CodeGenerator; import org.jetbrains.k2js.translate.general.Translation; -import org.jetbrains.k2js.utils.JetTestUtils; +import org.jetbrains.k2js.utils.GenerationUtils; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import static org.jetbrains.k2js.translate.utils.BindingUtils.getNamespaceDescriptor; +import static org.jetbrains.k2js.translate.utils.DescriptorUtils.nameForNamespace; +import static org.jetbrains.k2js.utils.JetTestUtils.analyzeNamespace; /** * @author Talanov Pavel */ public final class K2JSTranslator { - private final JetCoreEnvironment myEnvironment = new JetCoreEnvironment(new Disposable() { + @NotNull + private final JetCoreEnvironment environment = new JetCoreEnvironment(new Disposable() { @Override public void dispose() { } }); + @Nullable + private BindingContext bindingContext = null; + public K2JSTranslator() { } public void translateFile(@NotNull String inputFile, @NotNull String outputFile) throws Exception { - JetFile PsiFile = loadPsiFile(inputFile); - + includeRtJar(); JsProgram program = generateProgram(PsiFile); - CodeGenerator generator = new CodeGenerator(); generator.generateToFile(program, new File(outputFile)); } @NotNull - public String translateString(@NotNull String programText) { - JetFile PsiFile = createPsiFile("test", programText); - JsProgram program = generateProgram(PsiFile); + public String translateStringWithCallToMain(@NotNull String programText, @NotNull String argumentsString) { + JetFile file = createPsiFile("test", programText); + String programCode = generateProgramCode(file); + String callToMain = generateCallToMain(file, argumentsString); + return programCode + callToMain; + } + private String generateProgramCode(JetFile psiFile) { + JsProgram program = generateProgram(psiFile); CodeGenerator generator = new CodeGenerator(); return generator.generateToString(program); } @NotNull private JsProgram generateProgram(@NotNull JetFile psiFile) { - final File rtJar = CompileEnvironment.findRtJar(true); - myEnvironment.addToClasspath(rtJar); JetNamespace namespace = psiFile.getRootNamespace(); - BindingContext bindingContext = JetTestUtils.analyzeNamespace(namespace, + bindingContext = analyzeNamespace(namespace, JetControlFlowDataTraceFactory.EMPTY); + assert bindingContext != null; - return Translation.generateAst(bindingContext, namespace, myEnvironment.getProject()); + return Translation.generateAst(bindingContext, namespace, environment.getProject()); + } + + private void includeRtJar() { + final File rtJar = CompileEnvironment.findRtJar(true); + environment.addToClasspath(rtJar); } @NotNull @@ -102,7 +121,34 @@ public final class K2JSTranslator { protected PsiFile createFile(@NonNls String name, String text) { LightVirtualFile virtualFile = new LightVirtualFile(name, JetLanguage.INSTANCE, text); virtualFile.setCharset(CharsetToolkit.UTF8_CHARSET); - return ((PsiFileFactoryImpl) PsiFileFactory.getInstance(myEnvironment.getProject())).trySetupPsiForFile(virtualFile, JetLanguage.INSTANCE, true, false); + return ((PsiFileFactoryImpl) PsiFileFactory.getInstance(environment.getProject())).trySetupPsiForFile(virtualFile, JetLanguage.INSTANCE, true, false); + } + + + @NotNull + public String generateCallToMain(@NotNull JetFile file, String argumentString) { + String namespaceName = getRootNamespaceName(file); + + List arguments = parseString(argumentString); + return GenerationUtils.generateCallToMain(namespaceName, arguments); + } + + @NotNull + private List parseString(@NotNull String argumentString) { + List result = new ArrayList(); + StringTokenizer stringTokenizer = new StringTokenizer(argumentString); + while (stringTokenizer.hasMoreTokens()) { + result.add(stringTokenizer.nextToken()); + } + return result; + } + + //TODO: make "anonymous" a constant + @NotNull + private String getRootNamespaceName(@NotNull JetFile psiFile) { + JetNamespace namespace = psiFile.getRootNamespace(); + assert bindingContext != null; + return nameForNamespace(getNamespaceDescriptor(bindingContext, namespace)); } } diff --git a/translator/src/org/jetbrains/k2js/K2JSTranslatorApplet.java b/translator/src/org/jetbrains/k2js/K2JSTranslatorApplet.java index 0c1ddc42e61..054ed799a25 100644 --- a/translator/src/org/jetbrains/k2js/K2JSTranslatorApplet.java +++ b/translator/src/org/jetbrains/k2js/K2JSTranslatorApplet.java @@ -10,10 +10,11 @@ import java.applet.Applet; public final class K2JSTranslatorApplet extends Applet { @NotNull - public String translate(@NotNull String code) { - String generatedCode = (new K2JSTranslator()).translateString(code); + public String translate(@NotNull String code, @NotNull String arguments) { + String generatedCode = (new K2JSTranslator()).translateStringWithCallToMain(code, arguments); System.out.println("GENERATED JAVASCRIPT CODE:\n-----------------------------------\n"); System.out.println(generatedCode); return generatedCode; } + } diff --git a/translator/src/org/jetbrains/k2js/translate/context/DeclarationVisitor.java b/translator/src/org/jetbrains/k2js/translate/context/DeclarationVisitor.java index 9b27453b82c..6e278bd41fe 100644 --- a/translator/src/org/jetbrains/k2js/translate/context/DeclarationVisitor.java +++ b/translator/src/org/jetbrains/k2js/translate/context/DeclarationVisitor.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.jet.lang.descriptors.*; import static org.jetbrains.k2js.translate.utils.DescriptorUtils.getOwnDeclarations; +import static org.jetbrains.k2js.translate.utils.DescriptorUtils.nameForNamespace; /** * @author Talanov Pavel @@ -123,15 +124,6 @@ public final class DeclarationVisitor extends DeclarationDescriptorVisitor arguments) { + String constructArguments = "var args = Kotlin.array(" + arguments.size() + ");\n"; + int index = 0; + for (String argument : arguments) { + constructArguments = constructArguments + "args.set(" + index + ", \"" + argument + "\");\n"; + index++; + } + String callMain = namespaceName + ".main(args);\n"; + return constructArguments + callMain; + } + + +} diff --git a/translator/test/org/jetbrains/k2js/test/AppletTest.java b/translator/test/org/jetbrains/k2js/test/AppletTest.java index a39e710acdf..fe22f73d939 100644 --- a/translator/test/org/jetbrains/k2js/test/AppletTest.java +++ b/translator/test/org/jetbrains/k2js/test/AppletTest.java @@ -17,6 +17,6 @@ public final class AppletTest extends TranslationTest { @Test public void simpleTest() throws Exception { - System.out.println((new K2JSTranslatorApplet()).translate("fun main(args : Array) {}")); + (new K2JSTranslatorApplet()).translate("fun main(args : Array) {}", " a 3 1 2134"); } } diff --git a/translator/test/org/jetbrains/k2js/test/RhinoSystemOutputChecker.java b/translator/test/org/jetbrains/k2js/test/RhinoSystemOutputChecker.java index 807a6f3c1a4..d1f165cd537 100644 --- a/translator/test/org/jetbrains/k2js/test/RhinoSystemOutputChecker.java +++ b/translator/test/org/jetbrains/k2js/test/RhinoSystemOutputChecker.java @@ -1,6 +1,7 @@ package org.jetbrains.k2js.test; import org.jetbrains.annotations.NotNull; +import org.jetbrains.k2js.utils.GenerationUtils; import org.mozilla.javascript.Context; import org.mozilla.javascript.Scriptable; @@ -36,18 +37,7 @@ public final class RhinoSystemOutputChecker implements RhinoResultChecker { } private void runMain(Context context, Scriptable scope) { - context.evaluateString(scope, execMain(), "function call", 0, null); - } - - @NotNull - private String execMain() { - String constructArguments = "var args = Kotlin.array(" + arguments.size() + ");\n"; - int index = 0; - for (String argument : arguments) { - constructArguments = constructArguments + "args.set(" + index + ", \"" + argument + "\");\n"; - index++; - } - String callMain = "Anonymous.main(args);\n"; - return constructArguments + callMain; + String callToMain = GenerationUtils.generateCallToMain("Anonymous", arguments); + context.evaluateString(scope, callToMain, "function call", 0, null); } } diff --git a/translator/testFiles/kotlin_lib.js b/translator/testFiles/kotlin_lib.js index 3df08528df9..18a5726345b 100644 --- a/translator/testFiles/kotlin_lib.js +++ b/translator/testFiles/kotlin_lib.js @@ -40,7 +40,7 @@ function $A(iterable) { extend(Object, { extend:extend, keys:Object.keys || keys, - values:values, + values:values }); })();