PY-52925: Fix file browsing for WSL and other remote targets

User should have ability to browse target, not to type path manually

GitOrigin-RevId: b309e0683d0bc2ab3bfe1444ce945cb235d9fb14
This commit is contained in:
Ilya.Kazakevich
2022-07-18 21:12:16 +02:00
committed by intellij-monorepo-bot
parent 3dd5b1ed3e
commit 906a3a598c
12 changed files with 148 additions and 72 deletions

View File

@@ -16,19 +16,24 @@ import java.util.function.Supplier
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JPanel import javax.swing.JPanel
/**
* See [BrowsableTargetEnvironmentType.createBrowser]
*/
@Deprecated("Use overloaded method with Kotlin UI DSL 2 API") @Deprecated("Use overloaded method with Kotlin UI DSL 2 API")
fun textFieldWithBrowseTargetButton(row: Row, fun textFieldWithBrowseTargetButton(row: Row,
targetType: BrowsableTargetEnvironmentType, targetType: BrowsableTargetEnvironmentType,
targetSupplier: Supplier<out TargetEnvironmentConfiguration>, targetSupplier: Supplier<out TargetEnvironmentConfiguration>,
project: Project, project: Project,
@NlsContexts.DialogTitle title: String, @NlsContexts.DialogTitle title: String,
property: PropertyBinding<String>): CellBuilder<TextFieldWithBrowseButton> { property: PropertyBinding<String>,
noLocalFs: Boolean = false): CellBuilder<TextFieldWithBrowseButton> {
val textFieldWithBrowseButton = TextFieldWithBrowseButton() val textFieldWithBrowseButton = TextFieldWithBrowseButton()
val browser = targetType.createBrowser(project, val browser = targetType.createBrowser(project,
title, title,
TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT, TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT,
textFieldWithBrowseButton.textField, textFieldWithBrowseButton.textField,
targetSupplier) targetSupplier,
noLocalFs)
textFieldWithBrowseButton.addActionListener(browser) textFieldWithBrowseButton.addActionListener(browser)
textFieldWithBrowseButton.text = property.get() textFieldWithBrowseButton.text = property.get()
return row.component(textFieldWithBrowseButton).withBinding(TextFieldWithBrowseButton::getText, return row.component(textFieldWithBrowseButton).withBinding(TextFieldWithBrowseButton::getText,
@@ -36,17 +41,21 @@ fun textFieldWithBrowseTargetButton(row: Row,
property) property)
} }
/**
* See [BrowsableTargetEnvironmentType.createBrowser]
*/
fun com.intellij.ui.dsl.builder.Row.textFieldWithBrowseTargetButton(targetType: BrowsableTargetEnvironmentType, fun com.intellij.ui.dsl.builder.Row.textFieldWithBrowseTargetButton(targetType: BrowsableTargetEnvironmentType,
targetSupplier: Supplier<out TargetEnvironmentConfiguration>, targetSupplier: Supplier<out TargetEnvironmentConfiguration>,
project: Project, project: Project,
@NlsContexts.DialogTitle title: String, @NlsContexts.DialogTitle title: String,
property: MutableProperty<String>): Cell<TextFieldWithBrowseButton> { property: MutableProperty<String>): Cell<TextFieldWithBrowseButton> {
val textFieldWithBrowseButton = TextFieldWithBrowseButton() val textFieldWithBrowseButton = TextFieldWithBrowseButton()
val browser = targetType.createBrowser(project, val browser = targetType.createBrowser(project,
title, title,
TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT, TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT,
textFieldWithBrowseButton.textField, textFieldWithBrowseButton.textField,
targetSupplier) targetSupplier,
false)
textFieldWithBrowseButton.addActionListener(browser) textFieldWithBrowseButton.addActionListener(browser)
return cell(textFieldWithBrowseButton) return cell(textFieldWithBrowseButton)
.bind(TextFieldWithBrowseButton::getText, TextFieldWithBrowseButton::setText, property) .bind(TextFieldWithBrowseButton::getText, TextFieldWithBrowseButton::setText, property)

View File

@@ -66,14 +66,15 @@ class WslTargetType : TargetEnvironmentType<WslTargetEnvironmentConfiguration>(T
title: String?, title: String?,
textComponentAccessor: TextComponentAccessor<T>, textComponentAccessor: TextComponentAccessor<T>,
component: T, component: T,
configurationSupplier: Supplier<out TargetEnvironmentConfiguration>): ActionListener = ActionListener { configurationSupplier: Supplier<out TargetEnvironmentConfiguration>,
noLocalFs: Boolean): ActionListener = ActionListener {
val configuration = configurationSupplier.get() val configuration = configurationSupplier.get()
if (configuration is WslTargetEnvironmentConfiguration) { if (configuration is WslTargetEnvironmentConfiguration) {
configuration.distribution?.let { configuration.distribution?.let {
WslPathBrowser(object : TextAccessor { WslPathBrowser(object : TextAccessor {
override fun setText(text: String) = textComponentAccessor.setText(component, text) override fun setText(text: String) = textComponentAccessor.setText(component, text)
override fun getText() = textComponentAccessor.getText(component) override fun getText() = textComponentAccessor.getText(component)
}).browsePath(it, component) }).browsePath(it, component, accessWindowsFs = !noLocalFs)
return@ActionListener return@ActionListener
} }
} }

View File

@@ -10,18 +10,31 @@ import java.awt.*;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.util.function.Supplier; import java.util.function.Supplier;
/**
* Environment type provides access to its filesystem for services like {@link com.intellij.openapi.ui.TextFieldWithBrowseButton}
* So you can browse (possibily remote) target.
*/
public interface BrowsableTargetEnvironmentType { public interface BrowsableTargetEnvironmentType {
/**
* @param textComponentAccessor where path should be set. See {@link TextComponentAccessor#TEXT_FIELD_WHOLE_TEXT}
* @param component text field component
* @param configurationSupplier returns environment configuration
* @param noLocalFs some targets (WSL is the only known for now) may provide access to the local filesystem.
* True means you do not need it
* @return Action listener should be installed on "browse" button you want to show target FS browser.
*/
@NotNull <T extends Component> ActionListener createBrowser(@NotNull Project project, @NotNull <T extends Component> ActionListener createBrowser(@NotNull Project project,
@NlsContexts.DialogTitle String title, @NlsContexts.DialogTitle String title,
@NotNull TextComponentAccessor<T> textComponentAccessor, @NotNull TextComponentAccessor<T> textComponentAccessor,
@NotNull T component, @NotNull T component,
@NotNull Supplier<? extends TargetEnvironmentConfiguration> configurationSupplier); @NotNull Supplier<? extends TargetEnvironmentConfiguration> configurationSupplier,
boolean noLocalFs);
/** /**
* When configurable contains both connection parameters and components using them (text fields with browsing in this case), * When configurable contains both connection parameters and components using them (text fields with browsing in this case),
* those components need to have current connection settings available, not the last applied to with [Configurable.apply]. * those components need to have current connection settings available, not the last applied to with [Configurable.apply].
* * <p>
* This interface displays ability and provides API to get connection settings, which are shown in UI. See IDEA-255466. * This interface displays ability and provides API to get connection settings, which are shown in UI. See IDEA-255466.
*/ */
interface ConfigurableCurrentConfigurationProvider { interface ConfigurableCurrentConfigurationProvider {

View File

@@ -67,7 +67,7 @@ class GradleRuntimeTargetUI<C : TargetEnvironmentConfiguration>(private val conf
val configuration = configurationProvider.environmentConfiguration val configuration = configurationProvider.environmentConfiguration
val targetType = configuration.getTargetType() as? BrowsableTargetEnvironmentType ?: break val targetType = configuration.getTargetType() as? BrowsableTargetEnvironmentType ?: break
addTargetActionListener(configurationProvider.pathMapper, addTargetActionListener(configurationProvider.pathMapper,
targetType.createBrowser(project, title, TEXT_FIELD_WHOLE_TEXT, textField) { configuration }) targetType.createBrowser(project, title, TEXT_FIELD_WHOLE_TEXT, textField, { configuration }, false))
return this return this
} }
} }

View File

@@ -1,10 +1,16 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.configuration; package com.jetbrains.python.configuration;
import com.intellij.execution.target.BrowsableTargetEnvironmentType;
import com.intellij.execution.target.TargetBasedSdkAdditionalData;
import com.intellij.execution.target.TargetEnvironmentConfigurationKt;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.SdkAdditionalData;
import com.intellij.openapi.projectRoots.SdkModificator; import com.intellij.openapi.projectRoots.SdkModificator;
import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.TextComponentAccessor;
import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.NlsSafe; import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtil;
import com.intellij.ui.ClickListener; import com.intellij.ui.ClickListener;
@@ -19,9 +25,11 @@ import com.jetbrains.python.sdk.flavors.CondaEnvSdkFlavor;
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor; import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
@@ -48,12 +56,20 @@ public class EditSdkDialog extends DialogWrapper {
}); });
final String homePath = sdk.getHomePath(); final String homePath = sdk.getHomePath();
myInterpreterPathTextField.setText(homePath); myInterpreterPathTextField.setText(homePath);
myInterpreterPathTextField.addBrowseFolderListener(PyBundle.message("sdk.edit.dialog.specify.interpreter.path"), null, project, var sdkAdditionalData = sdk.getSdkAdditionalData();
PythonSdkType.getInstance().getHomeChooserDescriptor()); var label = PyBundle.message("sdk.edit.dialog.specify.interpreter.path");
var targetListener = createBrowseTargetListener(project, sdkAdditionalData, label);
if (targetListener != null) {
myInterpreterPathTextField.addActionListener(targetListener);
}
else {
myInterpreterPathTextField.addBrowseFolderListener(label, null, project,
PythonSdkType.getInstance().getHomeChooserDescriptor());
}
myRemoveAssociationLabel.setVisible(false); myRemoveAssociationLabel.setVisible(false);
final PythonSdkFlavor sdkFlavor = PythonSdkFlavor.getPlatformIndependentFlavor(homePath); final PythonSdkFlavor sdkFlavor = PythonSdkFlavor.getPlatformIndependentFlavor(homePath);
if ((sdkFlavor instanceof VirtualEnvSdkFlavor) || (sdkFlavor instanceof CondaEnvSdkFlavor)) { if ((sdkFlavor instanceof VirtualEnvSdkFlavor) || (sdkFlavor instanceof CondaEnvSdkFlavor)) {
PythonSdkAdditionalData data = (PythonSdkAdditionalData) sdk.getSdkAdditionalData(); PythonSdkAdditionalData data = (PythonSdkAdditionalData)sdk.getSdkAdditionalData();
if (data != null) { if (data != null) {
final String path = data.getAssociatedModulePath(); final String path = data.getAssociatedModulePath();
if (path != null) { if (path != null) {
@@ -86,6 +102,25 @@ public class EditSdkDialog extends DialogWrapper {
}.installOn(myRemoveAssociationLabel); }.installOn(myRemoveAssociationLabel);
} }
/**
* @return action to call when user clicks on "browse" button if sdk is target SDK that supports browsing
*/
private @Nullable ActionListener createBrowseTargetListener(@NotNull Project project, @NotNull SdkAdditionalData sdkAdditionalData,
@NotNull @NlsContexts.DialogTitle String label) {
if (!(sdkAdditionalData instanceof TargetBasedSdkAdditionalData)) {
return null;
}
var configuration = ((TargetBasedSdkAdditionalData)sdkAdditionalData).getTargetEnvironmentConfiguration();
if (configuration != null) {
var type = TargetEnvironmentConfigurationKt.getTargetType(configuration);
if (type instanceof BrowsableTargetEnvironmentType) {
return ((BrowsableTargetEnvironmentType)type).createBrowser(project, label, TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT,
myInterpreterPathTextField.getTextField(), () -> configuration, true);
}
}
return null;
}
@Override @Override
protected JComponent createCenterPanel() { protected JComponent createCenterPanel() {
return myMainPanel; return myMainPanel;

View File

@@ -41,7 +41,7 @@ import com.jetbrains.python.sdk.*;
import com.jetbrains.python.sdk.add.PyAddSdkDialog; import com.jetbrains.python.sdk.add.PyAddSdkDialog;
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
import com.jetbrains.python.target.PyTargetAwareAdditionalData; import com.jetbrains.python.target.PyTargetAwareAdditionalData;
import com.jetbrains.python.ui.ManualPathEntryDialog; import com.jetbrains.python.ui.remotePathEditor.ManualPathEntryDialog;
import one.util.streamex.StreamEx; import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -554,8 +554,8 @@ public class PythonSdkDetailsDialog extends DialogWrapper {
return remoteInterpreterManager.chooseRemoteFiles(myProject, (PyRemoteSdkAdditionalDataBase)sdkAdditionalData, false); return remoteInterpreterManager.chooseRemoteFiles(myProject, (PyRemoteSdkAdditionalDataBase)sdkAdditionalData, false);
} }
else if (sdkAdditionalData instanceof PyTargetAwareAdditionalData) { else if (sdkAdditionalData instanceof PyTargetAwareAdditionalData) {
// TODO [targets] Use proper file chooser dialog for corresponding target var dialog = new ManualPathEntryDialog(myProject, Platform.UNIX,
ManualPathEntryDialog dialog = new ManualPathEntryDialog(myProject, Platform.UNIX); ((PyTargetAwareAdditionalData)sdkAdditionalData).getTargetEnvironmentConfiguration());
if (dialog.showAndGet()) { if (dialog.showAndGet()) {
return new String[]{dialog.getPath()}; return new String[]{dialog.getPath()};
} }

View File

@@ -643,4 +643,3 @@ public final class PythonSdkType extends SdkType {
return true; return true;
} }
} }

View File

@@ -35,7 +35,7 @@ import com.intellij.util.PathUtil
import com.jetbrains.python.PyBundle import com.jetbrains.python.PyBundle
import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.add.target.createDetectedSdk import com.jetbrains.python.sdk.add.target.createDetectedSdk
import com.jetbrains.python.ui.ManualPathEntryDialog import com.jetbrains.python.ui.remotePathEditor.ManualPathEntryDialog
import java.awt.event.ActionListener import java.awt.event.ActionListener
import java.util.function.Supplier import java.util.function.Supplier
import javax.swing.JComboBox import javax.swing.JComboBox
@@ -94,7 +94,8 @@ class PySdkPathChoosingComboBox @JvmOverloads constructor(sdks: List<Sdk> = empt
title, title,
PY_SDK_COMBOBOX_TEXT_ACCESSOR, PY_SDK_COMBOBOX_TEXT_ACCESSOR,
childComponent, childComponent,
Supplier { targetEnvironmentConfiguration }) Supplier { targetEnvironmentConfiguration },
true)
} }
else { else {
// The fallback where the path is entered manually // The fallback where the path is entered manually

View File

@@ -34,6 +34,7 @@ fun TextFieldWithBrowseButton.withTargetBrowser(targetType: BrowsableTargetEnvir
title, title,
com.intellij.openapi.ui.TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT, com.intellij.openapi.ui.TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT,
textField, textField,
targetSupplier) targetSupplier,
true)
addActionListener(browser) addActionListener(browser)
} }

View File

@@ -1,50 +0,0 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.ui
import com.intellij.execution.Platform
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.util.io.OSAgnosticPathUtil
import com.intellij.ui.layout.*
import com.jetbrains.python.PyBundle
import javax.swing.JComponent
/**
* The dialog that allows to specify the path to a file or directory manually.
*
* Performs validation that the path that is being added is an absolute path on
* the specified [platform].
*/
class ManualPathEntryDialog(project: Project?, private val platform: Platform) : DialogWrapper(project) {
var path: String = ""
private set
init {
title = PyBundle.message("enter.path.dialog.title")
init()
}
override fun createCenterPanel(): JComponent =
panel {
row(label = PyBundle.message("path.label")) {
textField(prop = ::path).withValidationOnApply { textField ->
val text = textField.text
when {
text.isBlank() -> error(PyBundle.message("path.must.not.be.empty.error.message"))
!isAbsolutePath(text, platform) -> error(PyBundle.message("path.must.be.absolute.error.message"))
text.endsWith(" ") -> warning(PyBundle.message("path.ends.with.whitespace.warning.message"))
else -> null
}
}.focused()
}
}
companion object {
fun isAbsolutePath(path: String, platform: Platform): Boolean = when (platform) {
Platform.UNIX -> path.startsWith("/")
Platform.WINDOWS -> isAbsoluteWindowsPath(path)
}
private fun isAbsoluteWindowsPath(path: String): Boolean = OSAgnosticPathUtil.isAbsoluteDosPath(path)
}
}

View File

@@ -0,0 +1,66 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.ui.remotePathEditor
import com.intellij.execution.Platform
import com.intellij.execution.target.BrowsableTargetEnvironmentType
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.execution.target.getTargetType
import com.intellij.execution.target.textFieldWithBrowseTargetButton
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.util.io.OSAgnosticPathUtil
import com.intellij.ui.layout.*
import com.jetbrains.python.PyBundle
import java.util.function.Supplier
import javax.swing.JComponent
/**
* The dialog that allows to specify the path to a file or directory manually.
*
* Performs validation that the path that is being added is an absolute path on
* the specified [platform].
*/
class ManualPathEntryDialog(private val project: Project?,
private val platform: Platform = Platform.UNIX,
targetConfig: TargetEnvironmentConfiguration? = null) : DialogWrapper(project) {
private val targetConfigAndType: Pair<TargetEnvironmentConfiguration, BrowsableTargetEnvironmentType>? =
(targetConfig?.getTargetType() as? BrowsableTargetEnvironmentType)?.let { Pair(targetConfig, it) }
var path: String = ""
private set
init {
title = PyBundle.message("enter.path.dialog.title")
init()
}
override fun createCenterPanel(): JComponent {
val label = PyBundle.message("path.label")
return panel {
row(label = label) {
val textFieldComponent = if (targetConfigAndType == null)
textField(prop = ::path)
else
textFieldWithBrowseTargetButton(this, targetConfigAndType.second, Supplier { targetConfigAndType.first }, project!!, label, this@ManualPathEntryDialog::path.toBinding(), false)
textFieldComponent.withValidationOnApply { textField ->
val text = textField.text
when {
text.isBlank() -> error(PyBundle.message("path.must.not.be.empty.error.message"))
!isAbsolutePath(text, platform) -> error(PyBundle.message("path.must.be.absolute.error.message"))
text.endsWith(" ") -> warning(PyBundle.message("path.ends.with.whitespace.warning.message"))
else -> null
}
}.focused()
}
}
}
companion object {
fun isAbsolutePath(path: String, platform: Platform): Boolean = when (platform) {
Platform.UNIX -> path.startsWith("/")
Platform.WINDOWS -> isAbsoluteWindowsPath(path)
}
private fun isAbsoluteWindowsPath(path: String): Boolean = OSAgnosticPathUtil.isAbsoluteDosPath(path)
}
}

View File

@@ -2,6 +2,7 @@
package com.jetbrains.python.ui package com.jetbrains.python.ui
import com.intellij.execution.Platform import com.intellij.execution.Platform
import com.jetbrains.python.ui.remotePathEditor.ManualPathEntryDialog
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith