[terminal] IJPL-214284 store host working directory per terminal tab, not remote path

For example, if a terminal is running in WSL, store `\\wsl.localhost\Ubuntu\path\to\project` (host path) as a terminal working directory, not `/path/to/project` (remote path).
Similarly, with DevContainers: store `/docker-87f454dd49a9/IdeaProjects/CursorVueDevContainer`, not `/IdeaProjects/CursorVueDevContainer`

This will allow detecting the terminal environment correctly when restoring the terminal tab on project open.


(cherry picked from commit a964ce9e22dc48085016a7ea371cf39aaf9637b5)

IJ-CR-180815

GitOrigin-RevId: 051a56eab8cc90dc9e7ec251dc044aa145a8b624
This commit is contained in:
Sergey Simonchik
2025-11-03 20:50:08 +00:00
committed by intellij-monorepo-bot
parent 6be296bc1c
commit 0381e3f989
5 changed files with 53 additions and 7 deletions
+2
View File
@@ -31,6 +31,8 @@ jvm_library(
"//platform/statistics",
"//platform/core-ui",
"//platform/lang-api:lang",
"//platform/eel",
"//platform/eel-provider",
]
)
### auto-generated section `build intellij.terminal.backend` end
@@ -50,5 +50,7 @@
<orderEntry type="module" module-name="intellij.platform.statistics" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.lang" />
<orderEntry type="module" module-name="intellij.platform.eel" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
</component>
</module>
@@ -5,21 +5,29 @@ import com.intellij.codeWithMe.ClientIdContextElement
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.platform.eel.EelDescriptor
import com.intellij.platform.eel.path.EelPath
import com.intellij.platform.eel.path.EelPathException
import com.intellij.platform.eel.provider.asNioPath
import com.intellij.platform.util.coroutines.childScope
import com.intellij.util.AwaitCancellationAndInvoke
import com.intellij.util.asSafely
import com.intellij.util.awaitCancellationAndInvoke
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.jetbrains.plugins.terminal.LocalTerminalTtyConnector
import org.jetbrains.plugins.terminal.ShellStartupOptions
import org.jetbrains.plugins.terminal.block.reworked.session.TerminalSessionTab
import org.jetbrains.plugins.terminal.session.impl.TerminalCloseEvent
import org.jetbrains.plugins.terminal.session.impl.TerminalStateChangedEvent
import java.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.pathString
@OptIn(AwaitCancellationAndInvoke::class)
@Service(Service.Level.PROJECT)
@@ -89,7 +97,9 @@ internal class TerminalTabsManager(private val project: Project, private val cor
)
tabs[tabId] = updatedTab
trackWorkingDirectory(updatedTab, scope.childScope("Working directory tracking"))
// to be replaced with another way of getting EelDescriptor
val eelDescriptor = result.ttyConnector.connector.asSafely<LocalTerminalTtyConnector>()?.eelDescriptor
trackWorkingDirectory(updatedTab, eelDescriptor, scope.childScope("Working directory tracking"))
scope.awaitCancellationAndInvoke {
updateTabsAndStore { tabs ->
@@ -143,7 +153,7 @@ internal class TerminalTabsManager(private val project: Project, private val cor
* So, the working directory is persisted in the [TerminalTabsStorage]
* and can be used to start the new session on the next IDE launch.
*/
private fun trackWorkingDirectory(tab: TerminalSessionTab, coroutineScope: CoroutineScope) {
private fun trackWorkingDirectory(tab: TerminalSessionTab, eelDescriptor: EelDescriptor?, coroutineScope: CoroutineScope) {
val sessionId = tab.sessionId ?: error("This method should be called only for tabs with started sessions: $tab")
val session = TerminalSessionsManager.getInstance().getSession(sessionId) ?: error("No session for tab $tab")
@@ -153,11 +163,14 @@ internal class TerminalTabsManager(private val project: Project, private val cor
var currentDirectory: String? = tab.workingDirectory
outputFlow.collect { events ->
for (event in events) {
if (event is TerminalStateChangedEvent && event.state.currentDirectory != currentDirectory) {
currentDirectory = event.state.currentDirectory
updateTabsAndStore { tabs ->
val updatedTab = tabs[tab.id]?.copy(workingDirectory = currentDirectory) ?: return@updateTabsAndStore
tabs[tab.id] = updatedTab
if (event is TerminalStateChangedEvent) {
val newCurrentDirectory = convertRemoteDirectoryToHost(event.state.currentDirectory, eelDescriptor)
if (newCurrentDirectory != currentDirectory) {
currentDirectory = newCurrentDirectory
updateTabsAndStore { tabs ->
val updatedTab = tabs[tab.id]?.copy(workingDirectory = newCurrentDirectory) ?: return@updateTabsAndStore
tabs[tab.id] = updatedTab
}
}
}
}
@@ -165,6 +178,27 @@ internal class TerminalTabsManager(private val project: Project, private val cor
}
}
private fun convertRemoteDirectoryToHost(remotePath: String, descriptor: EelDescriptor?): String {
if (descriptor == null) return remotePath
val eelPath: EelPath = try {
EelPath.parse(remotePath, descriptor)
}
catch (e: EelPathException) {
LOG.debug(e) {
"Failed to convert remote path ($remotePath) to host: descriptor=$descriptor"
}
return remotePath
}
return try {
eelPath.asNioPath().pathString
}
catch (e: IllegalArgumentException) {
LOG.info("Failed to convert remote path ($remotePath) to host: " +
"EelPath($eelPath) -> Nio conversion failed, descriptor=$descriptor", e)
remotePath
}
}
private suspend fun <T> updateTabsAndStore(action: suspend (MutableMap<Int, TerminalSessionTab>) -> T): T {
return tabsLock.withLock {
try {
@@ -4,6 +4,7 @@ package org.jetbrains.plugins.terminal;
import com.google.common.base.Ascii;
import com.intellij.execution.ijent.IjentChildPtyProcessAdapter;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.platform.eel.EelDescriptor;
import com.intellij.terminal.pty.PtyProcessTtyConnector;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.jediterm.core.util.TermSize;
@@ -30,6 +31,11 @@ public class LocalTerminalTtyConnector extends PtyProcessTtyConnector {
myShellProcessHolder = shellProcessHolder;
}
@ApiStatus.Internal
public @NotNull EelDescriptor getEelDescriptor() {
return myShellProcessHolder.getDescriptor();
}
@Override
public void close() {
if (myProcess instanceof UnixPtyProcess) {
@@ -236,6 +236,8 @@ internal class ShellProcessHolder(
val ptyProcess: PtyProcess = eelProcess.convertToJavaProcess() as PtyProcess
private val shellPid: EelApi.Pid = eelProcess.pid
val descriptor: EelDescriptor get() = eelApi.descriptor
fun terminatePosixShell() {
terminalApplicationScope().launch(Dispatchers.IO) {
if (!ptyProcess.isAlive) {