diff --git a/platform/recentFiles/backend/src/com/intellij/platform/recentFiles/backend/BackendRecentFileEventsModel.kt b/platform/recentFiles/backend/src/com/intellij/platform/recentFiles/backend/BackendRecentFileEventsModel.kt index 1c10f5a6b937..97da7cfeab81 100644 --- a/platform/recentFiles/backend/src/com/intellij/platform/recentFiles/backend/BackendRecentFileEventsModel.kt +++ b/platform/recentFiles/backend/src/com/intellij/platform/recentFiles/backend/BackendRecentFileEventsModel.kt @@ -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) diff --git a/platform/recentFiles/backend/src/com/intellij/platform/recentFiles/backend/BackendRecentFilesMutableState.kt b/platform/recentFiles/backend/src/com/intellij/platform/recentFiles/backend/BackendRecentFilesMutableState.kt index 1773b1cd38f2..c3c25d38fbcb 100644 --- a/platform/recentFiles/backend/src/com/intellij/platform/recentFiles/backend/BackendRecentFilesMutableState.kt +++ b/platform/recentFiles/backend/src/com/intellij/platform/recentFiles/backend/BackendRecentFilesMutableState.kt @@ -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(project) { - override fun isAllowedInModel(targetFilesKind: RecentFileKind, model: VirtualFile): Boolean { - return isAllowedInRecentFilesModel(project, model) - } - fun getFilesByKind(filesKind: RecentFileKind): List { return chooseStateToWriteTo(filesKind).value.entries } -} - -internal fun isAllowedInRecentFilesModel(project: Project, file: VirtualFile): Boolean { - return file.isValid && (file !is IdeDocumentHistoryImpl.OptionallyIncluded || file.isIncludedInDocumentHistory(project)) } \ No newline at end of file diff --git a/platform/recentFiles/frontend/resources/intellij.platform.recentFiles.frontend.xml b/platform/recentFiles/frontend/resources/intellij.platform.recentFiles.frontend.xml index 71953375c8bb..0c64701addd0 100644 --- a/platform/recentFiles/frontend/resources/intellij.platform.recentFiles.frontend.xml +++ b/platform/recentFiles/frontend/resources/intellij.platform.recentFiles.frontend.xml @@ -31,9 +31,6 @@ - = ExtensionPointName("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 -} diff --git a/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/RecentlyOpenedFileListener.kt b/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/RecentlyOpenedFileListener.kt index 2993cbac3265..ee449bde5d7e 100644 --- a/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/RecentlyOpenedFileListener.kt +++ b/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/RecentlyOpenedFileListener.kt @@ -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) } } diff --git a/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/model/FrontendRecentFilesModel.kt b/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/model/FrontendRecentFilesModel.kt index 6dcb791beeaf..3cbea1afeb01 100644 --- a/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/model/FrontendRecentFilesModel.kt +++ b/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/model/FrontendRecentFilesModel.kt @@ -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, 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) { diff --git a/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/model/FrontendRecentFilesMutableState.kt b/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/model/FrontendRecentFilesMutableState.kt index 94820849c1a7..bf2defafdec3 100644 --- a/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/model/FrontendRecentFilesMutableState.kt +++ b/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/model/FrontendRecentFilesMutableState.kt @@ -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(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> { return when (filesKind) { RecentFileKind.RECENTLY_EDITED -> recentlyEditedFilesState diff --git a/platform/recentFiles/shared/resources/intellij.platform.recentFiles.xml b/platform/recentFiles/shared/resources/intellij.platform.recentFiles.xml index b2e8c0b8aefd..e1ed13103be4 100644 --- a/platform/recentFiles/shared/resources/intellij.platform.recentFiles.xml +++ b/platform/recentFiles/shared/resources/intellij.platform.recentFiles.xml @@ -1,4 +1,4 @@ - + @@ -14,4 +14,10 @@ + + + + \ No newline at end of file diff --git a/platform/recentFiles/shared/src/com/intellij/platform/recentFiles/shared/RecentFilesExcluder.kt b/platform/recentFiles/shared/src/com/intellij/platform/recentFiles/shared/RecentFilesExcluder.kt new file mode 100644 index 000000000000..f05ed0da382a --- /dev/null +++ b/platform/recentFiles/shared/src/com/intellij/platform/recentFiles/shared/RecentFilesExcluder.kt @@ -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 = 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 +} diff --git a/platform/recentFiles/shared/src/com/intellij/platform/recentFiles/shared/RecentFilesMutableState.kt b/platform/recentFiles/shared/src/com/intellij/platform/recentFiles/shared/RecentFilesMutableState.kt index fe46f815c39f..a7beaee649d2 100644 --- a/platform/recentFiles/shared/src/com/intellij/platform/recentFiles/shared/RecentFilesMutableState.kt +++ b/platform/recentFiles/shared/src/com/intellij/platform/recentFiles/shared/RecentFilesMutableState.kt @@ -13,13 +13,11 @@ private val LOG by lazy { fileLogger() } class RecentFilesState(val entries: List = listOf()) @ApiStatus.Internal -abstract class RecentFilesMutableState(protected val project: Project) { +open class RecentFilesMutableState(protected val project: Project) { protected val recentlyOpenedFilesState: MutableStateFlow> = MutableStateFlow(RecentFilesState()) protected val recentlyEditedFilesState: MutableStateFlow> = MutableStateFlow(RecentFilesState()) protected val recentlyOpenedPinnedFilesState: MutableStateFlow> = MutableStateFlow(RecentFilesState()) - abstract fun isAllowedInModel(targetFilesKind: RecentFileKind, model: T): Boolean - fun chooseStateToWriteTo(targetFilesKind: RecentFileKind): MutableStateFlow> { return when (targetFilesKind) { RecentFileKind.RECENTLY_EDITED -> recentlyEditedFilesState @@ -30,11 +28,10 @@ abstract class RecentFilesMutableState(protected val project: Project) { fun addEvent(targetFilesKind: RecentFileKind, batch: List) { 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(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(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) } } diff --git a/platform/recentFiles/tests/testSrc/com/intellij/platform/recentFiles/backend/RecentFilesDistributedModelTest.kt b/platform/recentFiles/tests/testSrc/com/intellij/platform/recentFiles/backend/RecentFilesDistributedModelTest.kt index 05388e01f0f8..61e007869687 100644 --- a/platform/recentFiles/tests/testSrc/com/intellij/platform/recentFiles/backend/RecentFilesDistributedModelTest.kt +++ b/platform/recentFiles/tests/testSrc/com/intellij/platform/recentFiles/backend/RecentFilesDistributedModelTest.kt @@ -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 diff --git a/platform/vcs-impl/frontend/BUILD.bazel b/platform/vcs-impl/frontend/BUILD.bazel index 7499d6db8454..54393c0608bc 100644 --- a/platform/vcs-impl/frontend/BUILD.bazel +++ b/platform/vcs-impl/frontend/BUILD.bazel @@ -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", diff --git a/platform/vcs-impl/frontend/intellij.platform.vcs.impl.frontend.iml b/platform/vcs-impl/frontend/intellij.platform.vcs.impl.frontend.iml index 52b7babf00cd..e3b6ebb0d49d 100644 --- a/platform/vcs-impl/frontend/intellij.platform.vcs.impl.frontend.iml +++ b/platform/vcs-impl/frontend/intellij.platform.vcs.impl.frontend.iml @@ -50,6 +50,7 @@ + diff --git a/platform/vcs-impl/frontend/resources/intellij.platform.vcs.impl.frontend.xml b/platform/vcs-impl/frontend/resources/intellij.platform.vcs.impl.frontend.xml index 00c5428c989c..bd87985bc0bf 100644 --- a/platform/vcs-impl/frontend/resources/intellij.platform.vcs.impl.frontend.xml +++ b/platform/vcs-impl/frontend/resources/intellij.platform.vcs.impl.frontend.xml @@ -18,6 +18,7 @@ + diff --git a/platform/vcs-impl/frontend/src/com/intellij/platform/vcs/impl/frontend/diff/DiffRecentFilesExcluder.kt b/platform/vcs-impl/frontend/src/com/intellij/platform/vcs/impl/frontend/diff/DiffRecentFilesExcluder.kt index 31e76e41c308..da21a1e493c1 100644 --- a/platform/vcs-impl/frontend/src/com/intellij/platform/vcs/impl/frontend/diff/DiffRecentFilesExcluder.kt +++ b/platform/vcs-impl/frontend/src/com/intellij/platform/vcs/impl/frontend/diff/DiffRecentFilesExcluder.kt @@ -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 {