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 8288e7385d7e..1c10f5a6b937 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 @@ -114,6 +114,7 @@ internal class BackendRecentFileEventsModel(private val project: Project, corout val metadata = metadataRequest.frontendRecentFiles .mapNotNull { frontendFileId -> frontendFileId.virtualFile() } + .filter { isAllowedInRecentFilesModel(project, it) } .map { frontendFile -> readAction { createRecentFileViewModel(frontendFile, project) @@ -183,7 +184,7 @@ internal class BackendRecentFileEventsModel(private val project: Project, corout private suspend fun processOrderChangeEvent(event: OrderChangeEvent) { when (event.changeKind) { FileChangeKind.ADDED -> { - val models = createRecentFilesViewModels(event.files) + val models = createRecentFilesViewModels(event.files.filter { isAllowedInRecentFilesModel(project, it) }) val fileEvent = BackendRecentFilesEvent.ItemsAdded(models) for (fileKind in RecentFileKind.entries) { @@ -212,7 +213,9 @@ internal class BackendRecentFileEventsModel(private val project: Project, corout BackendRecentFilesModel.getInstance(project).getFilesByKind(fileKind).toSet() } - val filesToUpdate = files.filter { file -> knownFilesByKind.values.any { known -> known.contains(file) } } + val filesToUpdate = files.filter { file -> + isAllowedInRecentFilesModel(project, file) && knownFilesByKind.values.any { known -> known.contains(file) } + } val models = createRecentFilesViewModels(filesToUpdate) assert(models.size == filesToUpdate.size) @@ -266,6 +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) } .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 19be55f9d422..1773b1cd38f2 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,17 +1,22 @@ // 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 checkValidity(model: VirtualFile): Boolean { - return model.isValid + 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/src/com/intellij/platform/recentFiles/frontend/RecentFilesExcluder.kt b/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/RecentFilesExcluder.kt index 9c92681f5a7f..b63708c22b82 100644 --- a/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/RecentFilesExcluder.kt +++ b/platform/recentFiles/frontend/src/com/intellij/platform/recentFiles/frontend/RecentFilesExcluder.kt @@ -4,6 +4,7 @@ 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 @@ -16,4 +17,13 @@ interface RecentFilesExcluder { @JvmField val EP_NAME: ExtensionPointName = ExtensionPointName("com.intellij.recentFiles.excluder") } -} \ No newline at end of file +} + +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 3476c94eaf24..2993cbac3265 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 @@ -25,8 +25,11 @@ 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)) { + frontendRecentFilesModel.applyFrontendChanges(RecentFileKind.RECENTLY_OPENED, listOf(file), FileChangeKind.REMOVED) + } } - } + } override fun selectionChanged(event: FileEditorManagerEvent) { val file = event.newFile ?: return 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 cdaa5c1baf75..6dcb791beeaf 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 @@ -15,11 +15,11 @@ import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.platform.project.projectId -import com.intellij.platform.recentFiles.frontend.RecentFilesExcluder 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 @@ -42,38 +42,13 @@ class FrontendRecentFilesModel(private val project: Project) { private val modelState = FrontendRecentFilesMutableState(project) fun getRecentFiles(fileKind: RecentFileKind): List { val capturedModelState = modelState.chooseStateToReadFrom(fileKind).value.entries - val filteredModel = filterOutExcludedFiles(capturedModelState, fileKind) return when (fileKind) { - RecentFileKind.RECENTLY_OPENED_UNPINNED -> considerOpenedEditorWindowsForFiles(filteredModel) - else -> filteredModel + RecentFileKind.RECENTLY_OPENED_UNPINNED -> considerOpenedEditorWindowsForFiles(capturedModelState) + else -> capturedModelState } } - private fun filterOutExcludedFiles( - capturedModelState: List, - fileKind: RecentFileKind, - ): List { - val filteredModel = capturedModelState.filter { fileModel -> - val file = fileModel.virtualFile ?: return@filter true - val excluder = 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) - } - } - excluder == null - } - LOG.trace { - val modelData = if (filteredModel.size != capturedModelState.size) - "After filtering: ${filteredModel.joinToString { it.virtualFile?.name ?: "null" }}" - else - "" - "Return requested $fileKind list: ${capturedModelState.joinToString { it.virtualFile?.name ?: "null" }} $modelData" - } - return filteredModel - } - private fun considerOpenedEditorWindowsForFiles(filteredModel: List): List { // With the tabs disabled, switching between editor windows doesn't work anyway, // so trying to associate the editors with the files just produces annoying duplicate entries. @@ -101,36 +76,52 @@ class FrontendRecentFilesModel(private val project: Project) { fun applyFrontendChanges(filesKind: RecentFileKind, files: List, changeKind: FileChangeKind) { if (files.isEmpty()) return - LOG.trace { "Applying frontend changes for kind: $filesKind, changeKind: $changeKind, files: ${files.joinToString { it.name }}" } + val filesToApply = if (changeKind == FileChangeKind.REMOVED) files else files.filterNot { isExcludedFromRecentFiles(project, filesKind, it) } + if (changeKind != FileChangeKind.REMOVED) { + val filesToRemove = files - filesToApply.toSet() + if (filesToRemove.isNotEmpty()) { + when (filesKind) { + RecentFileKind.RECENTLY_EDITED -> applyFrontendChanges(RecentFileKind.RECENTLY_EDITED, filesToRemove, FileChangeKind.REMOVED) + RecentFileKind.RECENTLY_OPENED, RecentFileKind.RECENTLY_OPENED_UNPINNED -> { + applyFrontendChanges(RecentFileKind.RECENTLY_OPENED, filesToRemove, FileChangeKind.REMOVED) + applyFrontendChanges(RecentFileKind.RECENTLY_OPENED_UNPINNED, filesToRemove, FileChangeKind.REMOVED) + } + } + } + } + if (filesToApply.isEmpty()) return + + LOG.trace { "Applying frontend changes for kind: $filesKind, changeKind: $changeKind, files: ${filesToApply.joinToString { it.name }}" } modelUpdateScope.launch { val frontendStateToUpdate = modelState.chooseStateToWriteTo(filesKind) - val fileModels = files.map { convertVirtualFileToViewModel(it, project) } + val fileModels = filesToApply.map { convertVirtualFileToViewModel(it, project) } frontendStateToUpdate.update { oldList -> - when (changeKind) { + val updatedEntries = when (changeKind) { FileChangeKind.ADDED -> { val maybeItemsWithRichMetadata = oldList.entries.associateBy { it } val effectiveModelsToInsert = fileModels.map { fileModel -> maybeItemsWithRichMetadata[fileModel] ?: fileModel } - RecentFilesState(effectiveModelsToInsert + (oldList.entries - effectiveModelsToInsert.toSet())) + effectiveModelsToInsert + (oldList.entries - effectiveModelsToInsert.toSet()) } FileChangeKind.REMOVED -> { - RecentFilesState(oldList.entries - fileModels.toSet()) + oldList.entries - fileModels.toSet() } FileChangeKind.UPDATED_AND_PUT_ON_TOP -> { - RecentFilesState(fileModels + oldList.entries - fileModels.toSet()) + fileModels + oldList.entries - fileModels.toSet() } else -> { - oldList + oldList.entries } } + RecentFilesState(updatedEntries.filter { modelState.isAllowedInModel(filesKind, it) }) } when (changeKind) { FileChangeKind.REMOVED -> { - FileSwitcherApi.getInstance().updateRecentFilesBackendState(createHideFilesRequest(filesKind, files, project)) + FileSwitcherApi.getInstance().updateRecentFilesBackendState(createHideFilesRequest(filesKind, filesToApply, project)) } else -> { - FileSwitcherApi.getInstance().updateRecentFilesBackendState(createFilesUpdateRequest(filesKind, files, true, project)) + FileSwitcherApi.getInstance().updateRecentFilesBackendState(createFilesUpdateRequest(filesKind, filesToApply, true, project)) } } } @@ -176,4 +167,4 @@ class FrontendRecentFilesModel(private val project: Project) { return project.serviceAsync() } } -} \ No newline at end of file +} 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 fa2404d5bbf0..94820849c1a7 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,6 +7,7 @@ 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 @@ -14,8 +15,9 @@ import com.intellij.util.IconUtil import kotlinx.coroutines.flow.MutableStateFlow internal class FrontendRecentFilesMutableState(project: Project) : RecentFilesMutableState(project) { - override fun checkValidity(model: SwitcherVirtualFile): Boolean { - return model.virtualFile?.isValid != false + 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> { 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 7dfe2ecd12c5..fe46f815c39f 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 @@ -18,7 +18,7 @@ abstract class RecentFilesMutableState(protected val project: Project) { protected val recentlyEditedFilesState: MutableStateFlow> = MutableStateFlow(RecentFilesState()) protected val recentlyOpenedPinnedFilesState: MutableStateFlow> = MutableStateFlow(RecentFilesState()) - abstract fun checkValidity(model: T): Boolean + abstract fun isAllowedInModel(targetFilesKind: RecentFileKind, model: T): Boolean fun chooseStateToWriteTo(targetFilesKind: RecentFileKind): MutableStateFlow> { return when (targetFilesKind) { @@ -34,7 +34,7 @@ abstract class RecentFilesMutableState(protected val project: Project) { LOG.debug("Adding ${batch.size} items to $targetFilesKind frontend model") targetModel.update { oldList -> val updatedState = toAdd + (oldList.entries - toAdd.toSet()) - RecentFilesState(updatedState.filter(::checkValidity)) + RecentFilesState(updatedState.filter { isAllowedInModel(targetFilesKind, it) }) } } @@ -49,11 +49,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(::checkValidity)) + RecentFilesState(updatedState.filter { isAllowedInModel(targetFilesKind, it) }) } else { val effectiveModelsToInsert = oldList.entries.map { oldItem -> itemsToMergeWithExisting[oldItem] ?: oldItem } - RecentFilesState(effectiveModelsToInsert.filter(::checkValidity)) + RecentFilesState(effectiveModelsToInsert.filter { isAllowedInModel(targetFilesKind, it) }) } } } @@ -63,7 +63,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(::checkValidity)) + RecentFilesState(updatedState.filter { isAllowedInModel(targetFilesKind, it) }) } }