diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/source/PsiJavaFileBaseImpl.java b/java/java-psi-impl/src/com/intellij/psi/impl/source/PsiJavaFileBaseImpl.java index 241d47d0e2e3..cd5e45b9d56b 100644 --- a/java/java-psi-impl/src/com/intellij/psi/impl/source/PsiJavaFileBaseImpl.java +++ b/java/java-psi-impl/src/com/intellij/psi/impl/source/PsiJavaFileBaseImpl.java @@ -27,7 +27,9 @@ import com.intellij.psi.impl.java.stubs.PsiJavaFileStub; import com.intellij.psi.impl.source.resolve.ClassResolverProcessor; import com.intellij.psi.impl.source.resolve.SymbolCollectingProcessor; import com.intellij.psi.impl.source.resolve.SymbolCollectingProcessor.ResultWithContext; +import com.intellij.psi.impl.source.tree.FileElement; import com.intellij.psi.impl.source.tree.JavaElementType; +import com.intellij.psi.impl.source.tree.TreeUtil; import com.intellij.psi.scope.*; import com.intellij.psi.scope.processor.MethodsProcessor; import com.intellij.psi.stubs.StubElement; @@ -618,31 +620,36 @@ public abstract class PsiJavaFileBaseImpl extends PsiFileImpl implements PsiJava if (virtualFile == null) virtualFile = getViewProvider().getVirtualFile(); String sourceLevel = null; - try { - CharSequence contents = getViewProvider().getContents(); - int lineBound = Strings.indexOf(contents, "\n"); - CharSequence line = lineBound > 0 ? contents.subSequence(0, lineBound) : contents; - if (Strings.startsWith(line, 0,"#!")) { - List params = ParametersListUtil.parse(line.toString()); - int srcIdx = params.indexOf("--source"); - if (srcIdx > 0 && srcIdx + 1 < params.size()) { - sourceLevel = params.get(srcIdx + 1); - LanguageLevel sheBangLevel = LanguageLevel.parse(sourceLevel); - if (sheBangLevel != null) { - return sheBangLevel; + FileElement treeElement = getTreeElement(); + ASTNode firstLeaf = treeElement == null ? null : TreeUtil.findFirstLeaf(treeElement, false); + // optimization: do not (re)compute the whole file text on each PSI change - will lead to quadratic nightmare in case of many small changes otherwise + if (firstLeaf == null || Strings.startsWith(firstLeaf.getChars(), 0, "#!")) { + try { + CharSequence contents = getViewProvider().getContents(); + int lineBound = Strings.indexOf(contents, "\n"); + CharSequence line = lineBound > 0 ? contents.subSequence(0, lineBound) : contents; + if (Strings.startsWith(line, 0, "#!")) { + List params = ParametersListUtil.parse(line.toString()); + int srcIdx = params.indexOf("--source"); + if (srcIdx > 0 && srcIdx + 1 < params.size()) { + sourceLevel = params.get(srcIdx + 1); + LanguageLevel sheBangLevel = LanguageLevel.parse(sourceLevel); + if (sheBangLevel != null) { + return sheBangLevel; + } } } } - } - catch (Throwable ignored) { - } - finally { - if (!Objects.equals(sourceLevel, virtualFile.getUserData(SHEBANG_SOURCE_LEVEL)) && virtualFile.isInLocalFileSystem()) { - virtualFile.putUserData(SHEBANG_SOURCE_LEVEL, sourceLevel); - VirtualFile file = virtualFile; - ApplicationManager.getApplication().invokeLater(() -> FileContentUtilCore.reparseFiles(file), - ModalityState.nonModal(), - ApplicationManager.getApplication().getDisposed()); + catch (Throwable ignored) { + } + finally { + if (!Objects.equals(sourceLevel, virtualFile.getUserData(SHEBANG_SOURCE_LEVEL)) && virtualFile.isInLocalFileSystem()) { + virtualFile.putUserData(SHEBANG_SOURCE_LEVEL, sourceLevel); + VirtualFile file = virtualFile; + ApplicationManager.getApplication().invokeLater(() -> FileContentUtilCore.reparseFiles(file), + ModalityState.nonModal(), + ApplicationManager.getApplication().getDisposed()); + } } } diff --git a/java/java-tests/testSrc/com/intellij/psi/impl/PsiModificationStressTest.java b/java/java-tests/testSrc/com/intellij/psi/impl/PsiModificationStressTest.java new file mode 100644 index 000000000000..5bae27259c67 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/psi/impl/PsiModificationStressTest.java @@ -0,0 +1,24 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.psi.impl; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElementFactory; +import com.intellij.psi.PsiField; +import com.intellij.testFramework.LightJavaCodeInsightTestCase; + +public class PsiModificationStressTest extends LightJavaCodeInsightTestCase { + public void testManySmallPSIChangesDoNotCauseQuadraticRecomputationsOfWholeFileText() { + int N = 100_000; + String text = " int field;\n".repeat(N); + + PsiClass aClass = PsiElementFactory.getInstance(getProject()).createClassFromText(text, null); + assertFalse(aClass.isPhysical()); + PsiField firstField = aClass.getFields()[0]; + // would pass only if each PSI change does not cause recomputation of the whole file text in com.intellij.psi.impl.source.PsiJavaFileBaseImpl.getLanguageLevelInner + for (int i = 0; i < N; i++) { + firstField.setName("f" + i); + } + assertFalse(aClass.isPhysical()); + assertEquals(N, aClass.getFields().length); + } +}