introduce VirtualFileGist and PsiFileGist
authorpeter <peter@jetbrains.com>
Mon, 7 Nov 2016 07:41:20 +0000 (08:41 +0100)
committerpeter <peter@jetbrains.com>
Mon, 7 Nov 2016 10:10:57 +0000 (11:10 +0100)
java/java-tests/testSrc/com/intellij/util/gist/FileGistTest.groovy [new file with mode: 0644]
platform/core-impl/src/com/intellij/util/indexing/FileContentImpl.java
platform/indexing-api/src/com/intellij/util/gist/GistManager.java [new file with mode: 0644]
platform/indexing-api/src/com/intellij/util/gist/PsiFileGist.java [new file with mode: 0644]
platform/indexing-api/src/com/intellij/util/gist/VirtualFileGist.java [new file with mode: 0644]
platform/lang-impl/src/com/intellij/util/gist/GistManagerImpl.java [new file with mode: 0644]
platform/lang-impl/src/com/intellij/util/gist/PsiFileGistImpl.java [new file with mode: 0644]
platform/lang-impl/src/com/intellij/util/gist/VirtualFileGistImpl.java [new file with mode: 0644]
platform/lang-impl/src/com/intellij/util/indexing/FileBasedIndexImpl.java
platform/platform-resources/src/componentSets/Lang.xml

diff --git a/java/java-tests/testSrc/com/intellij/util/gist/FileGistTest.groovy b/java/java-tests/testSrc/com/intellij/util/gist/FileGistTest.groovy
new file mode 100644 (file)
index 0000000..a1c67d0
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2000-2016 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.gist
+
+import com.intellij.openapi.application.WriteAction
+import com.intellij.openapi.fileEditor.impl.LoadTextUtil
+import com.intellij.openapi.fileTypes.PlainTextFileType
+import com.intellij.openapi.project.ProjectManager
+import com.intellij.openapi.roots.impl.PushedFilePropertiesUpdater
+import com.intellij.openapi.vfs.VfsUtil
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.psi.JavaPsiFacade
+import com.intellij.psi.PsiDocumentManager
+import com.intellij.psi.PsiFileFactory
+import com.intellij.psi.impl.source.PsiFileImpl
+import com.intellij.psi.search.GlobalSearchScope
+import com.intellij.testFramework.LightVirtualFile
+import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
+import com.intellij.util.FileContentUtilCore
+import com.intellij.util.GCUtil
+import com.intellij.util.io.EnumeratorIntegerDescriptor
+import com.intellij.util.io.EnumeratorStringDescriptor
+
+/**
+ * @author peter
+ */
+class FileGistTest extends LightCodeInsightFixtureTestCase {
+
+  void "test get data"() {
+    def gist = take3Gist()
+    assert 'foo' == gist.getFileData(project, addFooBarFile())
+    assert 'bar' == gist.getFileData(project, myFixture.addFileToProject('b.txt', 'bar foo').virtualFile)
+  }
+
+  private VirtualFile addFooBarFile() {
+    return myFixture.addFileToProject('a.txt', 'foo bar').virtualFile
+  }
+
+  private VirtualFileGist<String> take3Gist() {
+    return GistManager.instance.newVirtualFileGist(getTestName(true), 0, EnumeratorStringDescriptor.INSTANCE, { p, f -> LoadTextUtil.loadText(f).toString().substring(0, 3) })
+  }
+
+  void "test data is cached per file"() {
+    VirtualFileGist<Integer> gist = countingVfsGist()
+
+    def file = addFooBarFile()
+    assert gist.getFileData(project, file) == 1
+    assert gist.getFileData(project, file) == 1
+
+    assert gist.getFileData(project, myFixture.addFileToProject('b.txt', '').virtualFile) == 2
+  }
+
+  private VirtualFileGist<Integer> countingVfsGist() {
+    return GistManager.instance.newVirtualFileGist(getTestName(true), 0, EnumeratorIntegerDescriptor.INSTANCE, countingCalculator())
+  }
+
+  void "test data is recalculated on file change"() {
+    VirtualFileGist<Integer> gist = countingVfsGist()
+    def file = addFooBarFile()
+    assert gist.getFileData(project, file) == 1
+
+    WriteAction.run { VfsUtil.saveText(file, 'x') }
+    assert gist.getFileData(project, file) == 2
+
+    FileContentUtilCore.reparseFiles(file)
+    assert gist.getFileData(project, file) == 3
+
+    PushedFilePropertiesUpdater.getInstance(project).filePropertiesChanged(file)
+    assert gist.getFileData(project, file) == 4
+  }
+
+  void "test data is not recalculated on another file change"() {
+    VirtualFileGist<Integer> gist = countingVfsGist()
+    def file1 = addFooBarFile()
+    def file2 = myFixture.addFileToProject('b.txt', 'foo bar').virtualFile
+    assert gist.getFileData(project, file1) == 1
+    assert gist.getFileData(project, file2) == 2
+
+    WriteAction.run { VfsUtil.saveText(file1, 'x') }
+    assert gist.getFileData(project, file2) == 2
+  }
+
+  void "test vfs gist works for light files"() {
+    assert 'goo' == take3Gist().getFileData(project, new LightVirtualFile('a.txt', 'goo goo'))
+  }
+
+  void "test different data for different projects"() {
+    int invocations = 0
+    VirtualFileGist<String> gist = GistManager.instance.newVirtualFileGist(getTestName(true), 0, EnumeratorStringDescriptor.INSTANCE, { p, f -> "$p.name ${++invocations}" as String })
+    def file = addFooBarFile()
+
+    assert "$project.name 1" == gist.getFileData(project, file)
+    assert "$ProjectManager.instance.defaultProject.name 2" == gist.getFileData(ProjectManager.instance.defaultProject, file)
+    assert "$project.name 1" == gist.getFileData(project, file)
+  }
+
+  void "test cannot register twice"() {
+    take3Gist()
+    try {
+      take3Gist()
+      fail()
+    }
+    catch (IllegalArgumentException ignore) {
+    }
+  }
+
+  private static VirtualFileGist.GistCalculator<Integer> countingCalculator() {
+    int invocations = 0
+    return { p, f -> ++invocations } as VirtualFileGist.GistCalculator<Integer>
+  }
+
+  void "test null data"() {
+    int invocations = 0
+    VirtualFileGist<String> gist = GistManager.instance.newVirtualFileGist(getTestName(true), 0, EnumeratorStringDescriptor.INSTANCE, { p, f -> invocations++; return null as String })
+    def file = addFooBarFile()
+    assert null == gist.getFileData(project, file)
+    assert null == gist.getFileData(project, file)
+    assert invocations == 1
+  }
+
+  void "test psi gist uses last committed document content"() {
+    def file = myFixture.addFileToProject("a.txt", "foo bar")
+    def gist = GistManager.instance.newPsiFileGist(getTestName(true), 0, EnumeratorStringDescriptor.INSTANCE, { it.text.substring(0, 3) })
+    assert 'foo' == gist.getFileData(file)
+
+    WriteAction.run {
+      VfsUtil.saveText(file.virtualFile, 'bar foo')
+      assert file.valid
+      assert PsiDocumentManager.getInstance(project).isUncommited(file.viewProvider.document)
+      assert 'foo' == gist.getFileData(file)
+    }
+
+    PsiDocumentManager.getInstance(project).commitAllDocuments()
+    assert 'bar' == gist.getFileData(file)
+  }
+
+
+  void "test psi gist does not load AST"() {
+    PsiFileImpl file = myFixture.addFileToProject("a.java", "package bar;") as PsiFileImpl
+    assert !file.contentsLoaded
+
+    assert GistManager.instance.newPsiFileGist(getTestName(true), 0, EnumeratorStringDescriptor.INSTANCE, { it.findElementAt(0).text }).getFileData(file) == 'package'
+    assert !file.contentsLoaded
+  }
+
+  void "test psi gist works for binary files"() {
+    def objectClass = JavaPsiFacade.getInstance(project).findClass(Object.name, GlobalSearchScope.allScope(project)).containingFile
+
+    assert GistManager.instance.newPsiFileGist(getTestName(true), 0, EnumeratorStringDescriptor.INSTANCE, { it.name }).getFileData(objectClass) == 'Object.class'
+  }
+
+  void "test psi gist does not load document"() {
+    PsiFileGist<Integer> gist = countingPsiGist()
+    def file = myFixture.addFileToProject('a.xtt', 'foo')
+    assert gist.getFileData(file) == 1
+
+    GCUtil.tryGcSoftlyReachableObjects()
+    assert !PsiDocumentManager.getInstance(project).getCachedDocument(file)
+
+    assert gist.getFileData(file) == 1
+    assert !PsiDocumentManager.getInstance(project).getCachedDocument(file)
+  }
+
+  private PsiFileGist<Integer> countingPsiGist() {
+    int invocations = 0
+    return GistManager.instance.newPsiFileGist(getTestName(true) + ' psi', 0, EnumeratorIntegerDescriptor.INSTANCE, { f -> ++invocations })
+  }
+
+  void "test invalidateData works for non-physical files"() {
+    def psiFile = PsiFileFactory.getInstance(project).createFileFromText('a.txt', PlainTextFileType.INSTANCE, 'foo bar')
+    def vFile = psiFile.viewProvider.virtualFile
+    def vfsGist = countingVfsGist()
+    def psiGist = countingPsiGist()
+
+    assert 1 == vfsGist.getFileData(project, vFile)
+    assert 1 == psiGist.getFileData(psiFile)
+
+    ((GistManagerImpl)GistManager.instance).invalidateData()
+    assert 2 == vfsGist.getFileData(project, vFile)
+    assert 2 == psiGist.getFileData(psiFile)
+  }
+
+  void "test data is recalculated when ancestor directory changes"() {
+    def gist = countingVfsGist()
+    def file = myFixture.addFileToProject('foo/bar/a.txt', '').virtualFile
+    assert 1 == gist.getFileData(project, file)
+
+    WriteAction.run { file.parent.parent.rename(this, 'goo') }
+    assert 2 == gist.getFileData(project, file)
+  }
+
+}
index 35d06318f88f3efc109ce13ba1a375eb48493069..d1af65160e4e8f6bf95ac1f3e100e5f87ec7a00f 100644 (file)
@@ -107,10 +107,15 @@ public class FileContentImpl extends UserDataHolderBase implements FileContent {
     if (project == null) {
       project = DefaultProjectFactory.getInstance().getDefaultProject();
     }
-    final Language language = ((LanguageFileType)getFileTypeWithoutSubstitution()).getLanguage();
-    final VirtualFile file = getFile();
+    return createFileFromText(project, text, (LanguageFileType)getFileTypeWithoutSubstitution(), myFile, myFileName);
+  }
+
+  @NotNull
+  public static PsiFile createFileFromText(@NotNull Project project, @NotNull CharSequence text, @NotNull LanguageFileType fileType,
+                                           @NotNull VirtualFile file, @NotNull String fileName) {
+    final Language language = fileType.getLanguage();
     final Language substitutedLanguage = LanguageSubstitutors.INSTANCE.substituteLanguage(language, file, project);
-    return PsiFileFactory.getInstance(project).createFileFromText(getFileName(), substitutedLanguage, text, false, false, true, file);
+    return PsiFileFactory.getInstance(project).createFileFromText(fileName, substitutedLanguage, text, false, false, true, file);
   }
 
   public static class IllegalDataException extends RuntimeException {
diff --git a/platform/indexing-api/src/com/intellij/util/gist/GistManager.java b/platform/indexing-api/src/com/intellij/util/gist/GistManager.java
new file mode 100644 (file)
index 0000000..78198ae
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2016 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.gist;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.NullableFunction;
+import com.intellij.util.io.DataExternalizer;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A helper class for working with file gists: associating persistent data with current VFS or PSI file contents.
+ *
+ * @since 171.*
+ * @author peter
+ */
+public abstract class GistManager {
+
+  @NotNull
+  public static GistManager getInstance() {
+    return ApplicationManager.getApplication().getComponent(GistManager.class);
+  }
+
+  /**
+   * Create a new {@link VirtualFileGist}.
+   * @param id a unique identifier of this data
+   * @param version should be incremented each time the {@code externalizer} or {@code calcData} logic changes.
+   * @param externalizer used to store the data to the disk and retrieve it
+   * @param calcData calculates the data by the file content when needed
+   * @param <Data> the type of the data to cache
+   * @return the gist object, where {@link VirtualFileGist#getFileData} can later be used to retrieve the cached data
+   */
+  @NotNull
+  public abstract <Data> VirtualFileGist<Data> newVirtualFileGist(@NotNull String id,
+                                                                  int version,
+                                                                  @NotNull DataExternalizer<Data> externalizer,
+                                                                  @NotNull VirtualFileGist.GistCalculator<Data> calcData);
+
+  /**
+   * Create a new {@link PsiFileGist}.
+   * @param id a unique identifier of this data
+   * @param version should be incremented each time the {@code externalizer} or {@code calcData} logic changes.
+   * @param externalizer used to store the data to the disk and retrieve it
+   * @param calcData calculates the data by the file content when needed
+   * @param <Data> the type of the data to cache
+   * @return the gist object, where {@link PsiFileGist#getFileData} can later be used to retrieve the cached data
+   */
+  @NotNull
+  public abstract <Data> PsiFileGist<Data> newPsiFileGist(@NotNull String id,
+                                                          int version,
+                                                          @NotNull DataExternalizer<Data> externalizer,
+                                                          @NotNull NullableFunction<PsiFile, Data> calcData);
+
+}
diff --git a/platform/indexing-api/src/com/intellij/util/gist/PsiFileGist.java b/platform/indexing-api/src/com/intellij/util/gist/PsiFileGist.java
new file mode 100644 (file)
index 0000000..b2f172f
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2016 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.gist;
+
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Calculates some data based on {@link PsiFile} content, stores it persistently and updates it when the content is changed. The data is calculated lazily, when needed.<p/>
+ *
+ * Obtained using {@link GistManager#newPsiFileGist}.<p/>
+ *
+ * The difference to {@link VirtualFileGist} is that PSI content is used here. So if an uncommitted document is saved onto disk,
+ * this class will use the last committed content of the PSI file, while {@link VirtualFileGist} will use the saved virtual file content.
+ *
+ * @since 171.*
+ * @author peter
+ */
+public interface PsiFileGist<Data> {
+
+  /**
+   * Calculate or get the cached data by the current PSI content.
+   */
+  @Nullable
+  Data getFileData(@NotNull PsiFile file);
+}
diff --git a/platform/indexing-api/src/com/intellij/util/gist/VirtualFileGist.java b/platform/indexing-api/src/com/intellij/util/gist/VirtualFileGist.java
new file mode 100644 (file)
index 0000000..34adc86
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2000-2016 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.gist;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Calculates some data based on {@link VirtualFile} content, stores that data persistently and updates it when the content is changed. The data is calculated lazily, when needed, and can be different for different projects.<p/>
+ *
+ * Obtained using {@link GistManager#newVirtualFileGist}.<p/>
+ *
+ * Tracks VFS content only. Unsaved/uncommitted documents have no effect on the {@link #getFileData} results.
+ * Neither do any disk file changes, until VFS refresh has detected them.
+ *
+ * @see PsiFileGist
+ * @since 171.*
+ * @author peter
+ */
+public interface VirtualFileGist<Data> {
+
+  /**
+   * Calculate or get the cached data by the current virtual file content in the given project.
+   */
+  @Nullable
+  Data getFileData(@NotNull Project project, @NotNull VirtualFile file);
+
+  /**
+   * Used by {@link VirtualFileGist} to calculate the data when it's needed and to recalculate it after file changes.
+   */
+  @FunctionalInterface
+  interface GistCalculator<Data> {
+
+    @Nullable
+    Data calcData(@NotNull Project project, @NotNull VirtualFile file);
+  }
+}
diff --git a/platform/lang-impl/src/com/intellij/util/gist/GistManagerImpl.java b/platform/lang-impl/src/com/intellij/util/gist/GistManagerImpl.java
new file mode 100644 (file)
index 0000000..682c1bc
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2000-2016 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.gist;
+
+import com.intellij.ide.util.PropertiesComponent;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.newvfs.BulkFileListener;
+import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
+import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.NullableFunction;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.io.DataExternalizer;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @author peter
+ */
+public class GistManagerImpl extends GistManager {
+  private static final Set<String> ourKnownIds = ContainerUtil.newConcurrentSet();
+  private static final String ourPropertyName = "file.gist.reindex.count";
+  private final AtomicInteger myReindexCount = new AtomicInteger(PropertiesComponent.getInstance().getInt(ourPropertyName, 0));
+
+  public GistManagerImpl() {
+    ApplicationManager.getApplication().getMessageBus().connect().subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener.Adapter() {
+      @Override
+      public void after(@NotNull List<? extends VFileEvent> events) {
+        if (events.stream().anyMatch(this::shouldDropCache)) {
+          invalidateData();
+        }
+      }
+
+      private boolean shouldDropCache(VFileEvent e) {
+        if (!(e instanceof VFilePropertyChangeEvent)) return false;
+
+        String propertyName = ((VFilePropertyChangeEvent)e).getPropertyName();
+        return propertyName.equals(VirtualFile.PROP_NAME) || propertyName.equals(VirtualFile.PROP_ENCODING);
+      }
+    });
+  }
+
+  @NotNull
+  @Override
+  public <Data> VirtualFileGist<Data> newVirtualFileGist(@NotNull String id,
+                                                         int version,
+                                                         @NotNull DataExternalizer<Data> externalizer,
+                                                         @NotNull VirtualFileGist.GistCalculator<Data> calcData) {
+    if (!ourKnownIds.add(id)) {
+      throw new IllegalArgumentException("Gist '" + id + "' is already registered");
+    }
+
+    return new VirtualFileGistImpl<>(id, version, externalizer, calcData);
+  }
+
+  @NotNull
+  @Override
+  public <Data> PsiFileGist<Data> newPsiFileGist(@NotNull String id,
+                                                 int version,
+                                                 @NotNull DataExternalizer<Data> externalizer,
+                                                 @NotNull NullableFunction<PsiFile, Data> calculator) {
+    return new PsiFileGistImpl<>(id, version, externalizer, calculator);
+  }
+
+  int getReindexCount() {
+    return myReindexCount.get();
+  }
+
+  public void invalidateData() {
+    // Clear all cache at once to simplify and speedup this operation.
+    // It can be made per-file if cache recalculation ever becomes an issue.
+    PropertiesComponent.getInstance().setValue(ourPropertyName, myReindexCount.incrementAndGet(), 0);
+  }
+}
diff --git a/platform/lang-impl/src/com/intellij/util/gist/PsiFileGistImpl.java b/platform/lang-impl/src/com/intellij/util/gist/PsiFileGistImpl.java
new file mode 100644 (file)
index 0000000..0dfdf65
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2000-2016 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.gist;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+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.ModificationTracker;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.impl.source.PsiFileImpl;
+import com.intellij.psi.util.CachedValue;
+import com.intellij.psi.util.CachedValueProvider;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.util.NullableFunction;
+import com.intellij.util.indexing.FileContentImpl;
+import com.intellij.util.io.DataExternalizer;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author peter
+ */
+class PsiFileGistImpl<Data> implements PsiFileGist<Data> {
+  private static final ModificationTracker ourReindexTracker = () -> ((GistManagerImpl)GistManager.getInstance()).getReindexCount();
+  private final VirtualFileGist<Data> myPersistence;
+  private final VirtualFileGist.GistCalculator<Data> myCalculator;
+  private final Key<CachedValue<Data>> myCacheKey;
+
+  PsiFileGistImpl(@NotNull String id,
+                  int version,
+                  @NotNull DataExternalizer<Data> externalizer,
+                  @NotNull NullableFunction<PsiFile, Data> calculator) {
+    myCalculator = (project, file) -> {
+      PsiFile psiFile = getPsiFile(project, file);
+      return psiFile == null ? null : calculator.fun(psiFile);
+    };
+    myPersistence = GistManager.getInstance().newVirtualFileGist(id, version, externalizer, myCalculator);
+    myCacheKey = Key.create("PsiFileGist " + id);
+  }
+
+  @Override
+  @Nullable
+  public Data getFileData(@NotNull PsiFile file) {
+    ApplicationManager.getApplication().assertReadAccessAllowed();
+
+    if (shouldUseMemoryStorage(file)) {
+      return CachedValuesManager.getManager(file.getProject()).getCachedValue(
+        file, myCacheKey, () -> {
+          Data data = myCalculator.calcData(file.getProject(), file.getViewProvider().getVirtualFile());
+          return CachedValueProvider.Result.create(data, file, ourReindexTracker);
+        }, false);
+    }
+
+    file.putUserData(myCacheKey, null);
+    return myPersistence.getFileData(file.getProject(), file.getVirtualFile());
+  }
+
+  private static boolean shouldUseMemoryStorage(PsiFile file) {
+    if (!(file.getVirtualFile() instanceof NewVirtualFile)) return true;
+
+    PsiDocumentManager pdm = PsiDocumentManager.getInstance(file.getProject());
+    Document document = pdm.getCachedDocument(file);
+    return document != null && (pdm.isUncommited(document) || FileDocumentManager.getInstance().isDocumentUnsaved(document));
+  }
+
+  private static PsiFile getPsiFile(@NotNull Project project, @NotNull VirtualFile file) {
+    PsiFile psi = PsiManager.getInstance(project).findFile(file);
+    if (psi == null || !(psi instanceof PsiFileImpl) || ((PsiFileImpl)psi).isContentsLoaded()) {
+      return psi;
+    }
+
+    FileType fileType = file.getFileType();
+    if (!(fileType instanceof LanguageFileType)) return null;
+
+    return FileContentImpl.createFileFromText(project, psi.getViewProvider().getContents(), (LanguageFileType)fileType, file, file.getName());
+  }
+
+}
diff --git a/platform/lang-impl/src/com/intellij/util/gist/VirtualFileGistImpl.java b/platform/lang-impl/src/com/intellij/util/gist/VirtualFileGistImpl.java
new file mode 100644 (file)
index 0000000..684669a
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2000-2016 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.gist;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileWithId;
+import com.intellij.openapi.vfs.newvfs.FileAttribute;
+import com.intellij.util.containers.FactoryMap;
+import com.intellij.util.io.DataExternalizer;
+import com.intellij.util.io.DataInputOutputUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * @author peter
+ */
+class VirtualFileGistImpl<Data>implements VirtualFileGist<Data> {
+  private static final Logger LOG = Logger.getInstance("#com.intellij.util.gist.VirtualFileGist");
+
+  @NotNull private final String myId;
+  private final int myVersion;
+  @NotNull private final GistCalculator<Data> myCalculator;
+  @NotNull private final DataExternalizer<Data> myExternalizer;
+
+  VirtualFileGistImpl(@NotNull String id, int version, @NotNull DataExternalizer<Data> externalizer, @NotNull GistCalculator<Data> calcData) {
+    myId = id;
+    myVersion = version;
+    myExternalizer = externalizer;
+    myCalculator = calcData;
+  }
+
+  @Override@Nullable
+  public Data getFileData(@NotNull Project project, @NotNull VirtualFile file) {
+    ApplicationManager.getApplication().assertReadAccessAllowed();
+
+    if (!(file instanceof VirtualFileWithId)) return myCalculator.calcData(project, file);
+
+    long stamp = file.getTimeStamp() + ((GistManagerImpl)GistManager.getInstance()).getReindexCount();
+
+    try (DataInputStream stream = getFileAttribute(project).readAttribute(file)) {
+      if (stream != null && DataInputOutputUtil.readLONG(stream) == stamp) {
+        return stream.readBoolean() ? myExternalizer.read(stream) : null;
+      }
+    }
+    catch (IOException e) {
+      LOG.error(e);
+    }
+
+    Data result = myCalculator.calcData(project, file);
+    cacheResult(stamp, result, project, file);
+    return result;
+  }
+
+  private void cacheResult(long modCount, @Nullable Data result, Project project, VirtualFile file) {
+    try (DataOutputStream out = getFileAttribute(project).writeAttribute(file)) {
+      DataInputOutputUtil.writeLONG(out, modCount);
+      out.writeBoolean(result != null);
+      if (result != null) {
+        myExternalizer.save(out, result);
+      }
+    }
+    catch (IOException e) {
+      LOG.error(e);
+    }
+  }
+
+  @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
+  private static final Map<Pair<String, Integer>, FileAttribute> ourAttributes = new FactoryMap<Pair<String, Integer>, FileAttribute>() {
+    @Nullable
+    @Override
+    protected FileAttribute create(Pair<String, Integer> key) {
+      return new FileAttribute(key.first, key.second, false);
+    }
+  };
+
+  private FileAttribute getFileAttribute(Project project) {
+    synchronized (ourAttributes) {
+      return ourAttributes.get(Pair.create(myId + project.getLocationHash(), myVersion));
+    }
+  }
+
+}
+
index a3b63dbb8cfdb1e50a24a7ba40e04084267edbd1..e3388b013c490c67d8c33ee8e0c9fbac2c1caa87 100644 (file)
@@ -73,6 +73,8 @@ import com.intellij.util.*;
 import com.intellij.util.containers.ConcurrentIntObjectMap;
 import com.intellij.util.containers.ContainerUtil;
 import com.intellij.util.containers.JBIterable;
+import com.intellij.util.gist.GistManager;
+import com.intellij.util.gist.GistManagerImpl;
 import com.intellij.util.indexing.containers.TroveSetIntIterator;
 import com.intellij.util.io.DataOutputStream;
 import com.intellij.util.io.IOUtil;
@@ -283,6 +285,7 @@ public class FileBasedIndexImpl extends FileBasedIndex {
 
   @Override
   public void requestReindex(@NotNull final VirtualFile file) {
+    ((GistManagerImpl)GistManager.getInstance()).invalidateData();
     myChangedFilesCollector.invalidateIndicesRecursively(file, true);
   }
 
index d5f412f722ab383d7828ebf0b32d405df6cb415a..554b39486f1d69e6cd04c5db6c15d5a9431218ab 100644 (file)
       <interface-class>com.intellij.util.indexing.FileBasedIndex</interface-class>
       <implementation-class>com.intellij.util.indexing.FileBasedIndexImpl</implementation-class>
     </component>
+    <component>
+      <interface-class>com.intellij.util.gist.GistManager</interface-class>
+      <implementation-class>com.intellij.util.gist.GistManagerImpl</implementation-class>
+    </component>
     <component>
       <interface-class>com.intellij.psi.stubs.StubIndex</interface-class>
       <implementation-class>com.intellij.psi.stubs.StubIndexImpl</implementation-class>