From 0381e3f989ef59067137796bacd52bafecb7cf29 Mon Sep 17 00:00:00 2001 From: Sergey Simonchik Date: Mon, 3 Nov 2025 00:07:57 +0100 Subject: [PATCH] [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 --- plugins/terminal/backend/BUILD.bazel | 2 + .../backend/intellij.terminal.backend.iml | 2 + .../terminal/backend/TerminalTabsManager.kt | 48 ++++++++++++++++--- .../terminal/LocalTerminalTtyConnector.java | 6 +++ .../plugins/terminal/terminalStartup.kt | 2 + 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/plugins/terminal/backend/BUILD.bazel b/plugins/terminal/backend/BUILD.bazel index 7283952d4710..2c7e9a0a475b 100644 --- a/plugins/terminal/backend/BUILD.bazel +++ b/plugins/terminal/backend/BUILD.bazel @@ -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 \ No newline at end of file diff --git a/plugins/terminal/backend/intellij.terminal.backend.iml b/plugins/terminal/backend/intellij.terminal.backend.iml index ed21d9fbb38f..c724f58ad37c 100644 --- a/plugins/terminal/backend/intellij.terminal.backend.iml +++ b/plugins/terminal/backend/intellij.terminal.backend.iml @@ -50,5 +50,7 @@ + + \ No newline at end of file diff --git a/plugins/terminal/backend/src/com/intellij/terminal/backend/TerminalTabsManager.kt b/plugins/terminal/backend/src/com/intellij/terminal/backend/TerminalTabsManager.kt index 85b5831f52b3..4e665bb56210 100644 --- a/plugins/terminal/backend/src/com/intellij/terminal/backend/TerminalTabsManager.kt +++ b/plugins/terminal/backend/src/com/intellij/terminal/backend/TerminalTabsManager.kt @@ -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()?.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 updateTabsAndStore(action: suspend (MutableMap) -> T): T { return tabsLock.withLock { try { diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/LocalTerminalTtyConnector.java b/plugins/terminal/src/org/jetbrains/plugins/terminal/LocalTerminalTtyConnector.java index d2832f46ef8d..13ad11ca1e58 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/LocalTerminalTtyConnector.java +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/LocalTerminalTtyConnector.java @@ -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) { diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/terminalStartup.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/terminalStartup.kt index a6fc63ea197a..044e64a86d93 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/terminalStartup.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/terminalStartup.kt @@ -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) {