[recent files + diff] IJ-MR-209657 IJPL-242683 Use shared recent files filtering code on both fe and be sides to simplify and unify filtering approach

(cherry picked from commit 6cd3a5a5048434ff77184f707a2687538a88097f)

GitOrigin-RevId: 3d15129cd2cb8991930e2a3dc6b271e03a931602
This commit is contained in:
Nikita Katkov
2026-06-28 13:03:18 +00:00
committed by intellij-monorepo-bot
parent 1620fd53ef
commit ed5180618d
15 changed files with 74 additions and 75 deletions
@@ -33,6 +33,7 @@ import com.intellij.platform.recentFiles.shared.RecentFileKind
import com.intellij.platform.recentFiles.shared.RecentFilesBackendRequest
import com.intellij.platform.recentFiles.shared.RecentFilesEvent
import com.intellij.platform.recentFiles.shared.SwitcherRpcDto
import com.intellij.platform.recentFiles.shared.isAllowedInRecentFilesModel
import com.intellij.problems.ProblemListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.awaitCancellation
@@ -114,7 +115,7 @@ internal class BackendRecentFileEventsModel(private val project: Project, corout
val metadata =
metadataRequest.frontendRecentFiles
.mapNotNull { frontendFileId -> frontendFileId.virtualFile() }
.filter { isAllowedInRecentFilesModel(project, it) }
.filter { isAllowedInRecentFilesModel(project, metadataRequest.filesKind, it) }
.map { frontendFile ->
readAction {
createRecentFileViewModel(frontendFile, project)
@@ -184,10 +185,11 @@ internal class BackendRecentFileEventsModel(private val project: Project, corout
private suspend fun processOrderChangeEvent(event: OrderChangeEvent) {
when (event.changeKind) {
FileChangeKind.ADDED -> {
val models = createRecentFilesViewModels(event.files.filter { isAllowedInRecentFilesModel(project, it) })
val fileEvent = BackendRecentFilesEvent.ItemsAdded(models)
for (fileKind in RecentFileKind.entries) {
val models = createRecentFilesViewModels(
event.files.filter { isAllowedInRecentFilesModel(project, fileKind, it) }
)
val fileEvent = BackendRecentFilesEvent.ItemsAdded(models)
chooseTargetFlow(fileKind).emit(fileEvent)
}
}
@@ -213,16 +215,14 @@ internal class BackendRecentFileEventsModel(private val project: Project, corout
BackendRecentFilesModel.getInstance(project).getFilesByKind(fileKind).toSet()
}
val filesToUpdate = files.filter { file ->
isAllowedInRecentFilesModel(project, file) && knownFilesByKind.values.any { known -> known.contains(file) }
}
val models = createRecentFilesViewModels(filesToUpdate)
assert(models.size == filesToUpdate.size)
val filesToUpdate = files.filter { file -> knownFilesByKind.values.any { known -> known.contains(file) } }
for (fileKind in RecentFileKind.entries) {
val knownFiles = knownFilesByKind[fileKind]!!
val eventModels = models.filterIndexed { index, _ -> knownFiles.contains(filesToUpdate[index]) }
val filesForKind = filesToUpdate.filter { file ->
knownFiles.contains(file) && isAllowedInRecentFilesModel(project, fileKind, file)
}
val eventModels = createRecentFilesViewModels(filesForKind)
val fileEvent = BackendRecentFilesEvent.ItemsUpdated(eventModels, putOnTop)
chooseTargetFlow(fileKind).emit(fileEvent)
@@ -269,7 +269,7 @@ internal class BackendRecentFileEventsModel(private val project: Project, corout
getFilesToShow(project = project,
recentFileKind = filter.filesKind,
filesFromFrontendEditorSelectionHistory = filter.frontendEditorSelectionHistory.mapNotNull(VirtualFileId::virtualFile))
.filter { isAllowedInRecentFilesModel(project, it) }
.filter { isAllowedInRecentFilesModel(project, filter.filesKind, it) }
.map {
readAction {
createRecentFileViewModel(it, project)
@@ -1,22 +1,13 @@
// 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.platform.recentFiles.backend
import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.recentFiles.shared.RecentFileKind
import com.intellij.platform.recentFiles.shared.RecentFilesMutableState
internal class BackendRecentFilesMutableState(project: Project) : RecentFilesMutableState<VirtualFile>(project) {
override fun isAllowedInModel(targetFilesKind: RecentFileKind, model: VirtualFile): Boolean {
return isAllowedInRecentFilesModel(project, model)
}
fun getFilesByKind(filesKind: RecentFileKind): List<VirtualFile> {
return chooseStateToWriteTo(filesKind).value.entries
}
}
internal fun isAllowedInRecentFilesModel(project: Project, file: VirtualFile): Boolean {
return file.isValid && (file !is IdeDocumentHistoryImpl.OptionallyIncluded || file.isIncludedInDocumentHistory(project))
}
@@ -31,9 +31,6 @@
<extensionPoint interface="com.intellij.platform.recentFiles.frontend.RecentFilesNavigator"
qualifiedName="com.intellij.recentFiles.navigator"
dynamic="true"/>
<extensionPoint interface="com.intellij.platform.recentFiles.frontend.RecentFilesExcluder"
qualifiedName="com.intellij.recentFiles.excluder"
dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<registryKey key="switcher.use.fallback.in.monolith" defaultValue="false"
@@ -1,29 +0,0 @@
// 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.platform.recentFiles.frontend
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.recentFiles.shared.RecentFileKind
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface RecentFilesExcluder {
fun isExcludedFromRecentlyOpened(project: Project, file: VirtualFile): Boolean
fun isExcludedFromRecentlyEdited(project: Project, file: VirtualFile): Boolean = false
companion object {
@JvmField
val EP_NAME: ExtensionPointName<RecentFilesExcluder> = ExtensionPointName<RecentFilesExcluder>("com.intellij.recentFiles.excluder")
}
}
internal fun isExcludedFromRecentFiles(project: Project, fileKind: RecentFileKind, file: VirtualFile): Boolean {
return RecentFilesExcluder.EP_NAME.findFirstSafe { ext ->
when (fileKind) {
RecentFileKind.RECENTLY_EDITED -> ext.isExcludedFromRecentlyEdited(project, file)
RecentFileKind.RECENTLY_OPENED, RecentFileKind.RECENTLY_OPENED_UNPINNED -> ext.isExcludedFromRecentlyOpened(project, file)
}
} != null
}
@@ -10,6 +10,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.recentFiles.frontend.model.FrontendRecentFilesModel
import com.intellij.platform.recentFiles.shared.FileChangeKind
import com.intellij.platform.recentFiles.shared.RecentFileKind
import com.intellij.platform.recentFiles.shared.isAllowedInRecentFilesModel
private class RecentlySelectedEditorListener : FileEditorManagerListener {
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
@@ -25,7 +26,7 @@ private class RecentlySelectedEditorListener : FileEditorManagerListener {
if (source.getEditors(file).isEmpty()) {
thisLogger().trace { "Do remove closed frontend file from model: ${file.name}" }
frontendRecentFilesModel.applyFrontendChanges(RecentFileKind.RECENTLY_OPENED_UNPINNED, listOf(file), FileChangeKind.REMOVED)
if (isExcludedFromRecentFiles(source.project, RecentFileKind.RECENTLY_OPENED, file)) {
if (!isAllowedInRecentFilesModel(source.project, RecentFileKind.RECENTLY_OPENED, file)) {
frontendRecentFilesModel.applyFrontendChanges(RecentFileKind.RECENTLY_OPENED, listOf(file), FileChangeKind.REMOVED)
}
}
@@ -19,7 +19,6 @@ import com.intellij.platform.recentFiles.frontend.SwitcherVirtualFile
import com.intellij.platform.recentFiles.frontend.createFilesSearchRequestRequest
import com.intellij.platform.recentFiles.frontend.createFilesUpdateRequest
import com.intellij.platform.recentFiles.frontend.createHideFilesRequest
import com.intellij.platform.recentFiles.frontend.isExcludedFromRecentFiles
import com.intellij.platform.recentFiles.shared.FileChangeKind
import com.intellij.platform.recentFiles.shared.FileSwitcherApi
import com.intellij.platform.recentFiles.shared.RecentFileKind
@@ -27,6 +26,7 @@ import com.intellij.platform.recentFiles.shared.RecentFilesCoroutineScopeProvide
import com.intellij.platform.recentFiles.shared.RecentFilesEvent
import com.intellij.platform.recentFiles.shared.RecentFilesState
import com.intellij.platform.recentFiles.shared.SwitcherRpcDto
import com.intellij.platform.recentFiles.shared.isAllowedInRecentFilesModel
import com.intellij.platform.util.coroutines.childScope
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -76,7 +76,10 @@ class FrontendRecentFilesModel(private val project: Project) {
fun applyFrontendChanges(filesKind: RecentFileKind, files: List<VirtualFile>, changeKind: FileChangeKind) {
if (files.isEmpty()) return
val filesToApply = if (changeKind == FileChangeKind.REMOVED) files else files.filterNot { isExcludedFromRecentFiles(project, filesKind, it) }
val filesToApply = if (changeKind == FileChangeKind.REMOVED) files
else files.filter {
isAllowedInRecentFilesModel(project, filesKind, it)
}
if (changeKind != FileChangeKind.REMOVED) {
val filesToRemove = files - filesToApply.toSet()
if (filesToRemove.isNotEmpty()) {
@@ -113,7 +116,10 @@ class FrontendRecentFilesModel(private val project: Project) {
oldList.entries
}
}
RecentFilesState(updatedEntries.filter { modelState.isAllowedInModel(filesKind, it) })
RecentFilesState(updatedEntries.filter { fileModel ->
val file = fileModel.virtualFile ?: return@filter true
isAllowedInRecentFilesModel(project, filesKind, file)
})
}
when (changeKind) {
@@ -7,7 +7,6 @@ import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.recentFiles.frontend.SwitcherVirtualFile
import com.intellij.platform.recentFiles.frontend.isExcludedFromRecentFiles
import com.intellij.platform.recentFiles.shared.RecentFileKind
import com.intellij.platform.recentFiles.shared.RecentFilesMutableState
import com.intellij.platform.recentFiles.shared.RecentFilesState
@@ -15,11 +14,6 @@ import com.intellij.util.IconUtil
import kotlinx.coroutines.flow.MutableStateFlow
internal class FrontendRecentFilesMutableState(project: Project) : RecentFilesMutableState<SwitcherVirtualFile>(project) {
override fun isAllowedInModel(targetFilesKind: RecentFileKind, model: SwitcherVirtualFile): Boolean {
val file = model.virtualFile ?: return true
return file.isValid && !isExcludedFromRecentFiles(project, targetFilesKind, file)
}
fun chooseStateToReadFrom(filesKind: RecentFileKind): MutableStateFlow<RecentFilesState<SwitcherVirtualFile>> {
return when (filesKind) {
RecentFileKind.RECENTLY_EDITED -> recentlyEditedFilesState
@@ -1,4 +1,4 @@
<idea-plugin>
<idea-plugin visibility="internal">
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<dependencies>
<module name="fleet.rhizomedb"/>
@@ -14,4 +14,10 @@
<module name="intellij.platform.util.ui"/>
</dependencies>
<!-- endregion -->
<extensionPoints>
<extensionPoint interface="com.intellij.platform.recentFiles.shared.RecentFilesExcluder"
qualifiedName="com.intellij.recentFiles.excluder"
dynamic="true"/>
</extensionPoints>
</idea-plugin>
@@ -0,0 +1,32 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.recentFiles.shared
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface RecentFilesExcluder {
fun isExcludedFromRecentlyOpened(project: Project, file: VirtualFile): Boolean
fun isExcludedFromRecentlyEdited(project: Project, file: VirtualFile): Boolean = false
companion object {
@JvmField
val EP_NAME: ExtensionPointName<RecentFilesExcluder> = ExtensionPointName("com.intellij.recentFiles.excluder")
}
}
@ApiStatus.Internal
fun isAllowedInRecentFilesModel(project: Project, fileKind: RecentFileKind, file: VirtualFile): Boolean {
return file.isValid &&
(file !is IdeDocumentHistoryImpl.OptionallyIncluded || file.isIncludedInDocumentHistory(project)) &&
RecentFilesExcluder.EP_NAME.findFirstSafe { ext ->
when (fileKind) {
RecentFileKind.RECENTLY_EDITED -> ext.isExcludedFromRecentlyEdited(project, file)
RecentFileKind.RECENTLY_OPENED, RecentFileKind.RECENTLY_OPENED_UNPINNED -> ext.isExcludedFromRecentlyOpened(project, file)
}
} == null
}
@@ -13,13 +13,11 @@ private val LOG by lazy { fileLogger() }
class RecentFilesState<T>(val entries: List<T> = listOf())
@ApiStatus.Internal
abstract class RecentFilesMutableState<T>(protected val project: Project) {
open class RecentFilesMutableState<T>(protected val project: Project) {
protected val recentlyOpenedFilesState: MutableStateFlow<RecentFilesState<T>> = MutableStateFlow(RecentFilesState())
protected val recentlyEditedFilesState: MutableStateFlow<RecentFilesState<T>> = MutableStateFlow(RecentFilesState())
protected val recentlyOpenedPinnedFilesState: MutableStateFlow<RecentFilesState<T>> = MutableStateFlow(RecentFilesState())
abstract fun isAllowedInModel(targetFilesKind: RecentFileKind, model: T): Boolean
fun chooseStateToWriteTo(targetFilesKind: RecentFileKind): MutableStateFlow<RecentFilesState<T>> {
return when (targetFilesKind) {
RecentFileKind.RECENTLY_EDITED -> recentlyEditedFilesState
@@ -30,11 +28,10 @@ abstract class RecentFilesMutableState<T>(protected val project: Project) {
fun addEvent(targetFilesKind: RecentFileKind, batch: List<T>) {
val targetModel = chooseStateToWriteTo(targetFilesKind)
val toAdd = batch
LOG.debug("Adding ${batch.size} items to $targetFilesKind frontend model")
targetModel.update { oldList ->
val updatedState = toAdd + (oldList.entries - toAdd.toSet())
RecentFilesState(updatedState.filter { isAllowedInModel(targetFilesKind, it) })
val updatedState = batch + (oldList.entries - batch.toSet())
RecentFilesState(updatedState)
}
}
@@ -49,11 +46,11 @@ abstract class RecentFilesMutableState<T>(protected val project: Project) {
val newValuesToPutIntoFirstPosition = oldList.entries.mapNotNull { oldItem -> itemsToMergeWithExisting[oldItem] }
val restOfExistingValues = oldList.entries - newValuesToPutIntoFirstPosition.toSet()
val updatedState = newValuesToPutIntoFirstPosition + restOfExistingValues
RecentFilesState(updatedState.filter { isAllowedInModel(targetFilesKind, it) })
RecentFilesState(updatedState)
}
else {
val effectiveModelsToInsert = oldList.entries.map { oldItem -> itemsToMergeWithExisting[oldItem] ?: oldItem }
RecentFilesState(effectiveModelsToInsert.filter { isAllowedInModel(targetFilesKind, it) })
RecentFilesState(effectiveModelsToInsert)
}
}
}
@@ -63,7 +60,7 @@ abstract class RecentFilesMutableState<T>(protected val project: Project) {
LOG.debug("Removing ${batch.size} items from $targetFilesKind frontend model")
targetModel.update { oldList ->
val updatedState = oldList.entries - batch.toSet()
RecentFilesState(updatedState.filter { isAllowedInModel(targetFilesKind, it) })
RecentFilesState(updatedState)
}
}
@@ -6,10 +6,10 @@ import com.intellij.openapi.application.EDT
import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.recentFiles.frontend.RecentFilesExcluder
import com.intellij.platform.recentFiles.frontend.model.FrontendRecentFilesModel
import com.intellij.platform.recentFiles.shared.FileChangeKind
import com.intellij.platform.recentFiles.shared.RecentFileKind
import com.intellij.platform.recentFiles.shared.RecentFilesExcluder
import com.intellij.testFramework.LightVirtualFile
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.TestDisposable
+2
View File
@@ -40,6 +40,7 @@ jvm_library(
"//platform/util/coroutines",
"//libraries/kotlinx/serialization/core",
"//libraries/kotlinx/serialization/json",
"//platform/recentFiles/shared:recentFiles",
"//platform/recentFiles/frontend",
"//platform/analysis-api:analysis",
"//platform/vcs-api/shared",
@@ -83,6 +84,7 @@ jvm_library(
"//platform/util/coroutines:coroutines_test_lib",
"//libraries/kotlinx/serialization/core:core_test_lib",
"//libraries/kotlinx/serialization/json:json_test_lib",
"//platform/recentFiles/shared:recentFiles_test_lib",
"//platform/recentFiles/frontend:frontend_test_lib",
"//platform/analysis-api:analysis_test_lib",
"//platform/vcs-api/shared:shared_test_lib",
@@ -50,6 +50,7 @@
<orderEntry type="module" module-name="intellij.platform.util.coroutines" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.core" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.json" />
<orderEntry type="module" module-name="intellij.platform.recentFiles" />
<orderEntry type="module" module-name="intellij.platform.recentFiles.frontend" />
<orderEntry type="module" module-name="intellij.platform.analysis" />
<orderEntry type="module" module-name="intellij.platform.vcs.shared" />
@@ -18,6 +18,7 @@
<module name="intellij.platform.lang.core"/>
<module name="intellij.platform.lang.impl"/>
<module name="intellij.platform.projectModel"/>
<module name="intellij.platform.recentFiles"/>
<module name="intellij.platform.recentFiles.frontend"/>
<module name="intellij.platform.rpc.topics"/>
<module name="intellij.platform.util.coroutines"/>
@@ -5,7 +5,7 @@ import com.intellij.diff.impl.DiffSettingsHolder
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.recentFiles.frontend.RecentFilesExcluder
import com.intellij.platform.recentFiles.shared.RecentFilesExcluder
internal class DiffRecentFilesExcluder : RecentFilesExcluder {
override fun isExcludedFromRecentlyOpened(project: Project, file: VirtualFile): Boolean {