mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[terminal] IJPL-212254 Persist terminal tool window tabs
Listen for tool window tab manipulations, title and working directory changes and trigger persistence. Persistence is done periodically with 300 ms interval to protect from bursting updates. (cherry picked from commit 5ce6dfb967e04d1886aa5b3240f50f99aa0c46e2) IJ-CR-214214 GitOrigin-RevId: b4ad99c220dcdb19834a463ca39040e046f16f74
This commit is contained in:
committed by
intellij-monorepo-bot
parent
79c6b3f1c8
commit
62f28de625
+7
@@ -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<TerminalToolWindowTab> {
|
||||
return contentsRecursively.mapNotNull { it.getUserData(TerminalToolWindowTab.KEY) }
|
||||
}
|
||||
+153
@@ -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<Unit>(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<ContentWithListenersScope>()
|
||||
|
||||
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,
|
||||
)
|
||||
+1
-1
@@ -76,7 +76,7 @@ internal fun updateFileNameOnTitleChange(
|
||||
}
|
||||
}
|
||||
|
||||
private fun TerminalView.titleStateFlow(): Flow<TitleData> {
|
||||
internal fun TerminalView.titleStateFlow(): Flow<TitleData> {
|
||||
val terminalView = this
|
||||
|
||||
val titleStateFlow: Flow<TitleData> = title.stateFlow(
|
||||
|
||||
+20
-8
@@ -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<TerminalToolWindowTab>
|
||||
get() = getToolWindow().contentManager.contentsRecursively.mapNotNull { it.getUserData(TerminalToolWindowTab.KEY) }
|
||||
get() = getToolWindow().contentManager.getTerminalTabs()
|
||||
|
||||
private var tabsRestoredDeferred: Deferred<Unit> = 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<TerminalSessionPersistedTab> = TerminalTabsStorage.getInstance(manager.project).getStoredTabs()
|
||||
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
|
||||
restoreTabs(tabs, manager)
|
||||
}
|
||||
manager.tabsRestoredDeferred = manager.coroutineScope.async {
|
||||
val tabs: List<TerminalSessionPersistedTab> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user