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

View File

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

View File

@@ -10,18 +10,31 @@ import java.awt.*;
import java.awt.event.ActionListener;
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 {
/**
* @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,
@NlsContexts.DialogTitle String title,
@NotNull TextComponentAccessor<T> textComponentAccessor,
@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),
* 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.
*/
interface ConfigurableCurrentConfigurationProvider {

View File

@@ -67,7 +67,7 @@ class GradleRuntimeTargetUI<C : TargetEnvironmentConfiguration>(private val conf
val configuration = configurationProvider.environmentConfiguration
val targetType = configuration.getTargetType() as? BrowsableTargetEnvironmentType ?: break
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
}
}

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.
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.projectRoots.SdkAdditionalData;
import com.intellij.openapi.projectRoots.SdkModificator;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.TextComponentAccessor;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.io.FileUtil;
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.VirtualEnvSdkFlavor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
@@ -48,12 +56,20 @@ public class EditSdkDialog extends DialogWrapper {
});
final String homePath = sdk.getHomePath();
myInterpreterPathTextField.setText(homePath);
myInterpreterPathTextField.addBrowseFolderListener(PyBundle.message("sdk.edit.dialog.specify.interpreter.path"), null, project,
PythonSdkType.getInstance().getHomeChooserDescriptor());
var sdkAdditionalData = sdk.getSdkAdditionalData();
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);
final PythonSdkFlavor sdkFlavor = PythonSdkFlavor.getPlatformIndependentFlavor(homePath);
if ((sdkFlavor instanceof VirtualEnvSdkFlavor) || (sdkFlavor instanceof CondaEnvSdkFlavor)) {
PythonSdkAdditionalData data = (PythonSdkAdditionalData) sdk.getSdkAdditionalData();
PythonSdkAdditionalData data = (PythonSdkAdditionalData)sdk.getSdkAdditionalData();
if (data != null) {
final String path = data.getAssociatedModulePath();
if (path != null) {
@@ -86,6 +102,25 @@ public class EditSdkDialog extends DialogWrapper {
}.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
protected JComponent createCenterPanel() {
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.flavors.PythonSdkFlavor;
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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -554,8 +554,8 @@ public class PythonSdkDetailsDialog extends DialogWrapper {
return remoteInterpreterManager.chooseRemoteFiles(myProject, (PyRemoteSdkAdditionalDataBase)sdkAdditionalData, false);
}
else if (sdkAdditionalData instanceof PyTargetAwareAdditionalData) {
// TODO [targets] Use proper file chooser dialog for corresponding target
ManualPathEntryDialog dialog = new ManualPathEntryDialog(myProject, Platform.UNIX);
var dialog = new ManualPathEntryDialog(myProject, Platform.UNIX,
((PyTargetAwareAdditionalData)sdkAdditionalData).getTargetEnvironmentConfiguration());
if (dialog.showAndGet()) {
return new String[]{dialog.getPath()};
}

View File

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

View File

@@ -35,7 +35,7 @@ import com.intellij.util.PathUtil
import com.jetbrains.python.PyBundle
import com.jetbrains.python.sdk.PythonSdkType
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.util.function.Supplier
import javax.swing.JComboBox
@@ -94,7 +94,8 @@ class PySdkPathChoosingComboBox @JvmOverloads constructor(sdks: List<Sdk> = empt
title,
PY_SDK_COMBOBOX_TEXT_ACCESSOR,
childComponent,
Supplier { targetEnvironmentConfiguration })
Supplier { targetEnvironmentConfiguration },
true)
}
else {
// The fallback where the path is entered manually

View File

@@ -34,6 +34,7 @@ fun TextFieldWithBrowseButton.withTargetBrowser(targetType: BrowsableTargetEnvir
title,
com.intellij.openapi.ui.TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT,
textField,
targetSupplier)
targetSupplier,
true)
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
import com.intellij.execution.Platform
import com.jetbrains.python.ui.remotePathEditor.ManualPathEntryDialog
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith