diff --git a/platform/vcs-log/impl/resources/intellij.platform.vcs.log.impl.xml b/platform/vcs-log/impl/resources/intellij.platform.vcs.log.impl.xml index fe3b3a990aac..f42e7a985d21 100644 --- a/platform/vcs-log/impl/resources/intellij.platform.vcs.log.impl.xml +++ b/platform/vcs-log/impl/resources/intellij.platform.vcs.log.impl.xml @@ -93,6 +93,10 @@ + + + + @@ -140,6 +144,8 @@ + @@ -198,6 +204,7 @@ + @@ -248,6 +255,7 @@ + @@ -320,6 +328,7 @@ + diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogEditorUtil.kt b/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogEditorUtil.kt index 644707bc2ecd..a114990478d1 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogEditorUtil.kt +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogEditorUtil.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ModalityState import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VirtualFile @@ -23,12 +24,22 @@ object VcsLogEditorUtil { return FileEditorManager.getInstance(project).selectedEditors.flatMapTo(mutableSetOf(), ::getLogIds) } + @JvmStatic + fun findVcsLogUi(editors: Array, clazz: Class): T? { + return editors.asSequence().flatMap { VcsLogUiHolder.getLogUis(it.component) }.filterIsInstance(clazz).firstOrNull() + } + internal fun selectLogUi(project: Project, ui: VcsLogUi): Boolean { val fileEditorManager = FileEditorManager.getInstance(project) val editor = fileEditorManager.findLogFile(ui) ?: return false return fileEditorManager.openFile(editor, true, true).isNotEmpty() } + internal fun updateTabName(project: Project, ui: VcsLogUiEx) { + val fileEditorManager = FileEditorManagerEx.getInstanceEx(project) + fileEditorManager.findLogFile(ui)?.let { fileEditorManager.updateFilePresentation(it) } + } + private fun FileEditorManager.findLogFile(ui: VcsLogUi): VirtualFile? { return allEditors.first { getLogIds(it).contains(ui.id) }?.file } diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogTabLocation.kt b/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogTabLocation.kt index cffc33be8521..fc0cc6cb4db7 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogTabLocation.kt +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogTabLocation.kt @@ -19,7 +19,6 @@ enum class VcsLogTabLocation { TOOL_WINDOW { override fun select(project: Project, logUi: VcsLogUi): Boolean = VcsLogContentUtil.selectLogUi(project, logUi) }, - @Deprecated("Unused") EDITOR { override fun select(project: Project, logUi: VcsLogUi): Boolean = VcsLogEditorUtil.selectLogUi(project, logUi) }, diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogTabsManager.kt b/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogTabsManager.kt index e18744556838..883afded1f3e 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogTabsManager.kt +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/VcsLogTabsManager.kt @@ -1,7 +1,11 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.vcs.log.impl +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.NlsContexts.TabTitle @@ -19,9 +23,11 @@ import com.intellij.vcs.log.data.VcsLogData import com.intellij.vcs.log.impl.VcsLogContentUtil.getToolWindow import com.intellij.vcs.log.impl.VcsLogContentUtil.openLogTab import com.intellij.vcs.log.impl.VcsLogContentUtil.updateLogUiName +import com.intellij.vcs.log.impl.VcsLogEditorUtil.findVcsLogUi import com.intellij.vcs.log.impl.VcsLogManager.VcsLogUiFactory import com.intellij.vcs.log.ui.MainVcsLogUi import com.intellij.vcs.log.ui.VcsLogUiEx +import com.intellij.vcs.log.ui.editor.VcsLogVirtualFileSystem import com.intellij.vcs.log.util.GraphOptionsUtil.presentationForTabTitle import com.intellij.vcs.log.visible.filters.getPresentation import org.jetbrains.annotations.ApiStatus @@ -51,12 +57,19 @@ class VcsLogTabsManager internal constructor(private val project: Project, LOG.warn("Reopening standalone tabs is not supported") } - if (toolWindowTabs.isNotEmpty() || editorTabs.isNotEmpty()) { + if (editorTabs.isNotEmpty()) { + invokeLater(ModalityState.nonModal()) { + if (logManager.isDisposed) return@invokeLater + LOG.debug("Reopening editor tabs with ids: $editorTabs") + editorTabs.forEach { openEditorLogTab(it, false, null) } + } + } + + if (toolWindowTabs.isNotEmpty()) { futureToolWindow.thenAccept { toolWindow -> if (!LOG.assertTrue(!logManager.isDisposed, "Attempting to open tabs on disposed VcsLogManager")) return@thenAccept LOG.debug("Reopening toolwindow tabs with ids: $toolWindowTabs") toolWindowTabs.forEach { openToolWindowLogTab(toolWindow, it, false, null) } - editorTabs.forEach { openToolWindowLogTab(toolWindow, it, false, null) } } } @@ -86,7 +99,8 @@ class VcsLogTabsManager internal constructor(private val project: Project, val tabId = generateTabId(logManager) uiProperties.resetState(tabId) if (location === VcsLogTabLocation.EDITOR) { - error("Unsupported") + val editors = openEditorLogTab(tabId, true, filters) + return findVcsLogUi(editors, MainVcsLogUi::class.java)!! } else if (location === VcsLogTabLocation.TOOL_WINDOW) { val toolWindow = VcsLogContentUtil.getToolWindowOrThrow(project) @@ -96,6 +110,11 @@ class VcsLogTabsManager internal constructor(private val project: Project, throw UnsupportedOperationException("Only log in editor or tool window is supported") } + private fun openEditorLogTab(tabId: String, focus: Boolean, filters: VcsLogFilterCollection?): Array { + val file = VcsLogVirtualFileSystem.Holder.getInstance().createVcsLogFile(project, tabId, filters) + return FileEditorManager.getInstance(project).openFile(file, focus, true) + } + private fun openToolWindowLogTab(toolWindow: ToolWindow, tabId: String, focus: Boolean, filters: VcsLogFilterCollection?): MainVcsLogUi { val factory = getPersistentVcsLogUiFactory(tabId, VcsLogTabLocation.TOOL_WINDOW, filters) diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/actions/OpenAnotherLogTabAction.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/actions/OpenAnotherLogTabAction.java index 4a9bb3b61d3b..f324b98b66e5 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/actions/OpenAnotherLogTabAction.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/actions/OpenAnotherLogTabAction.java @@ -1,9 +1,7 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.vcs.log.ui.actions; -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.registry.Registry; @@ -108,11 +106,36 @@ public abstract class OpenAnotherLogTabAction extends DumbAwareAction { VcsLogManager logManager = VcsProjectLog.getInstance(project).getLogManager(); if (logManager == null) return VcsLogFilterObject.collection(); - Collection uis = ContainerUtil.filterIsInstance( - logManager.getVisibleLogUis(VcsLogTabLocation.TOOL_WINDOW), - MainVcsLogUi.class); + Collection uis = ContainerUtil.filterIsInstance(logManager.getVisibleLogUis(VcsLogTabLocation.TOOL_WINDOW), + MainVcsLogUi.class); if (uis.isEmpty()) return VcsLogFilterObject.collection(); return ContainerUtil.getFirstItem(uis).getFilterUi().getFilters(); } } + + @ApiStatus.Internal + public static class InEditor extends OpenAnotherLogTabAction { + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + if (e.getData(PlatformDataKeys.TOOL_WINDOW) != null && ActionPlaces.VCS_LOG_TOOLBAR_PLACE.equals(e.getPlace())) { + e.getPresentation().setEnabledAndVisible(false); + } + } + + @Override + protected @NotNull @Nls(capitalization = Nls.Capitalization.Sentence) String getDescription(@Nls @NotNull String vcsName) { + return VcsLogBundle.message("vcs.log.action.description.open.new.tab.with.log.in.editor", vcsName); + } + + @Override + protected @NotNull @Nls(capitalization = Nls.Capitalization.Title) String getText(@Nls @NotNull String vcsName) { + return VcsLogBundle.message("vcs.log.action.open.new.tab.with.log.in.editor", vcsName); + } + + @Override + protected @NotNull VcsLogTabLocation getLocation(@NotNull AnActionEvent e) { + return VcsLogTabLocation.EDITOR; + } + } } diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/editor/DefaultVcsLogFile.kt b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/editor/DefaultVcsLogFile.kt new file mode 100644 index 000000000000..af24d01c236e --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/editor/DefaultVcsLogFile.kt @@ -0,0 +1,128 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.vcs.log.ui.editor + +import com.intellij.ide.ui.UISettings +import com.intellij.openapi.components.* +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.fileEditor.FileEditorManagerKeys +import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFilePathWrapper +import com.intellij.openapi.vfs.VirtualFileSystem +import com.intellij.ui.components.JBPanelWithEmptyText +import com.intellij.util.xmlb.annotations.Tag +import com.intellij.vcs.log.VcsLogBundle +import com.intellij.vcs.log.VcsLogFilterCollection +import com.intellij.vcs.log.impl.* +import com.intellij.vcs.log.impl.VcsLogTabsManager.Companion.onDisplayNameChange +import com.intellij.vcs.log.ui.VcsLogPanel +import com.intellij.vcs.log.util.VcsLogUtil +import java.awt.BorderLayout +import javax.swing.JComponent + +internal class DefaultVcsLogFile(private val pathId: VcsLogVirtualFileSystem.VcsLogComplexPath, + private var filters: VcsLogFilterCollection? = null) : + VcsLogFile(VcsLogTabsManager.getFullName(pathId.logId)), VirtualFilePathWrapper { //NON-NLS not displayed + + private val fileSystemInstance: VcsLogVirtualFileSystem = VcsLogVirtualFileSystem.Holder.getInstance() + internal val tabId get() = pathId.logId + + internal var tabName: String + get() = service().getTabName(path) ?: name + set(value) = service().putTabName(path, value) + + init { + putUserData(FileEditorManagerKeys.FORBID_TAB_SPLIT, true) + } + + override fun createMainComponent(project: Project): JComponent { + val panel = JBPanelWithEmptyText(BorderLayout()).withEmptyText(VcsLogBundle.message("vcs.log.is.loading")) + VcsLogUtil.runWhenVcsAndLogIsReady(project) { logManager -> + val projectLog = VcsProjectLog.getInstance(project) + val tabsManager = projectLog.tabManager ?: return@runWhenVcsAndLogIsReady + + try { + val factory = tabsManager.getPersistentVcsLogUiFactory(tabId, VcsLogTabLocation.EDITOR, filters) + val ui = logManager.createLogUi(factory, VcsLogTabLocation.EDITOR) + tabName = VcsLogTabsManager.generateDisplayName(ui) + ui.onDisplayNameChange { + tabName = VcsLogTabsManager.generateDisplayName(ui) + VcsLogEditorUtil.updateTabName(project, ui) + } + if (filters != null) filters = null + panel.add(VcsLogPanel(logManager, ui), BorderLayout.CENTER) + } + catch (e: CannotAddVcsLogWindowException) { + LOG.error(e) + panel.emptyText.text = VcsLogBundle.message("vcs.log.duplicated.tab.id.error") + } + } + return panel + } + + override fun getFileSystem(): VirtualFileSystem = fileSystemInstance + override fun getPath(): String { + return fileSystemInstance.getPath(pathId) + } + + override fun enforcePresentableName(): Boolean = true + override fun getPresentableName(): String = tabName + override fun getPresentablePath(): String = tabName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DefaultVcsLogFile + + return tabId == other.tabId + } + + override fun hashCode(): Int { + return tabId.hashCode() + } + + companion object { + private val LOG = logger() + } +} + +internal class DefaultVcsLogFileTabTitleProvider : EditorTabTitleProvider, DumbAware { + + override fun getEditorTabTooltipText(project: Project, file: VirtualFile): String? { + if (file !is DefaultVcsLogFile) return null + return getEditorTabTitle(project, file) + } + + override fun getEditorTabTitle(project: Project, file: VirtualFile): String? { + if (file !is DefaultVcsLogFile) return null + return file.tabName + } +} + +@Service(Service.Level.APP) +@State(name = "Vcs.Log.Editor.Tab.Names", storages = [Storage(StoragePathMacros.CACHE_FILE)]) +private class VcsLogEditorTabNameCache : SimplePersistentStateComponent(MyState()) { + + fun getTabName(path: String) = state.pathToTabName[path] + + fun putTabName(path: String, tabName: String) { + state.pathToTabName.remove(path) + state.pathToTabName[path] = tabName // to put recently changed paths at the end of the linked map + + val limit = UISettings.getInstance().recentFilesLimit + while (state.pathToTabName.size > limit) { + val (firstPath, _) = state.pathToTabName.asIterable().first() + state.pathToTabName.remove(firstPath) + } + + state.intIncrementModificationCount() + } + + class MyState : BaseState() { + @get:Tag("path-to-tab-name") + val pathToTabName by linkedMap() + } +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/editor/VcsLogVirtualFileSystem.kt b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/editor/VcsLogVirtualFileSystem.kt new file mode 100644 index 000000000000..712b746f1817 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/editor/VcsLogVirtualFileSystem.kt @@ -0,0 +1,39 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.vcs.log.ui.editor + +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.vcs.editor.ComplexPathVirtualFileSystem +import com.intellij.vcs.editor.GsonComplexPathSerializer +import com.intellij.vcs.log.VcsLogFilterCollection + +internal class VcsLogVirtualFileSystem : + ComplexPathVirtualFileSystem(GsonComplexPathSerializer(VcsLogComplexPath::class.java)) { + + override fun findOrCreateFile(project: Project, path: VcsLogComplexPath): VirtualFile { + return createVcsLogFile(path, null) + } + + fun createVcsLogFile(project: Project, tabId: String, filters: VcsLogFilterCollection?): VirtualFile { + return createVcsLogFile(VcsLogComplexPath(project.locationHash, project.locationHash, tabId), filters) + } + + private fun createVcsLogFile(pathId: VcsLogComplexPath, filters: VcsLogFilterCollection?): DefaultVcsLogFile { + return DefaultVcsLogFile(pathId, filters) + } + + override fun getProtocol(): String = Holder.PROTOCOL + + object Holder { + internal const val PROTOCOL = "vcs-log" + + @JvmStatic + fun getInstance() = service().getFileSystem(PROTOCOL) as VcsLogVirtualFileSystem + } + + data class VcsLogComplexPath(override val sessionId: String, + override val projectHash: String, + val logId: String) : ComplexPath +}