diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalInternalUtils.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalInternalUtils.kt index 624b04cb69f8..16e3e655ca0e 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalInternalUtils.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalInternalUtils.kt @@ -4,10 +4,12 @@ import com.intellij.frontend.FrontendApplicationInfo import com.intellij.frontend.FrontendType import com.intellij.openapi.project.Project import com.intellij.openapi.util.NlsSafe +import com.intellij.terminal.frontend.toolwindow.TerminalToolWindowTab import com.intellij.terminal.frontend.toolwindow.TerminalToolWindowTabsManager import com.intellij.terminal.frontend.view.TerminalView import com.intellij.ui.ExperimentalUI import com.intellij.ui.content.ContentManager +import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.execution.ParametersListUtil import org.jetbrains.plugins.terminal.TerminalEngine import org.jetbrains.plugins.terminal.TerminalOptionsProvider @@ -72,4 +74,9 @@ internal fun TerminalView.getRunningProcessCommandLine(): String? { val currentBlock = shellIntegration.blocksModel.activeBlock as? TerminalCommandBlock ?: return null currentBlock.executedCommand ?: return null } +} + +@RequiresEdt +internal fun ContentManager.getTerminalTabs(): List { + return contentsRecursively.mapNotNull { it.getUserData(TerminalToolWindowTab.KEY) } } \ No newline at end of file diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalTabsPersistence.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalTabsPersistence.kt new file mode 100644 index 000000000000..df44312c8936 --- /dev/null +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalTabsPersistence.kt @@ -0,0 +1,153 @@ +package com.intellij.terminal.frontend.toolwindow.impl + +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.UI +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.diagnostic.fileLogger +import com.intellij.openapi.diagnostic.rethrowControlFlowException +import com.intellij.openapi.project.Project +import com.intellij.platform.util.coroutines.childScope +import com.intellij.terminal.frontend.toolwindow.TerminalToolWindowTab +import com.intellij.ui.content.Content +import com.intellij.ui.content.ContentManager +import com.intellij.ui.content.ContentManagerEvent +import com.intellij.ui.content.ContentManagerListener +import com.intellij.util.concurrency.annotations.RequiresEdt +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import org.jetbrains.plugins.terminal.settings.impl.TerminalSessionPersistedTab +import org.jetbrains.plugins.terminal.settings.impl.TerminalTabsStorage +import kotlin.io.path.pathString +import kotlin.time.Duration.Companion.milliseconds + +private val LOG = fileLogger() + +/** + * Watches the terminal tab changes in the [contentManager] and updates persisted tabs in [TerminalTabsStorage]. + */ +@OptIn(FlowPreview::class) +@RequiresEdt +internal fun installTerminalTabsPersistence( + project: Project, + contentManager: ContentManager, + coroutineScope: CoroutineScope, +) { + val updateRequestsFlow = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + coroutineScope.launch(Dispatchers.UI + ModalityState.any().asContextElement() + CoroutineName("persistTerminalTabs")) { + persistTerminalTabs(project, contentManager) + + updateRequestsFlow + .sample(300.milliseconds) + .collect { + persistTerminalTabs(project, contentManager) + } + } + + listenTerminalTabChangeEvents(contentManager, coroutineScope.childScope("listenTerminalTabChangeEvents")) { + updateRequestsFlow.tryEmit(Unit) + } +} + +@RequiresEdt +private fun persistTerminalTabs(project: Project, contentManager: ContentManager) { + try { + val tabs = contentManager.getTerminalTabs().map { computePersistedTab(it) } + TerminalTabsStorage.getInstance(project).updateStoredTabs(tabs) + } + catch (e: Exception) { + rethrowControlFlowException(e) + LOG.error("Error while persisting terminal tabs", e) + } +} + +@RequiresEdt +private fun computePersistedTab(tab: TerminalToolWindowTab): TerminalSessionPersistedTab { + val title = tab.view.title + val requestedProcessOptions = tab.processOptions + val processCurDirectory = tab.view.workingDirectoryFlow.value + // Prefer current directory of the running process + val workingDirectory = processCurDirectory?.pathString ?: requestedProcessOptions.workingDirectory + + return TerminalSessionPersistedTab( + name = title.userDefinedTitle ?: title.defaultTitle, + isUserDefinedName = title.userDefinedTitle != null, + shellCommand = requestedProcessOptions.shellCommand, + workingDirectory = workingDirectory, + envVariables = requestedProcessOptions.envVariables, + processType = requestedProcessOptions.processType, + ) +} + +/** + * Calls [onChange] when: + * 1. Terminal tabs are added or removed from the Terminal Tool Window + * 2. TerminalView title changes + * 3. TerminalView working directory changes + */ +@RequiresEdt +private fun listenTerminalTabChangeEvents( + contentManager: ContentManager, + coroutineScope: CoroutineScope, + onChange: () -> Unit, +) { + val contents = mutableListOf() + + fun addTerminalViewListeners(content: Content) { + val terminalView = content.getUserData(TerminalToolWindowTab.KEY)?.view ?: return // not a terminal tab + + val listenersScope = coroutineScope.childScope(terminalView.toString()) + listenersScope.launch { + terminalView.titleStateFlow().collect { + onChange() + } + } + listenersScope.launch { + terminalView.workingDirectoryFlow.collect { + onChange() + } + } + + contents.add(ContentWithListenersScope(content, listenersScope)) + } + + val listener = object : ContentManagerListener { + override fun contentAdded(event: ContentManagerEvent) { + onChange() + addTerminalViewListeners(event.content) + } + + override fun contentRemoved(event: ContentManagerEvent) { + onChange() + + // Cleanup listeners + val contentToScope = contents.find { it.content == event.content } + if (contentToScope != null) { + contentToScope.listenersScope.cancel() + contents.remove(contentToScope) + } + } + } + + for (content in contentManager.contentsRecursively) { + addTerminalViewListeners(content) + } + + contentManager.addRecursiveContentManagerListener(listener) + coroutineScope.coroutineContext.job.invokeOnCompletion { + contentManager.removeRecursiveContentManagerListener(listener) + } +} + +private data class ContentWithListenersScope( + val content: Content, + val listenersScope: CoroutineScope, +) diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalTitleUpdating.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalTitleUpdating.kt index 4bc7688e2a37..08aa87a3825c 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalTitleUpdating.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalTitleUpdating.kt @@ -76,7 +76,7 @@ internal fun updateFileNameOnTitleChange( } } -private fun TerminalView.titleStateFlow(): Flow { +internal fun TerminalView.titleStateFlow(): Flow { val terminalView = this val titleStateFlow: Flow = title.stateFlow( diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalToolWindowTabsManagerImpl.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalToolWindowTabsManagerImpl.kt index 89ecbeeecac6..e0d9cef2be6e 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalToolWindowTabsManagerImpl.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/toolwindow/impl/TerminalToolWindowTabsManagerImpl.kt @@ -34,6 +34,7 @@ import com.intellij.ui.content.ContentFactory import com.intellij.ui.content.ContentManager import com.intellij.util.AwaitCancellationAndInvoke import com.intellij.util.awaitCancellationAndInvoke +import com.intellij.util.concurrency.annotations.RequiresEdt import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -63,7 +64,7 @@ internal class TerminalToolWindowTabsManagerImpl( private val coroutineScope: CoroutineScope, ) : TerminalToolWindowTabsManager { override val tabs: List - get() = getToolWindow().contentManager.contentsRecursively.mapNotNull { it.getUserData(TerminalToolWindowTab.KEY) } + get() = getToolWindow().contentManager.getTerminalTabs() private var tabsRestoredDeferred: Deferred = CompletableDeferred(Unit) @@ -280,13 +281,24 @@ internal class TerminalToolWindowTabsManagerImpl( return toolWindow } + @RequiresEdt + private fun installTabsPersistence() { + val toolWindow = getToolWindow() + installTerminalTabsPersistence( + project = toolWindow.project, + contentManager = toolWindow.contentManager, + coroutineScope = coroutineScope.childScope("TerminalTabsPersistence") + ) + } + internal class Initializer : TerminalToolWindowInitializer { override fun initialize(toolWindow: ToolWindow) { val manager = TerminalToolWindowTabsManager.getInstance(toolWindow.project) as TerminalToolWindowTabsManagerImpl - if (shouldUseReworkedTerminal()) { + if (shouldUseReworkedTerminal() && TrustedProjects.isProjectTrusted(manager.project)) { scheduleTabsRestoring(manager) } + else manager.installTabsPersistence() TerminalAgentsAvailabilityService.getInstance(toolWindow.project).prewarm() @@ -312,12 +324,12 @@ internal class TerminalToolWindowTabsManagerImpl( } private fun scheduleTabsRestoring(manager: TerminalToolWindowTabsManagerImpl) { - if (TrustedProjects.isProjectTrusted(manager.project)) { - manager.tabsRestoredDeferred = manager.coroutineScope.async { - val tabs: List = TerminalTabsStorage.getInstance(manager.project).getStoredTabs() - withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) { - restoreTabs(tabs, manager) - } + manager.tabsRestoredDeferred = manager.coroutineScope.async { + val tabs: List = TerminalTabsStorage.getInstance(manager.project).getStoredTabs() + withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) { + restoreTabs(tabs, manager) + // Install tabs persistence after restoring already stored tabs to not override them accidentally with empty content. + manager.installTabsPersistence() } } }