[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
This commit is contained in:
Nikita Katkov
2026-06-28 13:03:18 +00:00
committed by intellij-monorepo-bot
parent 336d6e18ee
commit a3e9476e35
7 changed files with 66 additions and 51 deletions
@@ -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)
@@ -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<VirtualFile>(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<VirtualFile> {
return chooseStateToWriteTo(filesKind).value.entries
}
}
internal fun isAllowedInRecentFilesModel(project: Project, file: VirtualFile): Boolean {
return file.isValid && (file !is IdeDocumentHistoryImpl.OptionallyIncluded || file.isIncludedInDocumentHistory(project))
}
@@ -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<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
}
@@ -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
@@ -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<SwitcherVirtualFile> {
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<SwitcherVirtualFile>,
fileKind: RecentFileKind,
): List<SwitcherVirtualFile> {
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<SwitcherVirtualFile>): List<SwitcherVirtualFile> {
// 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<VirtualFile>, 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<FrontendRecentFilesModel>()
}
}
}
}
@@ -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<SwitcherVirtualFile>(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<RecentFilesState<SwitcherVirtualFile>> {
@@ -18,7 +18,7 @@ abstract class RecentFilesMutableState<T>(protected val project: Project) {
protected val recentlyEditedFilesState: MutableStateFlow<RecentFilesState<T>> = MutableStateFlow(RecentFilesState())
protected val recentlyOpenedPinnedFilesState: MutableStateFlow<RecentFilesState<T>> = MutableStateFlow(RecentFilesState())
abstract fun checkValidity(model: T): Boolean
abstract fun isAllowedInModel(targetFilesKind: RecentFileKind, model: T): Boolean
fun chooseStateToWriteTo(targetFilesKind: RecentFileKind): MutableStateFlow<RecentFilesState<T>> {
return when (targetFilesKind) {
@@ -34,7 +34,7 @@ abstract class RecentFilesMutableState<T>(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<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(::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<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(::checkValidity))
RecentFilesState(updatedState.filter { isAllowedInModel(targetFilesKind, it) })
}
}