From a3e9476e357b2fc64a5af60e8e7a1cdca970c512 Mon Sep 17 00:00:00 2001 From: Nikita Katkov Date: Mon, 22 Jun 2026 19:31:28 +0200 Subject: [PATCH] [recent files + diff] IJ-MR-209657 IJPL-242683 Remove diff and alike files from the data model instead of just hiding them from UI Previously, diff virtual files were just filtered out from the files list displayed in UI, but the backing data model retained them until the file got closed or the project got closed depending on the settings state. Now we always check on both frontend and backend if a specific file is allowed to be shown and do not add it/remove it from the model, should it be disallowed to be there. That does not solve the core problem with diff files holding a lot of hard references in their userdata, but improves the problem on the recent files side. Changes on the diff virtual file side are still necessary (cherry picked from commit 4df2acdff046ef2ae0ce8f106d64669bb8bfa902) GitOrigin-RevId: a6d724a4c64dad8a29287327a40ffd5e6189d8de --- .../backend/BackendRecentFileEventsModel.kt | 8 ++- .../backend/BackendRecentFilesMutableState.kt | 9 ++- .../frontend/RecentFilesExcluder.kt | 12 +++- .../frontend/RecentlyOpenedFileListener.kt | 5 +- .../model/FrontendRecentFilesModel.kt | 67 ++++++++----------- .../model/FrontendRecentFilesMutableState.kt | 6 +- .../shared/RecentFilesMutableState.kt | 10 +-- 7 files changed, 66 insertions(+), 51 deletions(-) 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) }) } }