Selaa lähdekoodia

Merge pull request #56 from windchargerj/feature-extension_toolkit_host

Fix compatibility issues with extension to XMake toolkit host
ruki 1 vuosi sitten
vanhempi
commit
59d5bd8034

+ 6 - 17
src/main/kotlin/io/xmake/actions/UpdateCmakeListsAction.kt

@@ -12,9 +12,8 @@ import io.xmake.project.xmakeConsoleView
 import io.xmake.shared.xmakeConfiguration
 import io.xmake.utils.SystemUtils
 import io.xmake.utils.exception.XMakeRunConfigurationNotSetException
-import io.xmake.utils.execute.SyncDirection
-import io.xmake.utils.execute.syncFileByToolkit
-import kotlinx.coroutines.GlobalScope
+import io.xmake.utils.execute.fetchGeneratedFile
+import io.xmake.utils.execute.syncBeforeFetch
 
 class UpdateCmakeListsAction : AnAction() {
     override fun actionPerformed(e: AnActionEvent) {
@@ -31,6 +30,8 @@ class UpdateCmakeListsAction : AnAction() {
                 SystemUtils.runvInConsole(project, xmakeConfiguration.configurationCommandLine)
                     ?.addProcessListener(object : ProcessAdapter() {
                         override fun processTerminated(e: ProcessEvent) {
+                            syncBeforeFetch(project, project.activatedToolkit!!)
+
                             SystemUtils.runvInConsole(
                                 project,
                                 xmakeConfiguration.updateCmakeListsCommandLine,
@@ -40,13 +41,7 @@ class UpdateCmakeListsAction : AnAction() {
                             )?.addProcessListener(
                                 object : ProcessAdapter() {
                                     override fun processTerminated(e: ProcessEvent) {
-                                        syncFileByToolkit(
-                                            GlobalScope,
-                                            project,
-                                            project.activatedToolkit!!,
-                                            "CMakeLists.txt",
-                                            SyncDirection.UPSTREAM_TO_LOCAL
-                                        )
+                                        fetchGeneratedFile(project, project.activatedToolkit!!, "CMakeLists.txt")
                                         // Todo: Reload from disks after download from remote.
                                     }
                                 }
@@ -59,13 +54,7 @@ class UpdateCmakeListsAction : AnAction() {
                     ?.addProcessListener(
                         object : ProcessAdapter() {
                             override fun processTerminated(e: ProcessEvent) {
-                                syncFileByToolkit(
-                                    GlobalScope,
-                                    project,
-                                    project.activatedToolkit!!,
-                                    "CMakeLists.txt",
-                                    SyncDirection.UPSTREAM_TO_LOCAL
-                                )
+                                fetchGeneratedFile(project, project.activatedToolkit!!, "CMakeLists.txt")
                             }
                         }
                     )

+ 7 - 14
src/main/kotlin/io/xmake/actions/UpdateCompileCommandsAction.kt

@@ -12,9 +12,8 @@ import io.xmake.project.xmakeConsoleView
 import io.xmake.shared.xmakeConfiguration
 import io.xmake.utils.SystemUtils
 import io.xmake.utils.exception.XMakeRunConfigurationNotSetException
-import io.xmake.utils.execute.SyncDirection
-import io.xmake.utils.execute.syncFileByToolkit
-import kotlinx.coroutines.GlobalScope
+import io.xmake.utils.execute.fetchGeneratedFile
+import io.xmake.utils.execute.syncBeforeFetch
 
 class UpdateCompileCommandsAction : AnAction() {
     override fun actionPerformed(e: AnActionEvent) {
@@ -31,6 +30,8 @@ class UpdateCompileCommandsAction : AnAction() {
                 SystemUtils.runvInConsole(project, xmakeConfiguration.configurationCommandLine)
                     ?.addProcessListener(object : ProcessAdapter() {
                         override fun processTerminated(e: ProcessEvent) {
+                            syncBeforeFetch(project, project.activatedToolkit!!)
+
                             SystemUtils.runvInConsole(
                                 project,
                                 xmakeConfiguration.updateCompileCommansLine,
@@ -41,12 +42,10 @@ class UpdateCompileCommandsAction : AnAction() {
                                 ?.addProcessListener(
                                     object : ProcessAdapter() {
                                         override fun processTerminated(e: ProcessEvent) {
-                                            syncFileByToolkit(
-                                                GlobalScope,
+                                            fetchGeneratedFile(
                                                 project,
                                                 project.activatedToolkit!!,
-                                                "compile_commands.json",
-                                                SyncDirection.UPSTREAM_TO_LOCAL
+                                                "compile_commands.json"
                                             )
                                             // Todo: Reload from disks after download from remote.
                                         }
@@ -60,13 +59,7 @@ class UpdateCompileCommandsAction : AnAction() {
                     ?.addProcessListener(
                         object : ProcessAdapter() {
                             override fun processTerminated(e: ProcessEvent) {
-                                syncFileByToolkit(
-                                    GlobalScope,
-                                    project,
-                                    project.activatedToolkit!!,
-                                    "compile_commands.json",
-                                    SyncDirection.UPSTREAM_TO_LOCAL
-                                )
+                                fetchGeneratedFile(project, project.activatedToolkit!!, "compile_commands.json")
                             }
                         }
                     )

+ 3 - 3
src/main/kotlin/io/xmake/icons/XMakeIcons.kt

@@ -1,6 +1,6 @@
 package io.xmake.icons
 
-import com.intellij.icons.ExpUiIcons
+import com.intellij.icons.AllIcons
 import com.intellij.openapi.util.IconLoader
 import javax.swing.Icon
 
@@ -13,10 +13,10 @@ object XMakeIcons {
     val FILE = load("/icons/xmake.svg")
 
     // error icon
-    val ERROR = ExpUiIcons.Status.Error
+    val ERROR = AllIcons.General.Error
 
     // warning icon
-    val WARNING = ExpUiIcons.Status.Warning
+    val WARNING = AllIcons.General.Warning
 
     private fun load(path: String): Icon = IconLoader.getIcon(path, XMakeIcons::class.java)
 

+ 19 - 39
src/main/kotlin/io/xmake/project/XMakeDirectoryProjectGenerator.kt

@@ -3,7 +3,6 @@ package io.xmake.project
 import com.intellij.execution.RunManager
 import com.intellij.execution.configurations.GeneralCommandLine
 import com.intellij.execution.process.ProcessNotCreatedException
-import com.intellij.execution.wsl.WSLDistribution
 import com.intellij.ide.util.projectWizard.AbstractNewProjectStep
 import com.intellij.ide.util.projectWizard.CustomStepProjectGenerator
 import com.intellij.openapi.diagnostic.logger
@@ -15,12 +14,13 @@ import com.intellij.openapi.wm.impl.welcomeScreen.AbstractActionWithPanel
 import com.intellij.platform.DirectoryProjectGenerator
 import com.intellij.platform.DirectoryProjectGeneratorBase
 import com.intellij.platform.ProjectGeneratorPeer
-import com.intellij.ssh.config.unified.SshConfig
 import io.xmake.icons.XMakeIcons
-import io.xmake.project.toolkit.ToolkitHostType.*
 import io.xmake.run.XMakeRunConfiguration
 import io.xmake.run.XMakeRunConfigurationType
-import io.xmake.utils.execute.*
+import io.xmake.utils.execute.SyncDirection
+import io.xmake.utils.execute.createProcess
+import io.xmake.utils.execute.runProcess
+import io.xmake.utils.execute.transferFolderByToolkit
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
@@ -48,15 +48,9 @@ class XMakeDirectoryProjectGenerator :
          */
         val tmpdir = "$contentEntryPath.dir"
 
-        val dir = when (data.toolkit!!.host.type) {
-            LOCAL -> tmpdir
-            WSL, SSH -> data.remotePath!!
-        }
+        val dir = if (!data.toolkit!!.isOnRemote) tmpdir else data.remotePath!!
 
-        val workingDir = when (data.toolkit.host.type) {
-            LOCAL -> contentEntryPath
-            WSL, SSH -> data.remotePath!!
-        }
+        val workingDir = if (!data.toolkit.isOnRemote) contentEntryPath else data.remotePath!!
 
         Log.debug("dir: $dir")
 
@@ -87,34 +81,20 @@ class XMakeDirectoryProjectGenerator :
         Log.info("results: $results")
 
         with(data.toolkit) {
-            when (host.type) {
-                io.xmake.project.toolkit.ToolkitHostType.LOCAL -> {
-                    val tmpFile = File(tmpdir)
-                    if (tmpFile.exists()) {
-                        tmpFile.copyRecursively(File(contentEntryPath), true)
-                        tmpFile.deleteRecursively()
-                    }
-                }
-
-                io.xmake.project.toolkit.ToolkitHostType.WSL -> {
-                    syncProjectByWslSync(
-                        scope,
-                        project,
-                        host.target as WSLDistribution,
-                        data.remotePath!!,
-                        SyncDirection.UPSTREAM_TO_LOCAL
-                    )
-                }
-
-                io.xmake.project.toolkit.ToolkitHostType.SSH -> {
-                    syncProjectBySftp(
-                        scope,
-                        project,
-                        host.target as SshConfig,
-                        data.remotePath!!,
-                        SyncDirection.UPSTREAM_TO_LOCAL
-                    )
+            if (!isOnRemote) {
+                val tmpFile = File(tmpdir)
+                if (tmpFile.exists()) {
+                    tmpFile.copyRecursively(File(contentEntryPath), true)
+                    tmpFile.deleteRecursively()
                 }
+            } else {
+                transferFolderByToolkit(
+                    project,
+                    this,
+                    SyncDirection.UPSTREAM_TO_LOCAL,
+                    directoryPath = data.remotePath!!,
+                    null
+                )
             }
         }
 

+ 20 - 25
src/main/kotlin/io/xmake/project/XMakeModuleBuilder.kt

@@ -3,7 +3,6 @@ package io.xmake.project
 import com.intellij.execution.RunManager
 import com.intellij.execution.configurations.GeneralCommandLine
 import com.intellij.execution.process.ProcessNotCreatedException
-import com.intellij.execution.wsl.WSLDistribution
 import com.intellij.ide.util.projectWizard.ModuleBuilder
 import com.intellij.ide.util.projectWizard.ModuleWizardStep
 import com.intellij.ide.util.projectWizard.WizardContext
@@ -15,11 +14,12 @@ import com.intellij.openapi.roots.ModifiableRootModel
 import com.intellij.openapi.util.Disposer
 import com.intellij.openapi.util.io.FileUtil
 import com.intellij.openapi.vfs.LocalFileSystem
-import com.intellij.ssh.config.unified.SshConfig
-import io.xmake.project.toolkit.ToolkitHostType.*
 import io.xmake.run.XMakeRunConfiguration
 import io.xmake.run.XMakeRunConfigurationType
-import io.xmake.utils.execute.*
+import io.xmake.utils.execute.SyncDirection
+import io.xmake.utils.execute.createProcess
+import io.xmake.utils.execute.runProcess
+import io.xmake.utils.execute.transferFolderByToolkit
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
@@ -52,15 +52,10 @@ class XMakeModuleBuilder : ModuleBuilder() {
          */
         val tmpdir = "$contentEntryPath.dir"
 
-        val dir = when(configurationData.toolkit!!.host.type) {
-            LOCAL -> tmpdir
-            WSL, SSH -> configurationData.remotePath!!
-        }
+        val dir = if (!configurationData.toolkit!!.isOnRemote) tmpdir else configurationData.remotePath!!
 
-        val workingDir = when(configurationData.toolkit!!.host.type) {
-            LOCAL -> contentEntryPath
-            WSL, SSH -> configurationData.remotePath!!
-        }
+        val workingDir =
+            if (!configurationData.toolkit!!.isOnRemote) contentEntryPath else configurationData.remotePath!!
 
         Log.debug("dir: $dir")
 
@@ -91,20 +86,20 @@ class XMakeModuleBuilder : ModuleBuilder() {
         Log.info("results: $results")
 
         with(configurationData.toolkit!!) {
-            when(host.type) {
-                LOCAL -> {
-                    val tmpFile = File(tmpdir)
-                    if (tmpFile.exists()) {
-                        tmpFile.copyRecursively(File(contentEntryPath), true)
-                        tmpFile.deleteRecursively()
-                    }
-                }
-                WSL -> {
-                    syncProjectByWslSync(scope, rootModel.project, host.target as WSLDistribution, configurationData.remotePath!!, SyncDirection.UPSTREAM_TO_LOCAL)
-                }
-                SSH -> {
-                    syncProjectBySftp(scope, rootModel.project, host.target as SshConfig, configurationData.remotePath!!, SyncDirection.UPSTREAM_TO_LOCAL)
+            if (!isOnRemote) {
+                val tmpFile = File(tmpdir)
+                if (tmpFile.exists()) {
+                    tmpFile.copyRecursively(File(contentEntryPath), true)
+                    tmpFile.deleteRecursively()
                 }
+            } else {
+                transferFolderByToolkit(
+                    rootModel.project,
+                    this,
+                    SyncDirection.UPSTREAM_TO_LOCAL,
+                    directoryPath = configurationData.remotePath!!,
+                    null
+                )
             }
         }
 

+ 1 - 3
src/main/kotlin/io/xmake/project/XMakeSdkSettingsStep.kt

@@ -9,8 +9,6 @@ import com.intellij.openapi.roots.ModifiableRootModel
 import com.intellij.openapi.util.Disposer
 import com.intellij.ui.dsl.builder.panel
 import com.intellij.util.ui.JBEmptyBorder
-import io.xmake.project.toolkit.ToolkitHostType.SSH
-import io.xmake.project.toolkit.ToolkitHostType.WSL
 import javax.swing.JComponent
 
 @Deprecated("Please refer to the relevant content in folder io/xmake/project/wizard.")
@@ -53,7 +51,7 @@ class XMakeSdkSettingsStep(
             }
 
             // Todo: Check whether working directory is valid.
-            if ((toolkit.host.type == WSL || toolkit.host.type == SSH) && remotePath.isNullOrBlank()) {
+            if ((toolkit.isOnRemote) && remotePath.isNullOrBlank()) {
                 throw RuntimeConfigurationError("Working directory is not set!")
             }
         }

+ 11 - 31
src/main/kotlin/io/xmake/project/directory/ui/DirectoryBrowser.kt

@@ -3,27 +3,23 @@ package io.xmake.project.directory.ui
 import com.intellij.execution.wsl.WSLDistribution
 import com.intellij.execution.wsl.ui.browseWslPath
 import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.extensions.ExtensionPointName
 import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.ui.TextComponentAccessor
 import com.intellij.openapi.ui.TextFieldWithBrowseButton
-import com.intellij.ssh.ConnectionBuilder
-import com.intellij.ssh.config.unified.SshConfig
-import com.intellij.ssh.interaction.PlatformSshPasswordProvider
-import com.intellij.ssh.ui.sftpBrowser.RemoteBrowserDialog
-import com.intellij.ssh.ui.sftpBrowser.SftpRemoteBrowserProvider
 import io.xmake.project.toolkit.Toolkit
 import io.xmake.project.toolkit.ToolkitHost
 import io.xmake.project.toolkit.ToolkitHostType.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
+import io.xmake.utils.extension.ToolkitHostExtension
 import java.awt.event.ActionListener
 
-
 class DirectoryBrowser(val project: Project?) : TextFieldWithBrowseButton() {
 
     private val listeners = mutableSetOf<ActionListener>()
 
+    private val EP_NAME: ExtensionPointName<ToolkitHostExtension> = ExtensionPointName("io.xmake.toolkitHostExtension")
+
     private fun createLocalBrowseListener(): ActionListener {
         val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
         val browseFolderListener = BrowseFolderActionListener(
@@ -68,12 +64,14 @@ class DirectoryBrowser(val project: Project?) : TextFieldWithBrowseButton() {
                 listeners.add(wslBrowseListener)
                 Log.debug("addActionListener wsl: $wslBrowseListener")
             }
-
             SSH -> {
-                val sftpBrowseListener = createSftpBrowseListener(host.target as SshConfig)
-                addActionListener(sftpBrowseListener)
-                listeners.add(sftpBrowseListener)
-                Log.debug("addActionListener remote: $sftpBrowseListener")
+                EP_NAME.extensions.first { it.KEY == "SSH" }.let { extension ->
+                    println("host: $host")
+                    val browseListener = with(extension) { createBrowseListener(host) }
+                    addActionListener(browseListener)
+                    listeners.add(browseListener)
+                    Log.debug("addActionListener ${extension.getHostType()}: $browseListener")
+                }
             }
         }
     }
@@ -88,21 +86,3 @@ class DirectoryBrowser(val project: Project?) : TextFieldWithBrowseButton() {
     }
 }
 
-fun DirectoryBrowser.createSftpBrowseListener(target: SshConfig): ActionListener {
-    val sftpChannel = runBlocking(Dispatchers.Default) {
-        ConnectionBuilder(target.host)
-            .withSshPasswordProvider(PlatformSshPasswordProvider(target.copyToCredentials()))
-            .openFailSafeSftpChannel()
-    }
-    val sftpRemoteBrowserProvider = SftpRemoteBrowserProvider(sftpChannel)
-    val remoteBrowseFolderListener = ActionListener {
-        text = RemoteBrowserDialog(
-            sftpRemoteBrowserProvider,
-            project,
-            true,
-            withCreateDirectoryButton = true
-        ).apply { showAndGet() }.getResult()
-    }
-    return remoteBrowseFolderListener
-}
-

+ 12 - 10
src/main/kotlin/io/xmake/project/toolkit/ToolkitHost.kt

@@ -2,13 +2,13 @@ package io.xmake.project.toolkit
 
 import com.intellij.execution.wsl.WSLDistribution
 import com.intellij.execution.wsl.WslDistributionManager
+import com.intellij.openapi.extensions.ExtensionPointName
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.util.SystemInfo
-import com.intellij.ssh.config.unified.SshConfig
-import com.intellij.ssh.config.unified.SshConfigManager
 import com.intellij.util.xmlb.annotations.Attribute
 import com.intellij.util.xmlb.annotations.Tag
 import io.xmake.project.toolkit.ToolkitHostType.*
+import io.xmake.utils.extension.ToolkitHostExtension
 import kotlinx.coroutines.coroutineScope
 
 @Tag("toolkitHost")
@@ -17,27 +17,33 @@ data class ToolkitHost(
     val type: ToolkitHostType = LOCAL,
 ) {
 
+    private val EP_NAME: ExtensionPointName<ToolkitHostExtension> =
+        ExtensionPointName("io.xmake.toolkitHostExtension")
+
     constructor(type: ToolkitHostType, target: Any? = null) : this(type) {
         this.target = target
         this.id = when (type) {
             LOCAL -> SystemInfo.getOsName()
             WSL -> (target as WSLDistribution).id
-            SSH -> (target as SshConfig).id
+            SSH -> EP_NAME.extensions.first { it.KEY == "SSH" }.getTargetId(target)
         }
     }
 
     @Transient
     var target: Any? = null
-        private set
 
     @Attribute
-    private var id: String? = null
+    var id: String? = null
 
     suspend fun loadTarget(project: Project? = null) {
         when (type) {
             LOCAL -> {}
             WSL -> loadWslTarget()
-            SSH -> loadSshTarget(project)
+            SSH -> {
+                with(EP_NAME.extensions.first { it.KEY == "SSH" }) {
+                    loadTargetX(project)
+                }
+            }
         }
     }
 
@@ -45,10 +51,6 @@ data class ToolkitHost(
         target = WslDistributionManager.getInstance().installedDistributions.find { it.id == id }!!
     }
 
-    private suspend fun loadSshTarget(project: Project? = null) = coroutineScope {
-        target = SshConfigManager.getInstance(project).findConfigById(id!!)!!
-    }
-
     override fun toString(): String {
         return "ToolkitHost(type=$type, id=$id)"
     }

+ 22 - 14
src/main/kotlin/io/xmake/project/toolkit/ToolkitManager.kt

@@ -7,16 +7,15 @@ import com.intellij.execution.wsl.WSLUtil
 import com.intellij.execution.wsl.WslDistributionManager
 import com.intellij.openapi.components.*
 import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.extensions.ExtensionPointName
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.project.ProjectManager
 import com.intellij.openapi.util.SystemInfo
-import com.intellij.ssh.config.unified.SshConfig
-import com.intellij.ssh.config.unified.SshConfigManager
-import com.intellij.util.PlatformUtils
 import com.intellij.util.xmlb.annotations.XCollection
 import io.xmake.project.toolkit.ToolkitHostType.*
 import io.xmake.run.XMakeRunConfiguration
 import io.xmake.utils.execute.*
+import io.xmake.utils.extension.ToolkitHostExtension
 import kotlinx.coroutines.*
 import kotlinx.coroutines.flow.*
 import java.util.*
@@ -25,6 +24,9 @@ import java.util.*
 @State(name = "toolkits", storages = [Storage("xmakeToolkits.xml")])
 class ToolkitManager(private val scope: CoroutineScope) : PersistentStateComponent<ToolkitManager.State> {
 
+    private val EP_NAME: ExtensionPointName<ToolkitHostExtension> =
+        ExtensionPointName("io.xmake.toolkitHostExtension")
+
     val fetchedToolkitsSet = mutableSetOf<Toolkit>()
     private lateinit var detectionJob: Job
     private lateinit var validateJob: Job
@@ -53,18 +55,18 @@ class ToolkitManager(private val scope: CoroutineScope) : PersistentStateCompone
 
     private fun toolkitHostFlow(project: Project? = null): Flow<ToolkitHost> = flow {
         val wslDistributions = scope.async { WslDistributionManager.getInstance().installedDistributions }
-        val sshConfigs = scope.async { SshConfigManager.getInstance(project).configs }
 
         emit(ToolkitHost(LOCAL).also { host -> Log.info("emit host: $host") })
+
         if (WSLUtil.isSystemCompatible()) {
             wslDistributions.await().forEach {
                 emit(ToolkitHost(WSL, it).also { host -> Log.info("emit host: $host") })
             }
         }
 
-        if (PlatformUtils.isCommercialEdition()) {
-            sshConfigs.await().forEach {
-                emit(ToolkitHost(SSH, it).also { host -> Log.info("emit host: $host") })
+        EP_NAME.extensions.filter { it.KEY == "SSH" }.forEach {
+            it.getToolkitHosts(project).forEach {
+                emit(it).also { host -> Log.info("emit host: $host") }
             }
         }
     }
@@ -74,7 +76,7 @@ class ToolkitManager(private val scope: CoroutineScope) : PersistentStateCompone
             when (host.type) {
                 LOCAL -> (if (SystemInfo.isWindows) probeXmakeLocCommandOnWin else it).createLocalProcess()
                 WSL -> it.createWslProcess(host.target as WSLDistribution)
-                SSH -> it.createSshProcess(host.target as SshConfig)
+                SSH -> with(EP_NAME.extensions.first { it.KEY == "SSH" }) { it.createProcess(host) }
             }
         }
 
@@ -93,7 +95,7 @@ class ToolkitManager(private val scope: CoroutineScope) : PersistentStateCompone
             when (host.type) {
                 LOCAL -> it.createLocalProcess()
                 WSL -> it.createWslProcess(host.target as WSLDistribution)
-                SSH -> it.createSshProcess(host.target as SshConfig)
+                SSH -> with(EP_NAME.extensions.first { it.KEY == "SSH" }) { it.createProcess(host) }
             }
         }
         val (stdout, exitCode) = runProcess(process)
@@ -135,9 +137,8 @@ class ToolkitManager(private val scope: CoroutineScope) : PersistentStateCompone
                         }
 
                         SSH -> {
-                            val sshConfig = (host.target as SshConfig)
-                            val name = sshConfig.presentableShortName
-                            Toolkit(name, host, path, versionString)
+                            EP_NAME.extensions.first { it.KEY == "SSH" }
+                                .createToolkit(host, path, versionString)
                         }
                     }.apply { this.isRegistered = true; this.isValid = true }
                 }
@@ -234,11 +235,18 @@ class ToolkitManager(private val scope: CoroutineScope) : PersistentStateCompone
     }
 
     fun getRegisteredToolkits(): List<Toolkit> {
-        return state.registeredToolkits.filterNot { (it.host.type == SSH && PlatformUtils.isCommunityEdition()) }
+        return state.registeredToolkits.filter { toolkit ->
+            !toolkit.isOnRemote ||
+                    EP_NAME.extensions.filter { it.KEY == "SSH" }.fold(true) { acc, sshExtension ->
+                        acc || sshExtension.filterRegistered()(toolkit)
+                    }
+        }
+//            .filterNot { (it.host.type == SSH && PlatformUtils.isCommunityEdition()) }
     }
 
     companion object {
-        fun getInstance(): ToolkitManager = serviceOrNull() ?: throw IllegalStateException()
         private val Log = logger<ToolkitManager>()
+
+        fun getInstance(): ToolkitManager = serviceOrNull() ?: throw IllegalStateException()
     }
 }

+ 17 - 34
src/main/kotlin/io/xmake/project/wizard/XMakeProjectWizardStep.kt

@@ -3,7 +3,6 @@ package io.xmake.project.wizard
 import com.intellij.execution.RunManager
 import com.intellij.execution.configurations.GeneralCommandLine
 import com.intellij.execution.process.ProcessNotCreatedException
-import com.intellij.execution.wsl.WSLDistribution
 import com.intellij.ide.util.projectWizard.ModuleBuilder
 import com.intellij.ide.util.projectWizard.WizardContext
 import com.intellij.ide.wizard.AbstractNewProjectWizardStep
@@ -23,7 +22,6 @@ import com.intellij.openapi.ui.shortenTextWithEllipsis
 import com.intellij.openapi.ui.validation.*
 import com.intellij.openapi.vfs.VfsUtil
 import com.intellij.project.stateStore
-import com.intellij.ssh.config.unified.SshConfig
 import com.intellij.ui.UIBundle
 import com.intellij.ui.dsl.builder.*
 import com.intellij.ui.util.getTextWidth
@@ -38,8 +36,10 @@ import io.xmake.project.toolkit.ui.ToolkitComboBox.Companion.forToolkitComboBox
 import io.xmake.project.wizard.XMakeNewProjectWizardData.Companion.xmakeData
 import io.xmake.run.XMakeRunConfiguration
 import io.xmake.run.XMakeRunConfigurationType
-import io.xmake.utils.execute.*
-import kotlinx.coroutines.CoroutineScope
+import io.xmake.utils.execute.SyncDirection
+import io.xmake.utils.execute.createProcess
+import io.xmake.utils.execute.runProcess
+import io.xmake.utils.execute.transferFolderByToolkit
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import java.io.File
@@ -50,8 +50,8 @@ import kotlin.io.path.Path
 class XMakeProjectWizardStep(parent: NewProjectWizardBaseStep) :
     AbstractNewProjectWizardStep(parent),
     XMakeNewProjectWizardData {
+
     private val toolkitManager = ToolkitManager.getInstance()
-    private val scope = CoroutineScope(Dispatchers.IO)
 
     override val nameProperty: GraphProperty<String> = baseData!!.nameProperty
     override val pathProperty: GraphProperty<String> = baseData!!.pathProperty
@@ -66,8 +66,8 @@ class XMakeProjectWizardStep(parent: NewProjectWizardBaseStep) :
     private val isOnRemoteProperty: GraphProperty<Boolean> =
         propertyGraph.lazyProperty { toolkit?.isOnRemote == true }
 
-    override var name: String = baseData!!.name
-    override var path: String = baseData!!.path
+    override var name: String by nameProperty
+    override var path: String by pathProperty
     override var remotePath: String by remotePathProperty
     override var language: String by languagesProperty
     override var kind: String by kindsProperty
@@ -104,7 +104,6 @@ class XMakeProjectWizardStep(parent: NewProjectWizardBaseStep) :
     private val browser = DirectoryBrowser(context.project)
     private val toolkitComboBox = ToolkitComboBox(::toolkit)
 
-    @Suppress("UnstableApiUsage")
     override fun setupUI(builder: Panel) {
         val locationProperty = remotePathProperty.joinCanonicalPath(nameProperty)
         with(builder) {
@@ -165,14 +164,14 @@ class XMakeProjectWizardStep(parent: NewProjectWizardBaseStep) :
 
     override fun setupProject(project: Project) {
         if (context.isCreatingNewProject) {
-            val workingDirectory = when (toolkit!!.host.type) {
-                LOCAL -> File(contentEntryPath).path
-                WSL, SSH -> remoteContentEntryPath
-            }
-            val generateDirectory = when (toolkit!!.host.type) {
-                LOCAL -> File("$contentEntryPath.tmpdir").path
-                WSL, SSH -> remoteContentEntryPath
-            }
+            val workingDirectory =
+                if (!toolkit!!.isOnRemote) File(contentEntryPath).path
+                else remoteContentEntryPath
+
+            val generateDirectory =
+                if (!toolkit!!.isOnRemote) File("$contentEntryPath.tmpdir").path
+                else remoteContentEntryPath
+
 
             Log.info("contentEntry path: $contentEntryPath")
             Log.info("remote contentEntry path: $remoteContentEntryPath")
@@ -214,24 +213,8 @@ class XMakeProjectWizardStep(parent: NewProjectWizardBaseStep) :
                         }
                     }
 
-                    WSL -> {
-                        syncProjectByWslSync(
-                            scope,
-                            project,
-                            host.target as WSLDistribution,
-                            workingDirectory,
-                            SyncDirection.UPSTREAM_TO_LOCAL
-                        )
-                    }
-
-                    SSH -> {
-                        syncProjectBySftp(
-                            scope,
-                            project,
-                            host.target as SshConfig,
-                            workingDirectory,
-                            SyncDirection.UPSTREAM_TO_LOCAL
-                        )
+                    WSL, SSH -> {
+                        transferFolderByToolkit(project, this, SyncDirection.UPSTREAM_TO_LOCAL, workingDirectory, null)
                     }
                 }
             }

+ 7 - 15
src/main/kotlin/io/xmake/run/XMakeDefaultRunner.kt

@@ -1,26 +1,18 @@
 package io.xmake.run
 
-import com.intellij.execution.ExecutionManager
-import com.intellij.execution.configurations.RunProfile
 import com.intellij.execution.configurations.RunProfileState
 import com.intellij.execution.configurations.RunnerSettings
-import com.intellij.execution.executors.DefaultRunExecutor
-import com.intellij.execution.runners.*
+import com.intellij.execution.runners.AsyncProgramRunner
+import com.intellij.execution.runners.ExecutionEnvironment
+import com.intellij.execution.runners.executeState
 import com.intellij.execution.ui.RunContentDescriptor
-import com.intellij.openapi.diagnostic.Logger
+import org.jetbrains.concurrency.Promise
 import org.jetbrains.concurrency.resolvedPromise
-import java.util.concurrent.ExecutionException
-import kotlin.jvm.Throws
 
-abstract class XMakeDefaultRunner : ProgramRunner<RunnerSettings> {
+abstract class XMakeDefaultRunner : AsyncProgramRunner<RunnerSettings>() {
 
-    @Throws(ExecutionException::class)
-    override fun execute(environment: ExecutionEnvironment) {
-        val state = environment.state ?: return
-        @Suppress("UnstableApiUsage")
-        ExecutionManager.getInstance(environment.project).startRunProfile(environment) {
-            resolvedPromise(doExecute(state, environment))
-        }
+    override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise<RunContentDescriptor?> {
+        return resolvedPromise(doExecute(state, environment))
     }
     protected open fun doExecute(state: RunProfileState, environment: ExecutionEnvironment) : RunContentDescriptor? {
         return executeState(state, environment, this)

+ 9 - 28
src/main/kotlin/io/xmake/run/XMakeRunConfigurationEditor.kt

@@ -1,12 +1,10 @@
 package io.xmake.run
 
 import com.intellij.execution.configuration.EnvironmentVariablesTextFieldWithBrowseButton
-import com.intellij.execution.wsl.WSLDistribution
 import com.intellij.openapi.diagnostic.Logger
 import com.intellij.openapi.options.SettingsEditor
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.ui.ComboBox
-import com.intellij.ssh.config.unified.SshConfig
 import com.intellij.ui.PopupMenuListenerAdapter
 import com.intellij.ui.RawCommandLineEditor
 import com.intellij.ui.components.CheckBox
@@ -18,7 +16,6 @@ import com.intellij.ui.layout.ComboBoxPredicate
 import io.xmake.project.directory.ui.DirectoryBrowser
 import io.xmake.project.target.TargetManager
 import io.xmake.project.toolkit.Toolkit
-import io.xmake.project.toolkit.ToolkitHostType.*
 import io.xmake.project.toolkit.ui.ToolkitComboBox
 import io.xmake.project.toolkit.ui.ToolkitListItem
 import io.xmake.run.XMakeRunConfiguration.Companion.getArchitecturesByPlatform
@@ -26,8 +23,7 @@ import io.xmake.run.XMakeRunConfiguration.Companion.modes
 import io.xmake.run.XMakeRunConfiguration.Companion.platforms
 import io.xmake.shared.xmakeConfiguration
 import io.xmake.utils.execute.SyncDirection
-import io.xmake.utils.execute.syncProjectBySftp
-import io.xmake.utils.execute.syncProjectByWslSync
+import io.xmake.utils.execute.transferFolderByToolkit
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -258,29 +254,14 @@ class XMakeRunConfigurationEditor(
                     val workingDirectoryPath = workingDirectoryBrowser.text
 
                     scope.launch(Dispatchers.IO) {
-                        when (toolkit.host.type) {
-                            LOCAL -> {}
-                            WSL -> {
-                                val wslDistribution = toolkit.host.target as? WSLDistribution
-                                syncProjectByWslSync(
-                                    scope,
-                                    project,
-                                    wslDistribution!!,
-                                    workingDirectoryPath,
-                                    SyncDirection.LOCAL_TO_UPSTREAM
-                                )
-                            }
-
-                            SSH -> {
-                                val sshConfig = toolkit.host.target as? SshConfig
-                                syncProjectBySftp(
-                                    scope,
-                                    project,
-                                    sshConfig!!,
-                                    workingDirectoryPath,
-                                    SyncDirection.LOCAL_TO_UPSTREAM
-                                )
-                            }
+                        if (toolkit.isOnRemote) {
+                            transferFolderByToolkit(
+                                project,
+                                toolkit,
+                                SyncDirection.UPSTREAM_TO_LOCAL,
+                                workingDirectoryPath,
+                                null
+                            )
                         }
                     }
                 }

+ 17 - 30
src/main/kotlin/io/xmake/utils/execute/CommandEx.kt

@@ -7,13 +7,11 @@ import com.intellij.execution.ui.ConsoleViewContentType
 import com.intellij.execution.wsl.WSLCommandLineOptions
 import com.intellij.execution.wsl.WSLDistribution
 import com.intellij.execution.wsl.WslPath
+import com.intellij.execution.wsl.rootMappings
 import com.intellij.openapi.diagnostic.fileLogger
+import com.intellij.openapi.extensions.ExtensionPointName
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.util.Key
-import com.intellij.ssh.ConnectionBuilder
-import com.intellij.ssh.config.unified.SshConfig
-import com.intellij.ssh.interaction.PlatformSshPasswordProvider
-import com.intellij.ssh.processBuilder
 import com.intellij.util.io.awaitExit
 import io.xmake.project.toolkit.Toolkit
 import io.xmake.project.toolkit.ToolkitHostType.*
@@ -23,12 +21,15 @@ import io.xmake.project.xmakeProblemList
 import io.xmake.project.xmakeToolWindow
 import io.xmake.shared.XMakeProblem
 import io.xmake.utils.SystemUtils.parseProblem
+import io.xmake.utils.extension.ToolkitHostExtension
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
-import java.nio.charset.Charset
 
 private val Log = fileLogger()
 
+private val EP_NAME: ExtensionPointName<ToolkitHostExtension> =
+    ExtensionPointName("io.xmake.toolkitHostExtension")
+
 fun GeneralCommandLine.createLocalProcess(): Process{
     return this
         .also { Log.info("commandOnLocal: ${this.commandLineString}") }
@@ -37,16 +38,19 @@ fun GeneralCommandLine.createLocalProcess(): Process{
 
 fun GeneralCommandLine.createWslProcess(wslDistribution: WSLDistribution, project: Project? = null): Process {
     val commandInWsl: GeneralCommandLine = wslDistribution.patchCommandLine(
-        object : GeneralCommandLine(this) { init {
-            parametersList.clearAll()
-        }
+        object : GeneralCommandLine(this) {
+            init {
+                parametersList.clearAll()
+            }
         }, project,
         WSLCommandLineOptions().apply {
+            wslDistribution.rootMappings
             isLaunchWithWslExe = true
+//            remoteWorkingDirectory = workingDirectory?.toCanonicalPath()
         }
     ).apply {
         workDirectory?.let {
-            withWorkDirectory(WslPath(wslDistribution.id, workDirectory.path).toWindowsUncPath())
+            withWorkDirectory(WslPath(wslDistribution.id, it.path).toWindowsUncPath())
         }
         parametersList.replaceOrAppend([email protected], [email protected])
     }
@@ -55,24 +59,6 @@ fun GeneralCommandLine.createWslProcess(wslDistribution: WSLDistribution, projec
         .toProcessBuilder().start()
 }
 
-fun GeneralCommandLine.createSshProcess(sshConfig: SshConfig): Process {
-    val builder = ConnectionBuilder(sshConfig.host)
-        .withSshPasswordProvider(PlatformSshPasswordProvider(sshConfig.copyToCredentials()))
-
-    val command = GeneralCommandLine("sh").withParameters("-c")
-        .withParameters(this.commandLineString)
-        .withWorkDirectory(workDirectory)
-        .withCharset(charset)
-        .withEnvironment(environment)
-        .withInput(inputFile)
-        .withRedirectErrorStream(isRedirectErrorStream)
-
-    return builder
-        .also { Log.info("commandOnRemote: ${command.commandLineString}") }
-        .processBuilder(command)
-        .start()
-}
-
 fun GeneralCommandLine.createProcess(toolkit: Toolkit): Process {
     return with(toolkit) {
         Log.info("createProcessWithToolkit: $toolkit")
@@ -87,8 +73,9 @@ fun GeneralCommandLine.createProcess(toolkit: Toolkit): Process {
             }
 
             SSH -> {
-                val sshConfig = host.target as SshConfig
-                [email protected](sshConfig)
+                with(EP_NAME.extensions.first { it.KEY == "SSH" }) {
+                    createProcess(toolkit.host)
+                }
             }
         }
     }
@@ -114,7 +101,7 @@ fun runProcessWithHandler(
     } catch (e: ProcessNotCreatedException) {
         return null
     }
-    val processHandler = KillableColoredProcessHandler(process, command.commandLineString, Charset.forName("UTF-8"))
+    val processHandler = KillableColoredProcessHandler(process, command.commandLineString, Charsets.UTF_8)
     var content = ""
 
     processHandler.addProcessListener(object : ProcessAdapter() {

+ 127 - 100
src/main/kotlin/io/xmake/utils/execute/Sync.kt

@@ -1,51 +1,52 @@
 package io.xmake.utils.execute
 
-import ai.grazie.utils.tryRunWithException
 import com.intellij.execution.RunManager
+import com.intellij.execution.target.TargetEnvironment
+import com.intellij.execution.target.TargetProgressIndicatorAdapter
 import com.intellij.execution.wsl.WSLDistribution
-import com.intellij.execution.wsl.WslPath
-import com.intellij.execution.wsl.sync.WslHashFilters
-import com.intellij.execution.wsl.sync.WslSync
+import com.intellij.execution.wsl.target.WslTargetEnvironment
+import com.intellij.execution.wsl.target.WslTargetEnvironmentConfiguration
+import com.intellij.execution.wsl.target.WslTargetEnvironmentRequest
+import com.intellij.openapi.application.EDT
+import com.intellij.openapi.application.invokeLater
+import com.intellij.openapi.application.runWriteAction
 import com.intellij.openapi.diagnostic.fileLogger
-import com.intellij.openapi.diagnostic.runAndLogException
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.Task
+import com.intellij.openapi.progress.util.ProgressIndicatorBase
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.project.guessProjectDir
-import com.intellij.openapi.roots.ProjectRootManager
-import com.intellij.openapi.util.text.Formats
-import com.intellij.ssh.ConnectionBuilder
-import com.intellij.ssh.SftpChannelConfig
-import com.intellij.ssh.SftpChannelNoSuchFileException
-import com.intellij.ssh.SftpProgressTracker
-import com.intellij.ssh.channels.SftpChannel
-import com.intellij.ssh.config.unified.SshConfig
-import com.intellij.ssh.interaction.PlatformSshPasswordProvider
-import com.intellij.util.io.systemIndependentPath
+import com.intellij.openapi.util.io.toCanonicalPath
+import com.intellij.openapi.vfs.VirtualFileManager
 import io.xmake.project.toolkit.Toolkit
+import io.xmake.project.toolkit.ToolkitHost
 import io.xmake.project.toolkit.ToolkitHostType
 import io.xmake.run.XMakeRunConfiguration
+import io.xmake.utils.extension.ToolkitHostExtension
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
-import java.io.File
+import kotlinx.coroutines.withContext
 import kotlin.io.path.Path
+import kotlin.io.path.isDirectory
 
 private val Log = fileLogger()
 
+private val EP_NAME: ExtensionPointName<ToolkitHostExtension> = ExtensionPointName("io.xmake.toolkitHostExtension")
+
 enum class SyncMode {
     SYNC_ONLY,
     FORCE_SYNC,
 }
 
-enum class SyncType {
-    PROJECT_ONLY,
-    FOLDERS_ONLY,
-}
-
 enum class SyncStatus {
     SUCCESS,
     FAILED,
 }
 
-enum class SyncDirection { LOCAL_TO_UPSTREAM, UPSTREAM_TO_LOCAL}
+enum class SyncDirection { LOCAL_TO_UPSTREAM, UPSTREAM_TO_LOCAL }
 
 fun SyncDirection.toBoolean(): Boolean = when (this) {
     SyncDirection.LOCAL_TO_UPSTREAM -> false
@@ -55,101 +56,127 @@ fun SyncDirection.toBoolean(): Boolean = when (this) {
 fun syncProjectByWslSync(
     scope: CoroutineScope,
     project: Project,
-    wslDistribution: WSLDistribution,
-    wslPath: String,
-    direction: SyncDirection
-) {
-    scope.launch {
-        WslSync.syncWslFolders(
-            WslPath.parseWindowsUncPath(wslPath)?.linuxPath ?: wslPath,
-            project.guessProjectDir()!!.toNioPath(),
-            wslDistribution,
-            direction.toBoolean(),
-            WslHashFilters.WslHashFiltersBuilder().build()
-        )
-    }
-}
-
-fun syncProjectBySftp(
-    scope: CoroutineScope,
-    project: Project,
-    config: SshConfig,
-    remotePath: String,
-    direction: SyncDirection
+    host: ToolkitHost,
+    direction: SyncDirection,
+    directoryPath: String,
+    relativePath: String? = null,
 ) {
-    val commandString = SftpChannelConfig.SftpCommand.detectSftpCommandString
-    Log.info("Command: $commandString")
-
-    val builder = ConnectionBuilder(config.host)
-        .withSshPasswordProvider(PlatformSshPasswordProvider(config.copyToCredentials()))
-
-    val sourceRoots = ProjectRootManager.getInstance(project).contentRoots
-    Log.info("Source roots: $sourceRoots")
-
-    Log.info("basePath: "+project.basePath)
-    Log.info("projectFile: "+project.projectFile)
-    Log.info("projectFilePath: "+project.projectFile)
-    Log.info("guessProjectDir: "+project.guessProjectDir())
-
-    scope.launch {
-        val sftpChannel = builder.openFailSafeSftpChannel()
-        Log.info("sftpChannel.home"+sftpChannel.home)
-
-        when (direction) {
-            SyncDirection.LOCAL_TO_UPSTREAM -> {
-
-                Log.runAndLogException {
-                    tryRunWithException<SftpChannelNoSuchFileException, List<SftpChannel.FileInfo>> {
-                        sftpChannel.ls(
-                            remotePath
+    val wslDistribution = host.target as? WSLDistribution ?: throw IllegalArgumentException()
+
+    ProgressManager.getInstance().runProcessWithProgressAsynchronously(
+        object : Task.Backgroundable(project, "Sync directory", true) {
+            override fun run(indicator: ProgressIndicator) {
+                scope.launch {
+                    indicator.isIndeterminate = true
+
+                    /*                    for (i in 1..100) {
+                                            if (indicator.isCanceled) {
+                                                break
+                                            }
+                                            withContext(Dispatchers.EDT) {
+                                                indicator.fraction = i / 100.0
+                                                indicator.text = "Processing $i%"
+                                            }
+                                        }*/
+
+                    val wslTargetEnvironmentRequest = WslTargetEnvironmentRequest(
+                        WslTargetEnvironmentConfiguration(wslDistribution)
+                    ).apply {
+                        downloadVolumes.add(
+                            TargetEnvironment.DownloadRoot(
+                                project.guessProjectDir()!!.toNioPath(),
+                                TargetEnvironment.TargetPath.Persistent(directoryPath)
+                            )
+                        )
+                        uploadVolumes.add(
+                            TargetEnvironment.UploadRoot(
+                                project.guessProjectDir()!!.toNioPath(),
+                                TargetEnvironment.TargetPath.Persistent(directoryPath),
+                            ).also { println(it.targetRootPath) }.apply {
+                                this.volumeData
+                            }
                         )
+                        shouldCopyVolumes = true
                     }
-                        .also { Log.info("before: $it") }
-                    sftpChannel.rmRecur(remotePath)
-                    Log.info("after: "+sftpChannel.ls("Project"))
-                }
 
-                sftpChannel.uploadFileOrDir(
-                    File(project.basePath ?: ""),
-                    remoteDir = remotePath, relativePath = "/",
-                    progressTracker = object : SftpProgressTracker {
-                        override val isCanceled: Boolean
-                            get() = false
-                        //                TODO("Not yet implemented")
+                    val wslTargetEnvironment = WslTargetEnvironment(
+                        wslTargetEnvironmentRequest,
+                        wslDistribution
+                    )
+
+                    when (direction) {
+                        SyncDirection.LOCAL_TO_UPSTREAM -> {
+                            wslTargetEnvironment.uploadVolumes.forEach { root, volume ->
+                                println("upload: ${root.localRootPath}, ${root.targetRootPath}")
+                                volume.upload(relativePath ?: "", TargetProgressIndicatorAdapter(indicator))
+                            }
+                        }
 
-                        override fun onBytesTransferred(count: Long) {
-                            println("onBytesTransferred(${Formats.formatFileSize(count)})")
+                        SyncDirection.UPSTREAM_TO_LOCAL -> {
+                            wslTargetEnvironment.downloadVolumes.forEach { root, volume ->
+                                volume.download(relativePath ?: "", indicator)
+                            }
                         }
+                    }
 
-                        override fun onFileCopied(file: File) {
-                            println("onFileCopied($file)")
+                    withContext(Dispatchers.EDT) {
+                        runWriteAction {
+                            VirtualFileManager.getInstance().syncRefresh()
                         }
-                    }, filesFilter = { file ->
-                        mutableListOf(".xmake", ".idea", "build", ".gitignore")
-                            .all { !file.startsWith(Path(project.basePath ?: "", it).toFile()) }
-                    }, persistExecutableBit = true)
-            }
-            SyncDirection.UPSTREAM_TO_LOCAL -> {
-                sftpChannel.downloadFileOrDir(remotePath, project.basePath ?: "")
+                    }
+
+                }
             }
-        }
-        sftpChannel.close()
-    }
+
+            override fun onCancel() {}
+
+            override fun onFinished() {}
+        },
+        ProgressIndicatorBase()
+    )
 }
 
-fun syncFileByToolkit(scope: CoroutineScope, project: Project, toolkit: Toolkit, remotePath: String, direction: SyncDirection) {
-    val fileRootPath =
-        (RunManager.getInstance(project).selectedConfiguration?.configuration as XMakeRunConfiguration).runWorkingDir
+private val scope = CoroutineScope(Dispatchers.IO)
 
-    val filePath = Path(fileRootPath, remotePath).systemIndependentPath
+fun transferFolderByToolkit(
+    project: Project,
+    toolkit: Toolkit,
+    direction: SyncDirection,
+    directoryPath: String = (RunManager.getInstance(project).selectedConfiguration?.configuration as XMakeRunConfiguration).runWorkingDir,
+    relativePath: String? = null,
+) {
 
     when (toolkit.host.type) {
-        ToolkitHostType.LOCAL -> {}
+        ToolkitHostType.LOCAL -> {
+            invokeLater {
+                runWriteAction {
+                    VirtualFileManager.getInstance().syncRefresh()
+                }
+            }
+        }
         ToolkitHostType.WSL -> {
-            syncProjectByWslSync(scope, project, toolkit.host.target as WSLDistribution, filePath, direction)
+            syncProjectByWslSync(
+                scope,
+                project,
+                toolkit.host,
+                direction,
+                directoryPath,
+                relativePath?.let { if (Path(it).isDirectory()) relativePath else null }
+            )
         }
         ToolkitHostType.SSH -> {
-            syncProjectBySftp(scope, project, toolkit.host.target as SshConfig, filePath, direction)
+            val path = relativePath?.let { Path(directoryPath).resolve(relativePath).toCanonicalPath() }
+                ?: directoryPath
+            EP_NAME.extensions.first { it.KEY == "SSH" }
+                .syncProject(scope, project, toolkit.host, direction, path)
         }
     }
+}
+
+fun syncBeforeFetch(project: Project, toolkit: Toolkit) {
+    transferFolderByToolkit(project, toolkit, SyncDirection.LOCAL_TO_UPSTREAM, relativePath = null)
+}
+
+fun fetchGeneratedFile(project: Project, toolkit: Toolkit, fileRelatedPath: String) {
+    transferFolderByToolkit(project, toolkit, SyncDirection.UPSTREAM_TO_LOCAL, relativePath = fileRelatedPath)
 }

+ 207 - 0
src/main/kotlin/io/xmake/utils/extension/SshToolkitHostExtensionImpl.kt

@@ -0,0 +1,207 @@
+package io.xmake.utils.extension
+
+import ai.grazie.utils.tryRunWithException
+import com.intellij.execution.configurations.GeneralCommandLine
+import com.intellij.openapi.application.EDT
+import com.intellij.openapi.application.runWriteAction
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.diagnostic.runAndLogException
+import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.Task
+import com.intellij.openapi.progress.util.ProgressIndicatorBase
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.roots.ProjectRootManager
+import com.intellij.openapi.util.text.Formats
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.ssh.*
+import com.intellij.ssh.channels.SftpChannel
+import com.intellij.ssh.config.unified.SshConfig
+import com.intellij.ssh.config.unified.SshConfigManager
+import com.intellij.ssh.interaction.PlatformSshPasswordProvider
+import com.intellij.ssh.ui.sftpBrowser.RemoteBrowserDialog
+import com.intellij.ssh.ui.sftpBrowser.SftpRemoteBrowserProvider
+import io.xmake.project.directory.ui.DirectoryBrowser
+import io.xmake.project.toolkit.Toolkit
+import io.xmake.project.toolkit.ToolkitHost
+import io.xmake.project.toolkit.ToolkitHostType
+import io.xmake.utils.execute.SyncDirection
+import io.xmake.utils.execute.rmRecur
+import kotlinx.coroutines.*
+import java.awt.event.ActionListener
+import java.io.File
+import kotlin.io.path.Path
+
+class SshToolkitHostExtensionImpl : ToolkitHostExtension {
+
+    override val KEY: String = "SSH"
+
+    private val sshConfigManager = SshConfigManager.getInstance(null)
+
+    override fun getHostType(): String {
+        return "SSH"
+    }
+
+    override fun getToolkitHosts(project: Project?): List<ToolkitHost> {
+        return sshConfigManager.configs.map {
+            ToolkitHost(ToolkitHostType.SSH, it)
+        }
+    }
+
+    override fun filterRegistered(): (Toolkit) -> Boolean {
+        return { it.isOnRemote }
+    }
+
+    override fun createToolkit(host: ToolkitHost, path: String, version: String): Toolkit {
+        val sshConfig = (host.target as? SshConfig) ?: throw IllegalArgumentException()
+        val name = sshConfig.presentableShortName
+        return Toolkit(name, host, path, version)
+    }
+
+    override fun syncProject(
+        scope: CoroutineScope,
+        project: Project,
+        host: ToolkitHost,
+        direction: SyncDirection,
+        remoteDirectory: String,
+    ) {
+        val sshConfig = (host.target as? SshConfig) ?: throw IllegalArgumentException()
+
+        ProgressManager.getInstance().runProcessWithProgressAsynchronously(
+            object : Task.Backgroundable(project, "Sync directory", true) {
+                override fun run(indicator: ProgressIndicator) {
+                    val commandString = SftpChannelConfig.SftpCommand.detectSftpCommandString
+                    Log.info("Command: $commandString")
+
+                    val builder = ConnectionBuilder(sshConfig.host)
+                        .withSshPasswordProvider(PlatformSshPasswordProvider(sshConfig.copyToCredentials()))
+
+                    val sourceRoots = ProjectRootManager.getInstance(project).contentRoots
+                    Log.info("Source roots: $sourceRoots")
+                    Log.info("guessProjectDir: " + project.guessProjectDir())
+
+                    scope.launch {
+                        val sftpChannel = builder.openFailSafeSftpChannel()
+                        Log.info("sftpChannel.home" + sftpChannel.home)
+
+                        when (direction) {
+                            SyncDirection.LOCAL_TO_UPSTREAM -> {
+
+                                Log.runAndLogException {
+                                    tryRunWithException<SftpChannelNoSuchFileException, List<SftpChannel.FileInfo>> {
+                                        sftpChannel.ls(
+                                            remoteDirectory
+                                        )
+                                    }
+                                        .also { Log.info("before: $it") }
+                                    sftpChannel.rmRecur(remoteDirectory)
+                                    Log.info("after: " + sftpChannel.ls("Project"))
+                                }
+
+                                sftpChannel.uploadFileOrDir(
+                                    File(project.guessProjectDir()?.path ?: ""),
+                                    remoteDir = remoteDirectory, relativePath = "/",
+                                    progressTracker = object : SftpProgressTracker {
+                                        override val isCanceled: Boolean
+                                            get() = false
+                                        //                TODO("Not yet implemented")
+
+                                        override fun onBytesTransferred(count: Long) {
+                                            println("onBytesTransferred(${Formats.formatFileSize(count)})")
+                                        }
+
+                                        override fun onFileCopied(file: File) {
+                                            println("onFileCopied($file)")
+                                        }
+                                    }, filesFilter = { file ->
+                                        mutableListOf(".xmake", ".idea", "build", ".gitignore")
+                                            .all {
+                                                !file.startsWith(
+                                                    Path(
+                                                        project.guessProjectDir()?.path ?: "",
+                                                        it
+                                                    ).toFile()
+                                                )
+                                            }
+                                    }, persistExecutableBit = true
+                                )
+                            }
+
+                            SyncDirection.UPSTREAM_TO_LOCAL -> {
+                                sftpChannel.downloadFileOrDir(remoteDirectory, project.guessProjectDir()?.path ?: "")
+                            }
+                        }
+                        sftpChannel.close()
+
+                        withContext(Dispatchers.EDT) {
+                            runWriteAction {
+                                VirtualFileManager.getInstance().syncRefresh()
+                            }
+                        }
+
+                    }
+                }
+
+                override fun onCancel() {}
+
+                override fun onFinished() {}
+            },
+            ProgressIndicatorBase()
+        )
+    }
+
+    override suspend fun ToolkitHost.loadTargetX(project: Project?) = coroutineScope {
+        target = SshConfigManager.getInstance(project).findConfigById(id!!)!!
+    }
+
+    override fun getTargetId(target: Any?): String {
+        val sshConfig = target as? SshConfig ?: throw IllegalArgumentException()
+        return sshConfig.id
+    }
+
+    override fun DirectoryBrowser.createBrowseListener(host: ToolkitHost): ActionListener {
+        val sshConfig = host.target as? SshConfig ?: throw IllegalArgumentException()
+
+        val sftpChannel = runBlocking(Dispatchers.Default) {
+            ConnectionBuilder(sshConfig.host)
+                .withSshPasswordProvider(PlatformSshPasswordProvider(sshConfig.copyToCredentials()))
+                .openFailSafeSftpChannel()
+        }
+        val sftpRemoteBrowserProvider = SftpRemoteBrowserProvider(sftpChannel)
+        val remoteBrowseFolderListener = ActionListener {
+            text = RemoteBrowserDialog(
+                sftpRemoteBrowserProvider,
+                project,
+                true,
+                withCreateDirectoryButton = true
+            ).apply { showAndGet() }.getResult()
+        }
+        return remoteBrowseFolderListener
+    }
+
+    override fun GeneralCommandLine.createProcess(host: ToolkitHost): Process {
+
+        val sshConfig = host.target as? SshConfig ?: throw IllegalArgumentException()
+
+        val builder = ConnectionBuilder(sshConfig.host)
+            .withSshPasswordProvider(PlatformSshPasswordProvider(sshConfig.copyToCredentials()))
+
+        val command = GeneralCommandLine("sh").withParameters("-c")
+            .withParameters(this.commandLineString)
+            .withWorkDirectory(workDirectory)
+            .withCharset(charset)
+            .withEnvironment(environment)
+            .withInput(inputFile)
+            .withRedirectErrorStream(isRedirectErrorStream)
+
+        return builder
+            .also { Log.info("commandOnRemote: ${command.commandLineString}") }
+            .processBuilder(command)
+            .start()
+    }
+
+    companion object {
+        private val Log = logger<SshToolkitHostExtensionImpl>()
+    }
+}

+ 38 - 0
src/main/kotlin/io/xmake/utils/extension/ToolkitHostExtension.kt

@@ -0,0 +1,38 @@
+package io.xmake.utils.extension
+
+import com.intellij.execution.configurations.GeneralCommandLine
+import com.intellij.openapi.project.Project
+import io.xmake.project.directory.ui.DirectoryBrowser
+import io.xmake.project.toolkit.Toolkit
+import io.xmake.project.toolkit.ToolkitHost
+import io.xmake.utils.execute.SyncDirection
+import kotlinx.coroutines.CoroutineScope
+import java.awt.event.ActionListener
+
+interface ToolkitHostExtension {
+    val KEY: String
+
+    fun getHostType(): String
+
+    fun getToolkitHosts(project: Project? = null): List<ToolkitHost>
+
+    fun filterRegistered(): (Toolkit) -> Boolean
+
+    fun createToolkit(host: ToolkitHost, path: String, version: String): Toolkit
+
+    fun syncProject(
+        scope: CoroutineScope,
+        project: Project,
+        host: ToolkitHost,
+        direction: SyncDirection,
+        remoteDirectory: String,
+    )
+
+    fun getTargetId(target: Any? = null): String
+
+    suspend fun ToolkitHost.loadTargetX(project: Project? = null)
+
+    fun DirectoryBrowser.createBrowseListener(host: ToolkitHost): ActionListener
+
+    fun GeneralCommandLine.createProcess(target: ToolkitHost): Process
+}

+ 5 - 0
src/main/resources/META-INF/io.xmake-ssh.xml

@@ -0,0 +1,5 @@
+<idea-plugin>
+    <extensions defaultExtensionNs="io.xmake">
+        <toolkitHostExtension implementation="io.xmake.utils.extension.SshToolkitHostExtensionImpl"/>
+    </extensions>
+</idea-plugin>

+ 0 - 2
src/main/resources/META-INF/io.xmake-ultimate.xml

@@ -1,2 +0,0 @@
-<idea-plugin>
-</idea-plugin>

+ 7 - 2
src/main/resources/META-INF/plugin.xml

@@ -4,14 +4,19 @@
     <name>XMake</name>
     <vendor email="[email protected]" url="https://xmake.io">xmake.io</vendor>
 
+    <extensionPoints>
+        <extensionPoint name="toolkitHostExtension" interface="io.xmake.utils.extension.ToolkitHostExtension"
+                        dynamic="true"/>
+    </extensionPoints>
+
     <!--all-->
     <depends>com.intellij.modules.platform</depends>
     <depends>com.intellij.modules.lang</depends>
     <depends>com.intellij.modules.xml</depends>
     <depends>com.intellij.modules.xdebugger</depends>
 
-    <depends optional="true" config-file="io.xmake-ultimate.xml">
-        com.intellij.modules.ultimate
+    <depends optional="true" config-file="io.xmake-ssh.xml">
+        com.intellij.modules.ssh
     </depends>
     <!--clion and c/cpp language-->
     <!--