[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
This commit is contained in:
Konstantin Hudyakov
2026-08-04 14:03:36 +00:00
committed by intellij-monorepo-bot
parent 8a5a5ac3de
commit 79c6b3f1c8
4 changed files with 52 additions and 16 deletions
@@ -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<TerminalKeyEvent>
/**
* 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<Path?>
/**
* 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
@@ -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,
@@ -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<TerminalKeyEvent> = mutableKeyEventsFlow.asSharedFlow()
private val inputInterceptors = ContainerUtil.createLockFreeCopyOnWriteList<TerminalInputInterceptor>()
override val workingDirectoryFlow: StateFlow<Path?>
override val shellIntegrationDeferred: CompletableDeferred<TerminalShellIntegration> =
CompletableDeferred(coroutineScope.coroutineContext.job)
override val startupOptionsDeferred: CompletableDeferred<TerminalStartupOptions> =
@@ -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 {
@@ -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
}
}