@@ -17,11 +17,16 @@ import com.intellij.openapi.Disposable
1717import com.intellij.openapi.application.ApplicationManager
1818import com.intellij.openapi.diagnostic.Logger
1919import com.intellij.openapi.ui.ComboBox
20+ import com.intellij.openapi.ui.ComponentValidator
21+ import com.intellij.openapi.ui.ValidationInfo
22+ import com.intellij.openapi.util.Disposer
2023import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
2124import com.intellij.remote.AuthType
2225import com.intellij.remote.RemoteCredentialsHolder
26+ import com.intellij.ssh.SshException
2327import com.intellij.ui.AnimatedIcon
2428import com.intellij.ui.ColoredListCellRenderer
29+ import com.intellij.ui.DocumentAdapter
2530import com.intellij.ui.components.JBTextField
2631import com.intellij.ui.dsl.builder.BottomGap
2732import com.intellij.ui.dsl.builder.RowLayout
@@ -30,6 +35,8 @@ import com.intellij.ui.dsl.builder.panel
3035import com.intellij.ui.dsl.gridLayout.HorizontalAlign
3136import com.intellij.util.ui.JBFont
3237import com.intellij.util.ui.UIUtil
38+ import com.intellij.util.ui.update.MergingUpdateQueue
39+ import com.intellij.util.ui.update.Update
3340import com.jetbrains.gateway.api.GatewayUI
3441import com.jetbrains.gateway.ssh.CachingProductsJsonWrapper
3542import com.jetbrains.gateway.ssh.DeployTargetOS
@@ -43,13 +50,17 @@ import kotlinx.coroutines.CancellationException
4350import kotlinx.coroutines.CoroutineScope
4451import kotlinx.coroutines.Dispatchers
4552import kotlinx.coroutines.Job
53+ import kotlinx.coroutines.TimeoutCancellationException
4654import kotlinx.coroutines.async
4755import kotlinx.coroutines.cancel
4856import kotlinx.coroutines.cancelAndJoin
4957import kotlinx.coroutines.launch
58+ import kotlinx.coroutines.runBlocking
59+ import kotlinx.coroutines.time.withTimeout
5060import kotlinx.coroutines.withContext
5161import java.awt.Component
5262import java.awt.FlowLayout
63+ import java.time.Duration
5364import java.util.Locale
5465import javax.swing.ComboBoxModel
5566import javax.swing.DefaultComboBoxModel
@@ -58,6 +69,7 @@ import javax.swing.JList
5869import javax.swing.JPanel
5970import javax.swing.ListCellRenderer
6071import javax.swing.SwingConstants
72+ import javax.swing.event.DocumentEvent
6173
6274class CoderLocateRemoteProjectStepView (private val disableNextAction : () -> Unit ) : CoderWorkspacesWizardStep, Disposable {
6375 private val cs = CoroutineScope (Dispatchers .Main )
@@ -68,10 +80,10 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
6880 private lateinit var titleLabel: JLabel
6981 private lateinit var wizard: CoderWorkspacesWizardModel
7082 private lateinit var cbIDE: IDEComboBox
71- private lateinit var tfProject: JBTextField
83+ private var tfProject = JBTextField ()
7284 private lateinit var terminalLink: LazyBrowserLink
73-
7485 private lateinit var ideResolvingJob: Job
86+ private val pathValidationJobs = MergingUpdateQueue (" remote-path-validation" , 1000 , true , tfProject)
7587
7688 override val component = panel {
7789 indent {
@@ -92,9 +104,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
92104
93105 row {
94106 label(" Project directory:" )
95- tfProject = textField()
96- .resizableColumn()
97- .horizontalAlign(HorizontalAlign .FILL ).component
107+ cell(tfProject).resizableColumn().horizontalAlign(HorizontalAlign .FILL ).component
98108 cell()
99109 }.topGap(TopGap .NONE ).bottomGap(BottomGap .NONE ).layout(RowLayout .PARENT_GRID )
100110 row {
@@ -113,6 +123,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
113123 override val nextActionText = CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.next.text" )
114124
115125 override fun onInit (wizardModel : CoderWorkspacesWizardModel ) {
126+ cbIDE.renderer = IDECellRenderer ()
116127 ideComboBoxModel.removeAllElements()
117128 wizard = wizardModel
118129 val selectedWorkspace = wizardModel.selectedWorkspace
@@ -127,11 +138,30 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
127138
128139 ideResolvingJob = cs.launch {
129140 try {
130- retrieveIDES(selectedWorkspace)
141+ val executor = withTimeout(Duration .ofSeconds(60 )) { createRemoteExecutor() }
142+ retrieveIDES(executor, selectedWorkspace)
143+ if (ComponentValidator .getInstance(tfProject).isEmpty) {
144+ installRemotePathValidator(executor)
145+ }
131146 } catch (e: Exception ) {
132147 when (e) {
133148 is InterruptedException -> Unit
134149 is CancellationException -> Unit
150+ is TimeoutCancellationException ,
151+ is SshException -> {
152+ logger.error(" Can't connect to workspace ${selectedWorkspace.name} . Reason: $e " )
153+ withContext(Dispatchers .Main ) {
154+ disableNextAction()
155+ cbIDE.renderer = object : ColoredListCellRenderer <IdeWithStatus >() {
156+ override fun customizeCellRenderer (list : JList <out IdeWithStatus >, value : IdeWithStatus ? , index : Int , isSelected : Boolean , cellHasFocus : Boolean ) {
157+ background = UIUtil .getListBackground(isSelected, cellHasFocus)
158+ icon = UIUtil .getBalloonErrorIcon()
159+ append(CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.ssh.error.text" ))
160+ }
161+ }
162+ }
163+ }
164+
135165 else -> {
136166 logger.error(" Could not resolve any IDE for workspace ${selectedWorkspace.name} . Reason: $e " )
137167 withContext(Dispatchers .Main ) {
@@ -140,7 +170,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
140170 override fun customizeCellRenderer (list : JList <out IdeWithStatus >, value : IdeWithStatus ? , index : Int , isSelected : Boolean , cellHasFocus : Boolean ) {
141171 background = UIUtil .getListBackground(isSelected, cellHasFocus)
142172 icon = UIUtil .getBalloonErrorIcon()
143- append(CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.ide.error.text" , selectedWorkspace.name ))
173+ append(CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.ide.error.text" ))
144174 }
145175 }
146176 }
@@ -150,23 +180,56 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
150180 }
151181 }
152182
153- private suspend fun retrieveIDES (selectedWorkspace : WorkspaceAgentModel ) {
154- logger.info(" Retrieving available IDE's for ${selectedWorkspace.name} workspace..." )
155- val hostAccessor = HighLevelHostAccessor .create(
183+ private fun installRemotePathValidator (executor : HighLevelHostAccessor ) {
184+ var disposable = Disposer .newDisposable(ApplicationManager .getApplication(), CoderLocateRemoteProjectStepView .javaClass.name)
185+ ComponentValidator (disposable).installOn(tfProject)
186+
187+ tfProject.document.addDocumentListener(object : DocumentAdapter () {
188+ override fun textChanged (event : DocumentEvent ) {
189+ pathValidationJobs.queue(Update .create(" validate-remote-path" ) {
190+ runBlocking {
191+ try {
192+ val isPathPresent = executor.isPathPresentOnRemote(tfProject.text)
193+ if (! isPathPresent) {
194+ ComponentValidator .getInstance(tfProject).ifPresent {
195+ it.updateInfo(ValidationInfo (" Can't find directory: ${tfProject.text} " , tfProject))
196+ }
197+ } else {
198+ ComponentValidator .getInstance(tfProject).ifPresent {
199+ it.updateInfo(null )
200+ }
201+ }
202+ } catch (e: Exception ) {
203+ ComponentValidator .getInstance(tfProject).ifPresent {
204+ it.updateInfo(ValidationInfo (" Can't validate directory: ${tfProject.text} " , tfProject))
205+ }
206+ }
207+ }
208+ })
209+ }
210+ })
211+ }
212+
213+ private suspend fun createRemoteExecutor (): HighLevelHostAccessor {
214+ return HighLevelHostAccessor .create(
156215 RemoteCredentialsHolder ().apply {
157- setHost(" coder.${selectedWorkspace.name} " )
216+ setHost(" coder.${wizard. selectedWorkspace? .name} " )
158217 userName = " coder"
159218 authType = AuthType .OPEN_SSH
160219 },
161220 true
162221 )
222+ }
223+
224+ private suspend fun retrieveIDES (executor : HighLevelHostAccessor , selectedWorkspace : WorkspaceAgentModel ) {
225+ logger.info(" Retrieving available IDE's for ${selectedWorkspace.name} workspace..." )
163226 val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null ) toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) else withContext(Dispatchers .IO ) {
164- hostAccessor .guessOs()
227+ executor .guessOs()
165228 }
166229
167230 logger.info(" Resolved OS and Arch for ${selectedWorkspace.name} is: $workspaceOS " )
168231 val installedIdesJob = cs.async(Dispatchers .IO ) {
169- hostAccessor .getInstalledIDEs().map { ide -> IdeWithStatus (ide.product, ide.buildNumber, IdeStatus .ALREADY_INSTALLED , null , ide.pathToIde, ide.presentableVersion, ide.remoteDevType) }
232+ executor .getInstalledIDEs().map { ide -> IdeWithStatus (ide.product, ide.buildNumber, IdeStatus .ALREADY_INSTALLED , null , ide.pathToIde, ide.presentableVersion, ide.remoteDevType) }
170233 }
171234 val idesWithStatusJob = cs.async(Dispatchers .IO ) {
172235 IntelliJPlatformProduct .values()
0 commit comments