From 79c6b3f1c8654ea481ef977b33e5d0a26331434a Mon Sep 17 00:00:00 2001 From: Konstantin Hudyakov Date: Thu, 16 Jul 2026 12:01:57 +0300 Subject: [PATCH] [terminal] IJPL-212254 Add an ability to listen for terminal process working directory updates Also, expose nio Path instead of plane native path string to avoid misuse. (cherry picked from commit e40073ba78fe429899a333081214ca88abf8fe2f) IJ-CR-214214 GitOrigin-RevId: b989cc23b9ac8d3dee75b715e8a400a9c5306641 --- .../terminal/frontend/view/TerminalView.kt | 16 ++++++++++++- .../TerminalToolWindowSplitContentProvider.kt | 15 +----------- .../frontend/view/impl/TerminalViewImpl.kt | 13 +++++++++- .../plugins/terminal/util/TerminalUtil.kt | 24 +++++++++++++++++++ 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/TerminalView.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/TerminalView.kt index 476dc885f417..c43a7296e844 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/TerminalView.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/TerminalView.kt @@ -2,6 +2,7 @@ package com.intellij.terminal.frontend.view import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataKey +import com.intellij.platform.eel.annotations.NativePath import com.intellij.terminal.TerminalTitle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -16,6 +17,7 @@ import org.jetbrains.plugins.terminal.view.TerminalOutputModelsSet import org.jetbrains.plugins.terminal.view.TerminalSendTextBuilder import org.jetbrains.plugins.terminal.view.shellIntegration.TerminalShellIntegration import java.awt.event.KeyEvent +import java.nio.file.Path import javax.swing.JComponent @ApiStatus.Internal @@ -98,6 +100,17 @@ interface TerminalView { */ val keyEventsFlow: Flow + /** + * Absolute [Path] of the current working directory of the terminal process. + * + * When [TerminalShellIntegration] is available, it is used to track the working directory. + * Otherwise, the OS-specific heuristics and polling approach will be used. + * + * Can be null if process is not started yet. + * Or if shell integration is not available and OS-specific heuristics fail to determine the working directory of the process. + */ + val workingDirectoryFlow: StateFlow + /** * Can be used to get or await the shell integration initialization. * @@ -144,7 +157,8 @@ interface TerminalView { * Returns null if the shell process is not connected yet to the [TerminalView] * or if the initial value is not yet received from the backend. */ - fun getCurrentDirectory(): String? + @Deprecated("Use workingDirectoryFlow instead.", ReplaceWith("workingDirectoryFlow.value")) + fun getCurrentDirectory(): @NativePath String? /** * A shortcut to schedule sending the specified text to the shell process diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/impl/TerminalToolWindowSplitContentProvider.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/impl/TerminalToolWindowSplitContentProvider.kt index 7cb106124c76..cf92f46162f7 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/impl/TerminalToolWindowSplitContentProvider.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/impl/TerminalToolWindowSplitContentProvider.kt @@ -1,12 +1,9 @@ package com.intellij.terminal.frontend.view.impl import com.intellij.openapi.project.Project -import com.intellij.platform.eel.path.EelPath -import com.intellij.platform.eel.provider.asNioPath import com.intellij.terminal.frontend.toolwindow.TerminalToolWindowTabsManager import com.intellij.terminal.frontend.toolwindow.findTabByContent import com.intellij.terminal.frontend.toolwindow.impl.shouldUseReworkedTerminal -import com.intellij.terminal.frontend.view.TerminalView import com.intellij.toolWindow.ToolWindowSplitContentProvider import com.intellij.ui.content.Content import org.jetbrains.plugins.terminal.TerminalOptionsProvider @@ -14,8 +11,6 @@ import org.jetbrains.plugins.terminal.TerminalTabState import org.jetbrains.plugins.terminal.TerminalToolWindowManager import org.jetbrains.plugins.terminal.fus.TerminalStartupFusInfo import org.jetbrains.plugins.terminal.fus.TerminalTabOpeningWay -import org.jetbrains.plugins.terminal.util.getNow -import java.nio.file.Path internal class TerminalToolWindowSplitContentProvider : ToolWindowSplitContentProvider { override fun createContentCopy(project: Project, content: Content): Content { @@ -32,7 +27,7 @@ internal class TerminalToolWindowSplitContentProvider : ToolWindowSplitContentPr private fun createReworkedTerminalContent(project: Project, content: Content, fusInfo: TerminalStartupFusInfo): Content { val manager = TerminalToolWindowTabsManager.getInstance(project) val originalView = manager.findTabByContent(content)?.view - val currentDirectory = originalView?.getCurrentDirectoryPath() + val currentDirectory = originalView?.workingDirectoryFlow?.value return manager.createTabBuilder() .workingDirectory(currentDirectory?.toString()) @@ -42,14 +37,6 @@ internal class TerminalToolWindowSplitContentProvider : ToolWindowSplitContentPr .content } - private fun TerminalView.getCurrentDirectoryPath(): Path? { - val currentDirectory = getCurrentDirectory() ?: return null - val session = sessionDeferred.getNow() ?: return null - return runCatching { - EelPath.parse(currentDirectory, session.eelDescriptor).asNioPath() - }.getOrNull() - } - private fun createClassicTerminalContent( project: Project, originalContent: Content, diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/impl/TerminalViewImpl.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/impl/TerminalViewImpl.kt index 5f8625f1d750..dd525e599c95 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/impl/TerminalViewImpl.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/impl/TerminalViewImpl.kt @@ -21,6 +21,7 @@ import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.wm.IdeFocusManager import com.intellij.platform.eel.provider.LocalEelDescriptor import com.intellij.platform.util.coroutines.childScope +import com.intellij.platform.util.coroutines.flow.mapStateIn import com.intellij.psi.PsiDocumentManager import com.intellij.terminal.JBTerminalSystemSettingsProviderBase import com.intellij.terminal.TerminalTitle @@ -91,6 +92,7 @@ import org.jetbrains.plugins.terminal.hyperlinks.session.TerminalHyperlinksSessi import org.jetbrains.plugins.terminal.session.TerminalGridSize import org.jetbrains.plugins.terminal.session.TerminalStartupOptions import org.jetbrains.plugins.terminal.session.impl.TerminalSession +import org.jetbrains.plugins.terminal.util.convertNativePathToNioPath import org.jetbrains.plugins.terminal.util.getNow import org.jetbrains.plugins.terminal.view.TerminalContentChangeEvent import org.jetbrains.plugins.terminal.view.TerminalCursorOffsetChangeEvent @@ -117,6 +119,7 @@ import java.awt.event.ComponentEvent import java.awt.event.FocusEvent import java.awt.event.FocusListener import java.awt.event.KeyEvent +import java.nio.file.Path import javax.swing.JComponent import javax.swing.JPanel import kotlin.coroutines.cancellation.CancellationException @@ -181,6 +184,8 @@ class TerminalViewImpl( override val keyEventsFlow: Flow = mutableKeyEventsFlow.asSharedFlow() private val inputInterceptors = ContainerUtil.createLockFreeCopyOnWriteList() + override val workingDirectoryFlow: StateFlow + override val shellIntegrationDeferred: CompletableDeferred = CompletableDeferred(coroutineScope.coroutineContext.job) override val startupOptionsDeferred: CompletableDeferred = @@ -191,6 +196,12 @@ class TerminalViewImpl( init { sessionModel = TerminalSessionModelImpl() + workingDirectoryFlow = sessionModel.terminalState.mapStateIn(coroutineScope.childScope("workingDirectoryFlow")) { + val directoryString = it.currentDirectory ?: return@mapStateIn null + val eelDescriptor = sessionDeferred.getNow()?.eelDescriptor ?: return@mapStateIn null + convertNativePathToNioPath(directoryString, eelDescriptor) + } + encodingManager = TerminalKeyEncodingManager(sessionModel, coroutineScope.childScope("TerminalKeyEncodingManager")) terminalInput = TerminalInput( @@ -750,7 +761,7 @@ class TerminalViewImpl( override fun toString(): String { val commandText = startupOptionsDeferred.getNow()?.let { "${it.shellCommand}" } - return "TerminalViewImpl(state=${sessionState.value}, command=$commandText, cwd=${getCurrentDirectory()})" + return "TerminalViewImpl(state=${sessionState.value}, command=$commandText, cwd=${workingDirectoryFlow.value})" } private inner class TerminalPanel(initialContent: Editor) : BorderLayoutPanel(), UiDataProvider, TerminalPanelMarker { diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/util/TerminalUtil.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/util/TerminalUtil.kt index d55117e5ed5f..72a2b8c24fd5 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/util/TerminalUtil.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/util/TerminalUtil.kt @@ -13,6 +13,11 @@ import com.intellij.openapi.keymap.Keymap import com.intellij.openapi.keymap.KeymapManager import com.intellij.openapi.keymap.ex.KeymapManagerEx import com.intellij.openapi.util.Disposer +import com.intellij.platform.eel.EelDescriptor +import com.intellij.platform.eel.annotations.NativePath +import com.intellij.platform.eel.path.EelPath +import com.intellij.platform.eel.path.EelPathException +import com.intellij.platform.eel.provider.asNioPath import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.concurrency.annotations.RequiresReadLockAbsence import com.intellij.util.io.awaitExit @@ -250,3 +255,22 @@ internal fun String?.toExistentNioDirectory(labelToLogOnFailure: String? = null) } return null } + +/** + * @return null if failed to convert [remotePath] to nio Path. + */ +@ApiStatus.Internal +fun convertNativePathToNioPath(remotePath: @NativePath String, descriptor: EelDescriptor): Path? { + val eelPath: EelPath = try { + EelPath.parse(remotePath, descriptor) + } + catch (_: EelPathException) { + return null + } + return try { + eelPath.asNioPath() + } + catch (_: IllegalArgumentException) { + null + } +}