diff --git a/.idea/modules.xml b/.idea/modules.xml
index eeb06f8ef1fb..5f4893961f75 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -457,6 +457,7 @@
+
diff --git a/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy b/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy
index 69cf4fc2a0c0..9fb47d3a9c68 100644
--- a/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy
+++ b/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy
@@ -64,6 +64,7 @@ abstract class BaseIdeaProperties extends JetBrainsProductProperties {
"intellij.statsCollector",
"intellij.sh",
"intellij.vcs.changeReminder",
+ "intellij.filePrediction",
"intellij.markdown",
"intellij.laf.macos",
"intellij.laf.win10"
diff --git a/intellij.idea.community.main.iml b/intellij.idea.community.main.iml
index 37f4b11f52fb..9054e8eafb12 100644
--- a/intellij.idea.community.main.iml
+++ b/intellij.idea.community.main.iml
@@ -137,5 +137,6 @@
+
\ No newline at end of file
diff --git a/platform/statistics/src/com/intellij/internal/statistic/eventLog/FeatureUsageData.kt b/platform/statistics/src/com/intellij/internal/statistic/eventLog/FeatureUsageData.kt
index c8f0610d28a7..8269d73b6394 100644
--- a/platform/statistics/src/com/intellij/internal/statistic/eventLog/FeatureUsageData.kt
+++ b/platform/statistics/src/com/intellij/internal/statistic/eventLog/FeatureUsageData.kt
@@ -201,6 +201,12 @@ class FeatureUsageData {
}
@FeatureUsageDataBuilder(additionalDataFields = ["value::0"])
+ fun addAnonymizedValue(@NonNls key: String, @NonNls value: String?): FeatureUsageData {
+ data[key] = value?.let { EventLogConfiguration.anonymize(value) } ?: "undefined"
+ return this
+ }
+
+ @FeatureUsageDataBuilder(additionalDataFields = ["value"])
fun addValue(value: Any): FeatureUsageData {
if (value is String || value is Boolean || value is Int || value is Long || value is Float || value is Double) {
return addDataInternal("value", value)
diff --git a/plugins/filePrediction/intellij.filePrediction.iml b/plugins/filePrediction/intellij.filePrediction.iml
new file mode 100644
index 000000000000..3d086fbfcfd7
--- /dev/null
+++ b/plugins/filePrediction/intellij.filePrediction.iml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/filePrediction/resources/META-INF/file-prediction-vcs.xml b/plugins/filePrediction/resources/META-INF/file-prediction-vcs.xml
new file mode 100644
index 000000000000..7061cbe63568
--- /dev/null
+++ b/plugins/filePrediction/resources/META-INF/file-prediction-vcs.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/plugins/filePrediction/resources/META-INF/plugin.xml b/plugins/filePrediction/resources/META-INF/plugin.xml
new file mode 100644
index 000000000000..0ab995d5dc5d
--- /dev/null
+++ b/plugins/filePrediction/resources/META-INF/plugin.xml
@@ -0,0 +1,27 @@
+
+ com.jetbrains.filePrediction
+ Next File Prediction
+ JetBrains
+
+
+
+ com.intellij.modules.vcs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionEditorManagerListener.kt b/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionEditorManagerListener.kt
new file mode 100644
index 000000000000..5bcb0ac82aa5
--- /dev/null
+++ b/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionEditorManagerListener.kt
@@ -0,0 +1,11 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.filePrediction
+
+import com.intellij.openapi.fileEditor.FileEditorManagerEvent
+import com.intellij.openapi.fileEditor.FileEditorManagerListener
+
+class FilePredictionEditorManagerListener : FileEditorManagerListener {
+ override fun selectionChanged(event: FileEditorManagerEvent) {
+ event.newFile?.let { FileUsagePredictor.onFileOpened(event.manager.project, it, event.oldFile) }
+ }
+}
\ No newline at end of file
diff --git a/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionFeature.kt b/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionFeature.kt
new file mode 100644
index 000000000000..f37322eabbb9
--- /dev/null
+++ b/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionFeature.kt
@@ -0,0 +1,59 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.filePrediction
+
+import com.intellij.internal.statistic.eventLog.FeatureUsageData
+
+sealed class FilePredictionFeature {
+ companion object {
+ @JvmStatic
+ fun binary(value: Boolean): FilePredictionFeature = if (value) BinaryValue.TRUE else BinaryValue.FALSE
+
+ @JvmStatic
+ fun numerical(value: Int): FilePredictionFeature = NumericalValue(value)
+
+ @JvmStatic
+ fun numerical(value: Double): FilePredictionFeature = DoubleValue(value)
+ }
+
+ protected abstract val value: Any
+
+ abstract fun addToEventData(key: String, data: FeatureUsageData)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as FilePredictionFeature
+
+ if (value != other.value) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ override fun toString(): String = value.toString()
+
+ private class BinaryValue private constructor(override val value: Boolean) : FilePredictionFeature() {
+ companion object {
+ val TRUE = BinaryValue(true)
+ val FALSE = BinaryValue(false)
+ }
+
+ override fun addToEventData(key: String, data: FeatureUsageData) {
+ data.addData(key, value)
+ }
+ }
+
+ private class NumericalValue(override val value: Int) : FilePredictionFeature() {
+ override fun addToEventData(key: String, data: FeatureUsageData) {
+ data.addData(key, value)
+ }
+ }
+
+ private class DoubleValue(override val value: Double) : FilePredictionFeature() {
+ override fun addToEventData(key: String, data: FeatureUsageData) {
+ data.addData(key, value)
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionFeatureProvider.kt b/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionFeatureProvider.kt
new file mode 100644
index 000000000000..0eecb1032f3b
--- /dev/null
+++ b/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionFeatureProvider.kt
@@ -0,0 +1,51 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.filePrediction
+
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.psi.PsiFile
+import org.jetbrains.annotations.ApiStatus
+
+object FilePredictionFeaturesHelper {
+ private val EP_NAME = ExtensionPointName.create("com.intellij.filePrediction.featureProvider")
+ private val EXTERNAL_REFERENCES_EP_NAME = ExtensionPointName.create("com.intellij.filePrediction.referencesProvider")
+
+ fun calculateExternalReferences(file: PsiFile?): Set {
+ return file?.let { getReferencesProvider(it) } ?: emptySet()
+ }
+
+ fun calculateFileFeatures(project: Project,
+ newFile: VirtualFile,
+ prevFile: VirtualFile?): Map {
+ val result = HashMap()
+ val providers = getFeatureProviders()
+ providers.forEach { provider ->
+ val prefix = if (provider.getName().isNotEmpty()) provider.getName() + "_" else ""
+ val features = provider.calculateFileFeatures(project, newFile, prevFile).mapKeys { prefix + it.key }
+ result.putAll(features)
+ }
+ return result
+ }
+
+ private fun getReferencesProvider(file: PsiFile): Set {
+ return EXTERNAL_REFERENCES_EP_NAME.extensions.flatMap { it.externalReferences(file) }.toSet()
+ }
+
+ private fun getFeatureProviders(): List {
+ return EP_NAME.extensionList
+ }
+}
+
+@ApiStatus.Internal
+interface FileExternalReferencesProvider {
+ fun externalReferences(file: PsiFile): Set
+}
+
+@ApiStatus.Internal
+interface FilePredictionFeatureProvider {
+
+ fun getName(): String
+
+ fun calculateFileFeatures(project: Project, newFile: VirtualFile, prevFile: VirtualFile?): Map
+}
\ No newline at end of file
diff --git a/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionGeneralFeatures.kt b/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionGeneralFeatures.kt
new file mode 100644
index 000000000000..e8b0150252cf
--- /dev/null
+++ b/plugins/filePrediction/src/com/intellij/filePrediction/FilePredictionGeneralFeatures.kt
@@ -0,0 +1,58 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.filePrediction
+
+import com.intellij.filePrediction.FilePredictionFeature.Companion.binary
+import com.intellij.filePrediction.FilePredictionFeature.Companion.numerical
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.roots.FileIndexFacade
+import com.intellij.openapi.util.SystemInfo
+import com.intellij.openapi.util.io.FileUtil
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.openapi.util.text.StringUtil.toLowerCase
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.util.PathUtil
+import java.io.File
+
+class FilePredictionGeneralFeatures: FilePredictionFeatureProvider {
+ override fun getName(): String = ""
+
+ override fun calculateFileFeatures(project: Project, newFile: VirtualFile, prevFile: VirtualFile?): Map {
+ val result = HashMap()
+ val fileIndex = FileIndexFacade.getInstance(project)
+ result["in_project"] = binary(fileIndex.isInProjectScope(newFile))
+ result["in_source"] = binary(fileIndex.isInSource(newFile))
+ result["in_library"] = binary(fileIndex.isInLibraryClasses(newFile) || fileIndex.isInLibrarySource(newFile))
+ result["excluded"] = binary(fileIndex.isExcludedFile(newFile))
+
+ if (prevFile != null) {
+ val newFileName = unify(newFile.name)
+ val newFilePath = unify(newFile.path)
+ val prevFileName = unify(prevFile.name)
+ result["name_prefix"] = numerical(StringUtil.commonPrefixLength(newFileName, prevFileName))
+
+ val prevFilePath = unify(prevFile.path)
+ result["path_prefix"] = numerical(StringUtil.commonPrefixLength(newFilePath, prevFilePath))
+
+ val baseDir = project.guessProjectDir()?.path?.let { unify(it) }
+ if (baseDir != null) {
+ val newRelativePath = FileUtil.getRelativePath(baseDir, newFilePath, File.separatorChar, false)
+ val prevRelativePath = FileUtil.getRelativePath(baseDir, prevFilePath, File.separatorChar, false)
+ if (newRelativePath != null && prevRelativePath != null) {
+ result["relative_path_prefix"] = numerical(StringUtil.commonPrefixLength(newRelativePath, prevRelativePath))
+ }
+ }
+
+ val newModule = fileIndex.getModuleForFile(newFile)
+ val prevModule = fileIndex.getModuleForFile(prevFile)
+ result["same_dir"] = binary(PathUtil.getParentPath(newFilePath) == PathUtil.getParentPath(prevFilePath))
+ result["same_module"] = binary(newModule != null && newModule == prevModule)
+ }
+ return result
+ }
+
+ private fun unify(path: String) : String {
+ val caseSensitive = SystemInfo.isFileSystemCaseSensitive
+ return if (caseSensitive) FileUtil.getNameWithoutExtension(path) else FileUtil.getNameWithoutExtension(toLowerCase(path))
+ }
+}
diff --git a/plugins/filePrediction/src/com/intellij/filePrediction/FileUsagePredictor.kt b/plugins/filePrediction/src/com/intellij/filePrediction/FileUsagePredictor.kt
new file mode 100644
index 000000000000..418ea11b6c85
--- /dev/null
+++ b/plugins/filePrediction/src/com/intellij/filePrediction/FileUsagePredictor.kt
@@ -0,0 +1,107 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.filePrediction
+
+import com.intellij.filePrediction.history.FilePredictionHistory
+import com.intellij.internal.statistic.collectors.fus.fileTypes.FileTypeUsagesCollector
+import com.intellij.internal.statistic.eventLog.FeatureUsageData
+import com.intellij.internal.statistic.service.fus.collectors.FUCounterUsageLogger
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.roots.FileIndexFacade
+import com.intellij.openapi.util.Computable
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.psi.PsiManager
+import com.intellij.util.concurrency.NonUrgentExecutor
+
+object FileUsagePredictor {
+ private const val CALCULATE_CANDIDATE_PROBABILITY: Double = 0.2
+ private const val MAX_CANDIDATE: Int = 10
+
+ fun onFileOpened(project: Project, newFile: VirtualFile, prevFile: VirtualFile?) {
+ NonUrgentExecutor.getInstance().execute {
+ val refs = calculateExternalReferences(project, prevFile)
+
+ FileNavigationLogger.logEvent(project, newFile, prevFile, "file.opened", refs.contains(newFile))
+ if (Math.random() < CALCULATE_CANDIDATE_PROBABILITY) {
+ prevFile?.let {
+ calculateCandidates(project, it, newFile, refs)
+ }
+ }
+
+ FilePredictionHistory.getInstance(project).onFileOpened(newFile.url)
+ }
+ }
+
+ private fun calculateExternalReferences(project: Project, prevFile: VirtualFile?): Set {
+ return ApplicationManager.getApplication().runReadAction(Computable> {
+ val prevPsiFile = prevFile?.let { PsiManager.getInstance(project).findFile(it) }
+ FilePredictionFeaturesHelper.calculateExternalReferences(prevPsiFile).mapNotNull { file -> file.virtualFile }.toSet()
+ })
+ }
+
+ private fun calculateCandidates(project: Project,
+ prevFile: VirtualFile,
+ openedFile: VirtualFile,
+ refs: Set) {
+ val candidates = selectFileCandidates(project, prevFile, refs)
+ for (candidate in candidates) {
+ if (candidate != openedFile) {
+ FileNavigationLogger.logEvent(project, candidate, prevFile, "candidate.calculated", refs.contains(candidate))
+ }
+ }
+ }
+
+ private fun selectFileCandidates(project: Project, currentFile: VirtualFile, refs: Set): List {
+ val result = ArrayList()
+ addWithLimit(refs.iterator(), result, currentFile, MAX_CANDIDATE / 2)
+
+ val fileIndex = FileIndexFacade.getInstance(project)
+ var parent = currentFile.parent
+ while (parent != null && parent.isDirectory && result.size < MAX_CANDIDATE && fileIndex.isInProjectScope(parent)) {
+ addWithLimit(parent.children.iterator(), result, currentFile, MAX_CANDIDATE)
+ parent = parent.parent
+ }
+ return result
+ }
+
+ private fun addWithLimit(from: Iterator, to: MutableList, skip: VirtualFile, limit: Int) {
+ while (to.size < limit && from.hasNext()) {
+ val next = from.next()
+ if (!next.isDirectory && skip != next) {
+ to.add(next)
+ }
+ }
+ }
+}
+
+private object FileNavigationLogger {
+ private const val GROUP_ID = "file.prediction"
+
+ fun logEvent(project: Project, newFile: VirtualFile, prevFile: VirtualFile?, event: String, isInRef: Boolean) {
+ val data = FileTypeUsagesCollector.newFeatureUsageData(newFile.fileType).
+ addNewFileInfo(newFile, isInRef).
+ addPrevFileInfo(prevFile).
+ addFileFeatures(project, newFile, prevFile)
+
+ FUCounterUsageLogger.getInstance().logEvent(project, GROUP_ID, event, data)
+ }
+
+ private fun FeatureUsageData.addNewFileInfo(newFile: VirtualFile, isInRef: Boolean): FeatureUsageData {
+ return addAnonymizedPath(newFile.path).addData("in_ref", isInRef)
+ }
+
+ private fun FeatureUsageData.addPrevFileInfo(prevFile: VirtualFile?): FeatureUsageData {
+ return addData("prev_file_type", prevFile?.fileType?.name ?: "undefined").addAnonymizedValue("prev_file_path", prevFile?.path)
+ }
+
+ private fun FeatureUsageData.addFileFeatures(project: Project,
+ newFile: VirtualFile,
+ prevFile: VirtualFile?): FeatureUsageData {
+ val features = FilePredictionFeaturesHelper.calculateFileFeatures(project, newFile, prevFile)
+ for (feature in features) {
+ feature.value.addToEventData(feature.key, this)
+ }
+ return this
+ }
+}
+
diff --git a/plugins/filePrediction/src/com/intellij/filePrediction/history/FilePredictionHistory.kt b/plugins/filePrediction/src/com/intellij/filePrediction/history/FilePredictionHistory.kt
new file mode 100644
index 000000000000..180eb6841e7f
--- /dev/null
+++ b/plugins/filePrediction/src/com/intellij/filePrediction/history/FilePredictionHistory.kt
@@ -0,0 +1,34 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.filePrediction.history
+
+import com.intellij.openapi.components.*
+import com.intellij.openapi.project.Project
+
+@State(name = "FilePredictionHistory", storages = [Storage(StoragePathMacros.CACHE_FILE)])
+class FilePredictionHistory: PersistentStateComponent {
+ private var state = FilePredictionHistoryState()
+
+ companion object {
+ private const val RECENT_FILES_LIMIT = 50
+
+ fun getInstance(project: Project): FilePredictionHistory {
+ return ServiceManager.getService(project, FilePredictionHistory::class.java)
+ }
+ }
+
+ fun onFileOpened(fileUrl: String) = state.onFileOpened(fileUrl, RECENT_FILES_LIMIT)
+
+ fun position(fileUrl: String): Int = state.position(fileUrl)
+
+ fun size(): Int = state.size()
+
+ fun cleanup() = state.cleanup()
+
+ override fun getState(): FilePredictionHistoryState? {
+ return state
+ }
+
+ override fun loadState(newState: FilePredictionHistoryState) {
+ state = newState
+ }
+}
\ No newline at end of file
diff --git a/plugins/filePrediction/src/com/intellij/filePrediction/history/FilePredictionHistoryFeatures.kt b/plugins/filePrediction/src/com/intellij/filePrediction/history/FilePredictionHistoryFeatures.kt
new file mode 100644
index 000000000000..6807f30a3095
--- /dev/null
+++ b/plugins/filePrediction/src/com/intellij/filePrediction/history/FilePredictionHistoryFeatures.kt
@@ -0,0 +1,20 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.filePrediction.history
+
+import com.intellij.filePrediction.FilePredictionFeature
+import com.intellij.filePrediction.FilePredictionFeature.Companion.numerical
+import com.intellij.filePrediction.FilePredictionFeatureProvider
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VirtualFile
+
+class FilePredictionHistoryFeatures: FilePredictionFeatureProvider {
+ override fun getName(): String = "history"
+
+ override fun calculateFileFeatures(project: Project, newFile: VirtualFile, prevFile: VirtualFile?): Map {
+ val result = HashMap()
+ val history = FilePredictionHistory.getInstance(project)
+ result["position"] = numerical(history.position(newFile.url))
+ result["size"] = numerical(history.size())
+ return result
+ }
+}
diff --git a/plugins/filePrediction/src/com/intellij/filePrediction/history/FilePredictionHistoryState.kt b/plugins/filePrediction/src/com/intellij/filePrediction/history/FilePredictionHistoryState.kt
new file mode 100644
index 000000000000..d5d66677d47f
--- /dev/null
+++ b/plugins/filePrediction/src/com/intellij/filePrediction/history/FilePredictionHistoryState.kt
@@ -0,0 +1,35 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.filePrediction.history
+
+import com.intellij.openapi.components.*
+import com.intellij.util.xmlb.annotations.OptionTag
+
+class FilePredictionHistoryState: BaseState() {
+ @get:OptionTag
+ val recentFiles by list()
+
+ @Synchronized
+ fun onFileOpened(fileUrl: String, limit: Int) {
+ recentFiles.remove(fileUrl)
+ recentFiles.add(fileUrl)
+
+ while (recentFiles.size > limit) {
+ recentFiles.removeAt(0)
+ }
+ }
+
+ @Synchronized
+ fun position(fileUrl: String): Int {
+ var i = recentFiles.size - 1
+ while (i >= 0 && recentFiles[i] != fileUrl) {
+ i--
+ }
+ return if (i < 0) -1 else recentFiles.size - 1 - i
+ }
+
+ @Synchronized
+ fun size(): Int = recentFiles.size
+
+ @Synchronized
+ fun cleanup() = recentFiles.clear()
+}
\ No newline at end of file
diff --git a/plugins/filePrediction/src/com/intellij/filePrediction/vcs/FilePredictionVcsFeatures.kt b/plugins/filePrediction/src/com/intellij/filePrediction/vcs/FilePredictionVcsFeatures.kt
new file mode 100644
index 000000000000..3ac08250566c
--- /dev/null
+++ b/plugins/filePrediction/src/com/intellij/filePrediction/vcs/FilePredictionVcsFeatures.kt
@@ -0,0 +1,23 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.filePrediction.vcs
+
+import com.intellij.filePrediction.FilePredictionFeature
+import com.intellij.filePrediction.FilePredictionFeatureProvider
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vcs.ProjectLevelVcsManager
+import com.intellij.openapi.vcs.changes.ChangeListManager
+import com.intellij.openapi.vfs.VirtualFile
+
+class FilePredictionVcsFeatures : FilePredictionFeatureProvider {
+ override fun getName(): String = "vcs"
+
+ override fun calculateFileFeatures(project: Project, newFile: VirtualFile, prevFile: VirtualFile?): Map {
+ if (!ProjectLevelVcsManager.getInstance(project).hasAnyMappings()) return emptyMap()
+
+ val result = HashMap()
+ val changeListManager = ChangeListManager.getInstance(project)
+ result["prev_in_changelist"] = FilePredictionFeature.binary(prevFile != null && changeListManager.isFileAffected(prevFile))
+ result["in_changelist"] = FilePredictionFeature.binary(changeListManager.isFileAffected(newFile))
+ return result
+ }
+}
diff --git a/plugins/filePrediction/test/com/intellij/filePrediction/FilePredictionFeaturesTest.kt b/plugins/filePrediction/test/com/intellij/filePrediction/FilePredictionFeaturesTest.kt
new file mode 100644
index 000000000000..870aeca32f8d
--- /dev/null
+++ b/plugins/filePrediction/test/com/intellij/filePrediction/FilePredictionFeaturesTest.kt
@@ -0,0 +1,396 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.filePrediction
+
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.roots.ModuleRootManager
+import com.intellij.openapi.roots.OrderRootType
+import com.intellij.openapi.util.io.FileUtil
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.testFramework.builders.ModuleFixtureBuilder
+import com.intellij.testFramework.fixtures.CodeInsightFixtureTestCase
+import com.intellij.testFramework.fixtures.ModuleFixture
+import com.intellij.util.io.URLUtil
+import org.jetbrains.jps.model.java.JavaSourceRootType
+import org.jetbrains.jps.model.java.JpsJavaExtensionService
+import org.junit.Test
+import java.io.File
+
+class FilePredictionFeaturesTest : CodeInsightFixtureTestCase>() {
+
+ private fun doTestGeneralFeatures(prevPath: String, newPath: String, featuresProvider: FileFeaturesProducer) {
+ val prevFile = myFixture.addFileToProject(prevPath, "PREVIOUS FILE")
+ val nextFile = myFixture.addFileToProject(newPath, "NEXT FILE")
+
+ val provider = FilePredictionGeneralFeatures()
+ val actual = provider.calculateFileFeatures(myFixture.project, nextFile.virtualFile, prevFile.virtualFile)
+ val expected = featuresProvider.produce(myFixture.project)
+ for (feature in expected.entries) {
+ assertTrue("Cannot find feature '${feature.key}' in $actual", actual.containsKey(feature.key))
+ assertEquals("The value of feature '${feature.key}' is different from expected", feature.value, actual[feature.key])
+ }
+ }
+
+ private fun doTestGeneralFeatures(newPath: String, configurator: ProjectConfigurator, featuresProvider: FileFeaturesProducer) {
+ val nextFile = myFixture.addFileToProject(newPath, "NEXT FILE")
+ configurator.configure(myFixture.project, myModule)
+
+ val provider = FilePredictionGeneralFeatures()
+ val actual = provider.calculateFileFeatures(myFixture.project, nextFile.virtualFile, null)
+ val expected = featuresProvider.produce(myFixture.project)
+ for (feature in expected.entries) {
+ assertTrue("Cannot find feature '${feature.key}' in $actual", actual.containsKey(feature.key))
+ assertEquals("The value of feature '${feature.key}' is different from expected", feature.value, actual[feature.key])
+ }
+ }
+
+ @Test
+ fun `test file name prefix for completely different files`() {
+ doTestGeneralFeatures(
+ "prevFile.txt", "nextFile.txt",
+ ConstFileFeaturesProducer(
+ "name_prefix" to FilePredictionFeature.numerical(0),
+ "relative_path_prefix" to FilePredictionFeature.numerical(0)
+ )
+ )
+ }
+
+ @Test
+ fun `test file name prefix for files with common prefix`() {
+ doTestGeneralFeatures(
+ "myPrevFile.txt", "myNextFile.txt",
+ ConstFileFeaturesProducer(
+ "name_prefix" to FilePredictionFeature.numerical(2),
+ "relative_path_prefix" to FilePredictionFeature.numerical(2)
+ )
+ )
+ }
+
+ @Test
+ fun `test file name prefix for equal files`() {
+ doTestGeneralFeatures(
+ "file.txt", "src/file.txt",
+ ConstFileFeaturesProducer(
+ "name_prefix" to FilePredictionFeature.numerical(4),
+ "relative_path_prefix" to FilePredictionFeature.numerical(0)
+ )
+ )
+ }
+
+ @Test
+ fun `test file name of different length`() {
+ doTestGeneralFeatures(
+ "someFile.txt", "src/file.txt",
+ ConstFileFeaturesProducer(
+ "name_prefix" to FilePredictionFeature.numerical(0),
+ "relative_path_prefix" to FilePredictionFeature.numerical(1)
+ )
+ )
+ }
+
+ @Test
+ fun `test file name in child directory`() {
+ doTestGeneralFeatures(
+ "src/someFile.txt", "src/file.txt",
+ ConstFileFeaturesProducer(
+ "name_prefix" to FilePredictionFeature.numerical(0),
+ "relative_path_prefix" to FilePredictionFeature.numerical(4)
+ )
+ )
+ }
+
+ @Test
+ fun `test file name in neighbour directories`() {
+ doTestGeneralFeatures(
+ "src/com/site/ui/someFile.txt", "src/com/site/component/file.txt",
+ ConstFileFeaturesProducer(
+ "name_prefix" to FilePredictionFeature.numerical(0),
+ "relative_path_prefix" to FilePredictionFeature.numerical(13)
+ )
+ )
+ }
+
+ @Test
+ fun `test files path in project root`() {
+ doTestGeneralFeatures(
+ "prevFile.txt", "nextFile.txt",
+ FileFeaturesByProjectPathProducer(
+ "path_prefix" to FilePredictionFeature.numerical(0)
+ )
+ )
+ }
+
+ @Test
+ fun `test files path in the same directory`() {
+ doTestGeneralFeatures(
+ "src/prevFile.txt", "src/nextFile.txt",
+ FileFeaturesByProjectPathProducer(
+ "path_prefix" to FilePredictionFeature.numerical(4)
+ )
+ )
+ }
+
+ @Test
+ fun `test files path in the neighbour directories`() {
+ doTestGeneralFeatures(
+ "src/ui/prevFile.txt", "src/components/nextFile.txt",
+ FileFeaturesByProjectPathProducer(
+ "path_prefix" to FilePredictionFeature.numerical(4)
+ )
+ )
+ }
+
+ @Test
+ fun `test files path of different length`() {
+ doTestGeneralFeatures(
+ "firstFile.txt", "another/nextFile.txt",
+ FileFeaturesByProjectPathProducer(
+ "path_prefix" to FilePredictionFeature.numerical(0)
+ )
+ )
+ }
+
+ @Test
+ fun `test files in the root directory`() {
+ doTestGeneralFeatures(
+ "prevFile.txt", "nextFile.txt",
+ ConstFileFeaturesProducer(
+ "same_dir" to FilePredictionFeature.binary(true),
+ "same_module" to FilePredictionFeature.binary(true)
+ )
+ )
+ }
+
+ @Test
+ fun `test files in same child directory`() {
+ doTestGeneralFeatures(
+ "src/prevFile.txt", "src/nextFile.txt",
+ ConstFileFeaturesProducer(
+ "same_dir" to FilePredictionFeature.binary(true),
+ "same_module" to FilePredictionFeature.binary(true)
+ )
+ )
+ }
+
+ @Test
+ fun `test files in different directories`() {
+ doTestGeneralFeatures(
+ "src/prevFile.txt", "test/nextFile.txt",
+ ConstFileFeaturesProducer(
+ "same_dir" to FilePredictionFeature.binary(false),
+ "same_module" to FilePredictionFeature.binary(true)
+ )
+ )
+ }
+
+ @Test
+ fun `test file not in a source root`() {
+ doTestGeneralFeatures(
+ "file.txt",
+ object : ProjectConfigurator {
+ override fun configure(project: Project, module: Module) {
+ TestProjectStructureConfigurator.removeSourceRoot(module)
+ }
+ },
+ ConstFileFeaturesProducer(
+ "in_project" to FilePredictionFeature.binary(true),
+ "in_source" to FilePredictionFeature.binary(false),
+ "in_library" to FilePredictionFeature.binary(false),
+ "excluded" to FilePredictionFeature.binary(false)
+ )
+ )
+ }
+
+ @Test
+ fun `test file in source root`() {
+ doTestGeneralFeatures(
+ "nextFile.txt",
+ EmptyProjectConfigurator,
+ ConstFileFeaturesProducer(
+ "in_project" to FilePredictionFeature.binary(true),
+ "in_source" to FilePredictionFeature.binary(true),
+ "in_library" to FilePredictionFeature.binary(false),
+ "excluded" to FilePredictionFeature.binary(false)
+ )
+ )
+ }
+
+ @Test
+ fun `test file not in a custom source root`() {
+ doTestGeneralFeatures(
+ "nextFile.txt",
+ object : ProjectConfigurator {
+ override fun configure(project: Project, module: Module) {
+ TestProjectStructureConfigurator.removeSourceRoot(module)
+ TestProjectStructureConfigurator.addSourceRoot(module, "src", false)
+ }
+ },
+ ConstFileFeaturesProducer(
+ "in_project" to FilePredictionFeature.binary(true),
+ "in_source" to FilePredictionFeature.binary(false),
+ "in_library" to FilePredictionFeature.binary(false),
+ "excluded" to FilePredictionFeature.binary(false)
+ )
+ )
+ }
+
+ @Test
+ fun `test file in a custom source root`() {
+ doTestGeneralFeatures(
+ "src/nextFile.txt",
+ object : ProjectConfigurator {
+ override fun configure(project: Project, module: Module) {
+ TestProjectStructureConfigurator.removeSourceRoot(module)
+ TestProjectStructureConfigurator.addSourceRoot(module, "src", false)
+ }
+ },
+ ConstFileFeaturesProducer(
+ "in_project" to FilePredictionFeature.binary(true),
+ "in_source" to FilePredictionFeature.binary(true),
+ "in_library" to FilePredictionFeature.binary(false),
+ "excluded" to FilePredictionFeature.binary(false)
+ )
+ )
+ }
+
+ @Test
+ fun `test file in a library source root`() {
+ doTestGeneralFeatures(
+ "lib/nextFile.txt",
+ object : ProjectConfigurator {
+ override fun configure(project: Project, module: Module) {
+ TestProjectStructureConfigurator.removeSourceRoot(module)
+ TestProjectStructureConfigurator.addLibrary(module, "lib", OrderRootType.SOURCES)
+ }
+ },
+ ConstFileFeaturesProducer(
+ "in_project" to FilePredictionFeature.binary(true),
+ "in_source" to FilePredictionFeature.binary(true),
+ "in_library" to FilePredictionFeature.binary(true),
+ "excluded" to FilePredictionFeature.binary(false)
+ )
+ )
+ }
+
+ @Test
+ fun `test file in library classes`() {
+ doTestGeneralFeatures(
+ "lib/nextFile.txt",
+ object : ProjectConfigurator {
+ override fun configure(project: Project, module: Module) {
+ TestProjectStructureConfigurator.removeSourceRoot(module)
+ TestProjectStructureConfigurator.addLibrary(module, "lib", OrderRootType.CLASSES)
+ }
+ },
+ ConstFileFeaturesProducer(
+ "in_project" to FilePredictionFeature.binary(false),
+ "in_source" to FilePredictionFeature.binary(false),
+ "in_library" to FilePredictionFeature.binary(true),
+ "excluded" to FilePredictionFeature.binary(false)
+ )
+ )
+ }
+}
+
+private class ConstFileFeaturesProducer(vararg included: Pair) : FileFeaturesProducer {
+ val features: MutableMap = hashMapOf()
+
+ init {
+ for (pair in included) {
+ features[pair.first] = pair.second
+ }
+ }
+
+ override fun produce(project: Project): Map {
+ return features
+ }
+}
+
+private class FileFeaturesByProjectPathProducer(vararg included: Pair) : FileFeaturesProducer {
+ val features: Array> = included
+
+ override fun produce(project: Project): Map {
+ val dir = project.guessProjectDir()?.path
+ CodeInsightFixtureTestCase.assertNotNull(dir)
+
+ val prefixLength = dir!!.length + 1
+
+ val result: MutableMap = hashMapOf()
+ for (feature in features) {
+ val value = feature.second.toString().toIntOrNull()
+ if (value != null) {
+ result[feature.first] = FilePredictionFeature.numerical(prefixLength + value)
+ }
+ else {
+ result[feature.first] = feature.second
+ }
+ }
+ return result
+ }
+}
+
+private interface FileFeaturesProducer {
+ fun produce(project: Project): Map
+}
+
+private object EmptyProjectConfigurator : ProjectConfigurator {
+ override fun configure(project: Project, module: Module) = Unit
+}
+
+private interface ProjectConfigurator {
+ fun configure(project: Project, module: Module)
+}
+
+private object TestProjectStructureConfigurator {
+ fun addSourceRoot(module: Module, path: String, isTest: Boolean) {
+ val project = module.project
+ val dir = project.guessProjectDir()?.path
+ CodeInsightFixtureTestCase.assertNotNull(dir)
+
+ val fullPath = "${dir}${File.separator}$path"
+
+ ApplicationManager.getApplication().runWriteAction {
+ val model = ModuleRootManager.getInstance(module).modifiableModel
+ val contentEntry = model.contentEntries.find {
+ it.file?.let { file -> FileUtil.isAncestor(file.path, fullPath, false) } ?: false
+ }
+
+ val rootType = if (isTest) JavaSourceRootType.TEST_SOURCE else JavaSourceRootType.SOURCE
+ val properties = JpsJavaExtensionService.getInstance().createSourceRootProperties("")
+ val url = VirtualFileManager.constructUrl(URLUtil.FILE_PROTOCOL, fullPath)
+ contentEntry?.addSourceFolder(url, rootType, properties)
+
+ model.commit()
+ }
+ }
+
+ fun addLibrary(module: Module, path: String, type: OrderRootType) {
+ val project = module.project
+ val dir = project.guessProjectDir()?.path
+ CodeInsightFixtureTestCase.assertNotNull(dir)
+ val fullPath = "${dir}${File.separator}$path"
+
+ ApplicationManager.getApplication().runWriteAction {
+ val model = ModuleRootManager.getInstance(module).modifiableModel
+ val libraryModel = model.moduleLibraryTable.modifiableModel
+
+ val modifiableModel = libraryModel.createLibrary("test_library").modifiableModel
+ val url = VirtualFileManager.constructUrl(URLUtil.FILE_PROTOCOL, fullPath)
+ modifiableModel.addRoot(url, type)
+
+ modifiableModel.commit()
+ model.commit()
+ }
+ }
+
+ fun removeSourceRoot(module: Module) {
+ ApplicationManager.getApplication().runWriteAction {
+ val model = ModuleRootManager.getInstance(module).modifiableModel
+ val contentEntry = model.contentEntries[0]
+ contentEntry.removeSourceFolder(contentEntry.sourceFolders[0])
+ model.commit()
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/filePrediction/test/com/intellij/filePrediction/FilePredictionHistoryFeaturesTest.kt b/plugins/filePrediction/test/com/intellij/filePrediction/FilePredictionHistoryFeaturesTest.kt
new file mode 100644
index 000000000000..d40997a8956e
--- /dev/null
+++ b/plugins/filePrediction/test/com/intellij/filePrediction/FilePredictionHistoryFeaturesTest.kt
@@ -0,0 +1,118 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.filePrediction
+
+import com.intellij.filePrediction.history.FilePredictionHistoryState
+import com.intellij.testFramework.builders.ModuleFixtureBuilder
+import com.intellij.testFramework.fixtures.CodeInsightFixtureTestCase
+import com.intellij.testFramework.fixtures.ModuleFixture
+
+class FilePredictionHistoryFeaturesTest : CodeInsightFixtureTestCase>() {
+
+ private fun doTest(openedFiles: List, size: Int, vararg expected: Pair) {
+ val history = FilePredictionHistoryState()
+ try {
+ for (file in openedFiles) {
+ history.onFileOpened(file, 5)
+ }
+
+ assertEquals(size, history.size())
+
+ for (pair in expected) {
+ assertEquals(pair.second, history.position(pair.first))
+ }
+ }
+ finally {
+ history.cleanup()
+ }
+ }
+
+ fun `test position of the file without history`() {
+ doTest(listOf(), 0, "file://a" to -1)
+ }
+
+ fun `test position of the new file`() {
+ doTest(listOf("file://a"), 1, "file://b" to -1)
+ }
+
+ fun `test position of the prev file`() {
+ doTest(listOf("file://a"), 1, "file://a" to 0)
+ }
+
+ fun `test position of the first file`() {
+ doTest(listOf("file://a", "file://b", "file://c"), 3, "file://a" to 2)
+ }
+
+ fun `test position of the middle file`() {
+ doTest(listOf("file://a", "file://b", "file://c"), 3, "file://b" to 1)
+ }
+
+ fun `test position of the latest file`() {
+ doTest(listOf("file://a", "file://b", "file://c"), 3, "file://c" to 0)
+ }
+
+ fun `test position of the file opened multiple times`() {
+ doTest(listOf("file://a", "file://b", "file://a", "file://c"), 3, "file://a" to 1)
+ }
+
+ fun `test position of the latest file opened multiple times`() {
+ doTest(listOf("file://a", "file://b", "file://a", "file://c", "file://a"), 3, "file://a" to 0)
+ }
+
+ fun `test position of the middle file opened multiple times`() {
+ doTest(listOf("file://a", "file://b", "file://a", "file://b", "file://a"), 2, "file://b" to 1)
+ }
+
+ fun `test recent files history is full`() {
+ doTest(
+ listOf("file://a", "file://b", "file://c", "file://d", "file://e"), 5,
+ "file://a" to 4,
+ "file://b" to 3,
+ "file://c" to 2,
+ "file://d" to 1,
+ "file://e" to 0
+ )
+ }
+
+ fun `test recent files history more than limit`() {
+ doTest(
+ listOf("file://a", "file://b", "file://c", "file://d", "file://e", "file://f"), 5,
+ "file://b" to 4,
+ "file://c" to 3,
+ "file://d" to 2,
+ "file://e" to 1,
+ "file://f" to 0
+ )
+ }
+
+ fun `test recent files history twice more than limit`() {
+ doTest(
+ listOf("file://a", "file://b", "file://c", "file://d", "file://e", "file://f", "file://g", "file://h", "file://i"), 5,
+ "file://e" to 4,
+ "file://f" to 3,
+ "file://g" to 2,
+ "file://h" to 1,
+ "file://i" to 0
+ )
+ }
+
+ fun `test recent files history with repetitions`() {
+ doTest(
+ listOf("file://a", "file://b", "file://a", "file://d", "file://a", "file://b", "file://c", "file://a", "file://d"), 4,
+ "file://b" to 3,
+ "file://c" to 2,
+ "file://a" to 1,
+ "file://d" to 0
+ )
+ }
+
+ fun `test recent files history with repetitions more than limit`() {
+ doTest(
+ listOf("file://a", "file://b", "file://a", "file://d", "file://a", "file://e", "file://c", "file://a", "file://d", "file://f"), 5,
+ "file://e" to 4,
+ "file://c" to 3,
+ "file://a" to 2,
+ "file://d" to 1,
+ "file://f" to 0
+ )
+ }
+}
\ No newline at end of file