PY-14036: Support remote Django (and other) project creation

* See PyProjectSynchronizer for entry point
* DownloadAction refactored to extract  download
* VagrantSupportImpl refactored to fetch mapped folders
This commit is contained in:
Ilya.Kazakevich
2016-10-25 20:42:52 +03:00
parent 2f30abfe38
commit 2de0aa4aa0
15 changed files with 633 additions and 63 deletions

View File

@@ -21,6 +21,7 @@ import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.PathMappingSettings;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -53,6 +54,13 @@ public abstract class VagrantSupport {
public abstract void runVagrant(@NotNull String vagrantFolder, @Nullable String machineName) throws ExecutionException;
/**
* @param vagrantFolder folder with Vagrantfile
* @return path mappings from vagrant file
*/
@Nullable
public abstract PathMappingSettings getMappedFolders(@NotNull String vagrantFolder);
public abstract Collection<? extends RemoteConnector> getVagrantInstancesConnectors(@NotNull Project project);
public abstract boolean isVagrantInstance(VirtualFile dir);

View File

@@ -13,7 +13,6 @@ import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.platform.DirectoryProjectGenerator;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiManager;
import com.jetbrains.edu.coursecreator.actions.CCCreateLesson;
@@ -27,6 +26,7 @@ import com.jetbrains.edu.learning.statistics.EduUsagesCollector;
import com.jetbrains.python.PythonLanguage;
import com.jetbrains.python.newProject.PyNewProjectSettings;
import com.jetbrains.python.newProject.PythonProjectGenerator;
import com.jetbrains.python.remote.PyProjectSynchronizer;
import icons.CourseCreatorPythonIcons;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
@@ -38,7 +38,7 @@ import java.io.File;
import static com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator.OUR_COURSES_DIR;
public class PyCCProjectGenerator extends PythonProjectGenerator<PyNewProjectSettings> {
public class PyCCProjectGenerator extends PythonProjectGenerator<PyNewProjectSettings> {
private static final Logger LOG = Logger.getInstance(PyCCProjectGenerator.class);
private CCNewProjectPanel mySettingsPanel;
@@ -57,7 +57,9 @@ public class PyCCProjectGenerator extends PythonProjectGenerator<PyNewProjectSet
@Override
public void configureProject(@NotNull final Project project, @NotNull final VirtualFile baseDir,
@NotNull PyNewProjectSettings settings, @NotNull Module module) {
@NotNull PyNewProjectSettings settings,
@NotNull Module module,
@Nullable final PyProjectSynchronizer synchronizer) {
generateProject(project, baseDir, mySettingsPanel);
}
@@ -84,7 +86,8 @@ public class PyCCProjectGenerator extends PythonProjectGenerator<PyNewProjectSet
}
private static void createTestHelper(@NotNull Project project, PsiDirectory projectDir) {
final FileTemplate template = FileTemplateManager.getInstance(project).getInternalTemplate(FileUtil.getNameWithoutExtension(EduNames.TEST_HELPER));
final FileTemplate template =
FileTemplateManager.getInstance(project).getInternalTemplate(FileUtil.getNameWithoutExtension(EduNames.TEST_HELPER));
try {
FileTemplateUtil.createFromTemplate(template, EduNames.TEST_HELPER, null, projectDir);
}

View File

@@ -20,7 +20,6 @@ import com.intellij.openapi.projectRoots.SdkAdditionalData;
import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl;
import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.platform.DirectoryProjectGenerator;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiManager;
import com.intellij.util.BooleanFunction;
@@ -34,6 +33,7 @@ import com.jetbrains.python.newProject.PyNewProjectSettings;
import com.jetbrains.python.newProject.PythonProjectGenerator;
import com.jetbrains.python.packaging.PyPackageManager;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.remote.PyProjectSynchronizer;
import com.jetbrains.python.sdk.AbstractCreateVirtualEnvDialog;
import com.jetbrains.python.sdk.PyDetectedSdk;
import com.jetbrains.python.sdk.PythonSdkAdditionalData;
@@ -118,7 +118,9 @@ public class PyStudyDirectoryProjectGenerator extends PythonProjectGenerator<PyN
@Override
public void configureProject(@NotNull final Project project, @NotNull final VirtualFile baseDir,
@NotNull PyNewProjectSettings settings, @NotNull Module module) {
@NotNull PyNewProjectSettings settings,
@NotNull Module module,
@Nullable PyProjectSynchronizer synchronizer) {
myGenerator.generateProject(project, baseDir);
final String testHelper = "test_helper.py";
if (baseDir.findChild(testHelper) != null) return;

View File

@@ -0,0 +1,15 @@
"""
Accepts folder, creates (if does not exist) it and checks it is writable.
Empty output if ok. Error in stderr otherwise
"""
import os
import sys
folder = sys.argv[1]
d = os.path.dirname(folder)
if not os.path.exists(folder):
os.makedirs(folder)
if not os.access(folder, os.W_OK):
raise Exception("Dir {0} is not writable".format(folder))

View File

@@ -0,0 +1,12 @@
"""
Accepts 2 args: project name and dir where one should be created
"""
import sys, os
from django.core import management
project_name = sys.argv[1]
path = sys.argv[2]
if not os.path.exists(path):
os.mkdir(path)
management.execute_from_command_line(argv=["django-admin", "startproject", project_name, path])

View File

@@ -22,9 +22,7 @@ import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootModificationUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.remote.RemoteSdkCredentials;
import com.jetbrains.python.remote.PythonRemoteInterpreterManager;
import com.jetbrains.python.remote.RemoteProjectSettings;
import com.jetbrains.python.remote.PyProjectSynchronizer;
import icons.PythonIcons;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
@@ -33,7 +31,12 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.io.File;
public class PythonBaseProjectGenerator extends PythonProjectGenerator<PyNewProjectSettings> {
public class PythonBaseProjectGenerator extends PythonProjectGenerator<PyNewProjectSettings> {
public PythonBaseProjectGenerator() {
super(true);
}
@NotNull
@Nls
@Override
@@ -59,19 +62,11 @@ public class PythonBaseProjectGenerator extends PythonProjectGenerator<PyNewProj
}
@Override
public void configureProject(@NotNull final Project project,
@NotNull VirtualFile baseDir,
@Nullable final PyNewProjectSettings settings,
@NotNull final Module module) {
if (settings instanceof RemoteProjectSettings) {
PythonRemoteInterpreterManager manager = PythonRemoteInterpreterManager.getInstance();
assert manager != null;
manager.createDeployment(project, baseDir, (RemoteProjectSettings)settings,
(RemoteSdkCredentials)settings.getSdk().getSdkAdditionalData());
}
else if (settings != null) {
ApplicationManager.getApplication().runWriteAction(() -> ModuleRootModificationUtil.setModuleSdk(module, settings.getSdk()));
}
public void configureProject(@NotNull final Project project, @NotNull VirtualFile baseDir, @NotNull final PyNewProjectSettings settings,
@NotNull final Module module, @Nullable final PyProjectSynchronizer synchronizer) {
// Super should be called according to its contract unless we sync project explicitly (we do not, so we call super)
super.configureProject(project, baseDir, settings, module, synchronizer);
ApplicationManager.getApplication().runWriteAction(() -> ModuleRootModificationUtil.setModuleSdk(module, settings.getSdk()));
}
@NotNull

View File

@@ -15,19 +15,26 @@
*/
package com.jetbrains.python.newProject.actions;
import com.intellij.execution.ExecutionException;
import com.intellij.facet.ui.ValidationResult;
import com.intellij.ide.util.projectWizard.ProjectSettingsStepBase;
import com.intellij.ide.util.projectWizard.WebProjectTemplate;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.*;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.platform.DirectoryProjectGenerator;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.HideableDecorator;
import com.intellij.ui.TextAccessor;
import com.intellij.util.NullableConsumer;
import com.intellij.util.PathUtil;
import com.intellij.util.ui.UIUtil;
@@ -38,7 +45,11 @@ import com.jetbrains.python.configuration.VirtualEnvProjectFilter;
import com.jetbrains.python.newProject.PyFrameworkProjectGenerator;
import com.jetbrains.python.newProject.PythonProjectGenerator;
import com.jetbrains.python.packaging.PyPackage;
import com.jetbrains.python.packaging.PyPackageManager;
import com.jetbrains.python.packaging.PyPackageUtil;
import com.jetbrains.python.remote.PyProjectSynchronizer;
import com.jetbrains.python.remote.PythonRemoteInterpreterManager;
import com.jetbrains.python.sdk.PySdkUtil;
import com.jetbrains.python.sdk.PythonSdkType;
import icons.PythonIcons;
import org.jetbrains.annotations.NotNull;
@@ -50,28 +61,44 @@ import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class ProjectSpecificSettingsStep extends ProjectSettingsStepBase implements DumbAware {
private static final Logger LOGGER = Logger.getInstance(ProjectSpecificSettingsStep.class);
private PythonSdkChooserCombo mySdkCombo;
private boolean myInstallFramework;
private Sdk mySdk;
/**
* For remote projects path for project on remote side
*/
private PyRemotePathField myRemotePathField;
/**
* If remote path required for project creation or not
*/
private boolean myRemotePathRequired;
public ProjectSpecificSettingsStep(@NotNull final DirectoryProjectGenerator projectGenerator,
@NotNull final NullableConsumer<ProjectSettingsStepBase> callback) {
super(projectGenerator, callback);
}
private static boolean acceptsRemoteSdk(DirectoryProjectGenerator generator) {
if (generator instanceof PyFrameworkProjectGenerator) {
return ((PyFrameworkProjectGenerator)generator).acceptsRemoteSdk();
private void acceptsSdk(@NotNull final DirectoryProjectGenerator<?> generator,
@NotNull final Sdk sdk,
@NotNull final File projectDirectory) throws PythonProjectGenerator.PyNoProjectAllowedOnSdkException {
if (generator instanceof PythonProjectGenerator) {
((PythonProjectGenerator<?>)generator).checkProjectCanBeCreatedOnSdk(sdk, projectDirectory);
}
return true;
}
@Override
protected JPanel createAndFillContentPanel() {
if (myProjectGenerator instanceof PythonProjectGenerator) {
// Allow generator to display custom error
((PythonProjectGenerator<?>)myProjectGenerator).setErrorCallback(this::setErrorText);
}
return createContentPanelWithAdvancedSettingsPanel();
}
@@ -79,8 +106,9 @@ public class ProjectSpecificSettingsStep extends ProjectSettingsStepBase impleme
@Nullable
protected JPanel createAdvancedSettings() {
JComponent advancedSettings = null;
if (myProjectGenerator instanceof PythonProjectGenerator)
if (myProjectGenerator instanceof PythonProjectGenerator) {
advancedSettings = ((PythonProjectGenerator)myProjectGenerator).getSettingsPanel(myProjectDirectory);
}
else if (myProjectGenerator instanceof WebProjectTemplate) {
advancedSettings = ((WebProjectTemplate)myProjectGenerator).getPeer().getComponent();
}
@@ -99,6 +127,7 @@ public class ProjectSpecificSettingsStep extends ProjectSettingsStepBase impleme
return null;
}
@Nullable
public Sdk getSdk() {
if (!(myProjectGenerator instanceof PythonProjectGenerator)) return null;
if (mySdk != null) return mySdk;
@@ -125,16 +154,43 @@ public class ProjectSpecificSettingsStep extends ProjectSettingsStepBase impleme
((PythonProjectGenerator)myProjectGenerator).locationChanged(PathUtil.getFileName(path));
}
});
mySdkCombo.getComboBox().addItemListener(e -> {
if (e.getStateChange() == ItemEvent.SELECTED) {
checkValid();
final Runnable checkValidOnSwing = () -> ApplicationManager.getApplication().invokeLater(this::checkValid);
ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
try {
// Refresh before validation to make sure no stale data
final Sdk sdk = getSdk();
if (sdk == null) {
return;
}
final boolean noPackages = PyPackageManager.getInstance(sdk).refreshAndGetPackages(true).isEmpty();
if (noPackages) {
LOGGER.warn(String.format("No packages on %s", sdk.getHomePath()));
}
checkValidOnSwing.run();
}
catch (final ExecutionException exception) {
LOGGER.warn(exception);
checkValidOnSwing.run();
}
}, "Refreshing List of Packages, Please Wait", false, null);
}
});
UiNotifyConnector.doWhenFirstShown(mySdkCombo, this::checkValid);
}
}
/**
* @return path for project on remote side provided by user
*/
@Nullable
final String getRemotePath() {
final PyRemotePathField field = myRemotePathField;
return (field != null ? field.getTextField().getText() : null);
}
@Override
protected void initGeneratorListeners() {
super.initGeneratorListeners();
@@ -160,17 +216,25 @@ public class ProjectSpecificSettingsStep extends ProjectSettingsStepBase impleme
}
return true;
}
else if (PythonSdkType.isInvalid(sdk)) {
if (PythonSdkType.isInvalid(sdk)) {
setErrorText("Choose valid python interpreter");
return false;
}
final List<String> warningList = new ArrayList<>();
final boolean isPy3k = PythonSdkType.getLanguageLevelForSdk(sdk).isPy3K();
if (PythonSdkType.isRemote(sdk) && !acceptsRemoteSdk(myProjectGenerator)) {
setErrorText("Please choose a local interpreter");
try {
acceptsSdk(myProjectGenerator, sdk, new File(myLocationField.getText()));
}
catch (final PythonProjectGenerator.PyNoProjectAllowedOnSdkException e) {
setErrorText(e.getMessage());
return false;
}
else if (myProjectGenerator instanceof PyFrameworkProjectGenerator) {
if (myRemotePathRequired && StringUtil.isEmpty(myRemotePathField.getTextField().getText())) {
setErrorText("Remote path not provided");
return false;
}
if (myProjectGenerator instanceof PyFrameworkProjectGenerator) {
PyFrameworkProjectGenerator frameworkProjectGenerator = (PyFrameworkProjectGenerator)myProjectGenerator;
String frameworkName = frameworkProjectGenerator.getFrameworkTitle();
if (!isFrameworkInstalled(sdk)) {
@@ -186,9 +250,9 @@ public class ProjectSpecificSettingsStep extends ProjectSettingsStepBase impleme
}
else {
warningList.add(frameworkName + " will be installed on the selected interpreter");
}
} else {
}
else {
warningList.add(frameworkName + " is not installed on the selected interpreter");
}
}
@@ -236,6 +300,14 @@ public class ProjectSpecificSettingsStep extends ProjectSettingsStepBase impleme
panel.add(labeled);
}
final PythonRemoteInterpreterManager remoteInterpreterManager = PythonRemoteInterpreterManager.getInstance();
final Sdk sdk = getSdk();
if (remoteInterpreterManager != null && sdk != null) {
createRemotePathField(panel, remoteInterpreterManager);
}
final JPanel basePanelExtension = ((PythonProjectGenerator)myProjectGenerator).extendBasePanel();
if (basePanelExtension != null) {
panel.add(basePanelExtension);
@@ -246,10 +318,81 @@ public class ProjectSpecificSettingsStep extends ProjectSettingsStepBase impleme
return super.createBasePanel();
}
private void createRemotePathField(@NotNull final JPanel panelToAddField,
@NotNull final PythonRemoteInterpreterManager remoteInterpreterManager) {
myRemotePathField = new PyRemotePathField();
myRemotePathField.addActionListener(e -> {
final Sdk currentSdk = getSdk();
if (!PySdkUtil.isRemote(currentSdk)) {
return;
}
// If chosen SDK is remote then display
final Pair<Supplier<String>, JPanel> browserForm;
try {
browserForm = remoteInterpreterManager.createServerBrowserForm(currentSdk);
}
catch (final ExecutionException | InterruptedException ex) {
Logger.getInstance(ProjectSpecificSettingsStep.class).warn("Failed to create server browse button", ex);
JBPopupFactory.getInstance().createMessage("Failed to browse remote server. Make sure you have permissions. ").show(panelToAddField);
return;
}
if (browserForm != null) {
browserForm.second.setVisible(true);
final DialogWrapper wrapper = new MyRemoteServerBrowserDialog(browserForm.second);
if (wrapper.showAndGet()) {
myRemotePathField.getTextField().setText(browserForm.first.get());
}
}
});
mySdkCombo.addChangedListener(e -> configureMappingField(remoteInterpreterManager));
panelToAddField.add(myRemotePathField.getMainPanel());
myRemotePathField.addTextChangeListener(() -> checkValid());
configureMappingField(remoteInterpreterManager);
}
/**
* Enables or disables "remote path" based on interpreter.
*/
private void configureMappingField(@NotNull final PythonRemoteInterpreterManager remoteInterpreterManager) {
if (myRemotePathField == null) {
return;
}
final JPanel mainPanel = myRemotePathField.getMainPanel();
final PyProjectSynchronizer synchronizer = getSynchronizer(remoteInterpreterManager);
if (synchronizer != null) {
final String defaultRemotePath = synchronizer.getDefaultRemotePath();
final boolean mappingRequired = defaultRemotePath != null;
mainPanel.setVisible(mappingRequired);
final TextAccessor textField = myRemotePathField.getTextField();
if (mappingRequired && StringUtil.isEmpty(textField.getText())) {
textField.setText(defaultRemotePath);
}
myRemotePathRequired = mappingRequired;
}
else {
mainPanel.setVisible(false);
myRemotePathRequired = false;
}
}
@Nullable
private PyProjectSynchronizer getSynchronizer(@NotNull final PythonRemoteInterpreterManager manager) {
final Sdk sdk = getSdk();
if (sdk == null) {
return null;
}
return manager.getSynchronizer(sdk);
}
private void addInterpreterButton(final JPanel locationPanel, final LabeledComponent<TextFieldWithBrowseButton> location) {
final JButton interpreterButton = new FixedSizeButton(location);
if (SystemInfo.isMac && !UIUtil.isUnderDarcula())
if (SystemInfo.isMac && !UIUtil.isUnderDarcula()) {
interpreterButton.putClientProperty("JButton.buttonType", null);
}
interpreterButton.setIcon(PythonIcons.Python.Python);
interpreterButton.addActionListener(new ActionListener() {
@Override
@@ -289,10 +432,31 @@ public class ProjectSpecificSettingsStep extends ProjectSettingsStepBase impleme
final Sdk preferred = compatibleSdk;
mySdkCombo = new PythonSdkChooserCombo(project, sdks, sdk -> sdk == preferred);
if (SystemInfo.isMac && !UIUtil.isUnderDarcula())
if (SystemInfo.isMac && !UIUtil.isUnderDarcula()) {
mySdkCombo.putClientProperty("JButton.buttonType", null);
}
mySdkCombo.setButtonIcon(PythonIcons.Python.InterpreterGear);
return LabeledComponent.create(mySdkCombo, "Interpreter", BorderLayout.WEST);
}
/**
* Dialog to display remote server browser
*/
private static class MyRemoteServerBrowserDialog extends DialogWrapper {
private final JPanel myBrowserForm;
MyRemoteServerBrowserDialog(@NotNull final JPanel browserForm) {
super(true);
myBrowserForm = browserForm;
init();
}
@Nullable
@Override
protected JComponent createCenterPanel() {
return myBrowserForm;
}
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.jetbrains.python.newProject.actions.PyRemotePathField">
<grid id="27dc6" binding="myMainPanel" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<vspacer id="8ed31">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="33807" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Remote project location:"/>
</properties>
</component>
<component id="b1f9a" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myLocationField">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
</children>
</grid>
</form>

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.newProject.actions;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.ui.TextAccessor;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.event.ActionListener;
/**
* Field for remote path with "browse" button
*
* @author Ilya.Kazakevich
*/
public final class PyRemotePathField {
private JPanel myMainPanel;
private TextFieldWithBrowseButton myLocationField;
@NotNull
JPanel getMainPanel() {
return myMainPanel;
}
/**
* Add listener for "browse" button
*/
void addActionListener(@NotNull final ActionListener listener) {
myLocationField.addActionListener(listener);
}
/**
* @param runnable to be called when text in textfield changed
*/
void addTextChangeListener(@NotNull final Runnable runnable) {
myLocationField.getTextField().getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
runnable.run();
}
@Override
public void removeUpdate(DocumentEvent e) {
runnable.run();
}
@Override
public void changedUpdate(DocumentEvent e) {
runnable.run();
}
});
}
/**
* @return test field with remote path
*/
@NotNull
TextAccessor getTextField() {
return myLocationField;
}
}

View File

@@ -119,17 +119,20 @@ public class PythonGenerateProjectCallback implements NullableConsumer<ProjectSe
file -> computeProjectSettings(generator, (ProjectSpecificSettingsStep)settings));
}
public static Object computeProjectSettings(DirectoryProjectGenerator generator, ProjectSpecificSettingsStep settings) {
public static Object computeProjectSettings(DirectoryProjectGenerator<?> generator, final ProjectSpecificSettingsStep settings) {
Object projectSettings = null;
if (generator instanceof PythonProjectGenerator) {
projectSettings = ((PythonProjectGenerator)generator).getProjectSettings();
final PythonProjectGenerator<?> projectGenerator = (PythonProjectGenerator<?>)generator;
projectSettings = projectGenerator.getProjectSettings();
}
else if (generator instanceof WebProjectTemplate) {
projectSettings = ((WebProjectTemplate)generator).getPeer().getSettings();
projectSettings = ((WebProjectTemplate<?>)generator).getPeer().getSettings();
}
if (projectSettings instanceof PyNewProjectSettings) {
((PyNewProjectSettings)projectSettings).setSdk(settings.getSdk());
((PyNewProjectSettings)projectSettings).setInstallFramework(settings.installFramework());
final PyNewProjectSettings newProjectSettings = (PyNewProjectSettings)projectSettings;
newProjectSettings.setSdk(settings.getSdk());
newProjectSettings.setInstallFramework(settings.installFramework());
newProjectSettings.setRemotePath(settings.getRemotePath());
}
return projectSettings;
}

View File

@@ -26,7 +26,5 @@ public interface PyFrameworkProjectGenerator {
boolean isFrameworkInstalled(Sdk sdk);
boolean acceptsRemoteSdk();
boolean supportsPython3();
}

View File

@@ -16,6 +16,7 @@
package com.jetbrains.python.newProject;
import com.intellij.openapi.projectRoots.Sdk;
import org.jetbrains.annotations.Nullable;
/**
* Project generation settings selected on the first page of the new project dialog.
@@ -25,6 +26,11 @@ import com.intellij.openapi.projectRoots.Sdk;
public class PyNewProjectSettings {
private Sdk mySdk;
private boolean myInstallFramework;
/**
* Path on remote server for remote project
*/
@Nullable
private String myRemotePath;
public Sdk getSdk() {
return mySdk;
@@ -41,4 +47,13 @@ public class PyNewProjectSettings {
public boolean installFramework() {
return myInstallFramework;
}
public final void setRemotePath(@Nullable final String remotePath) {
myRemotePath = remotePath;
}
@Nullable
public final String getRemotePath() {
return myRemotePath;
}
}

View File

@@ -16,16 +16,18 @@
package com.jetbrains.python.newProject;
import com.intellij.facet.ui.ValidationResult;
import com.intellij.icons.AllIcons.General;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.platform.DirectoryProjectGenerator;
import com.intellij.util.BooleanFunction;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.remote.PythonRemoteInterpreterManager;
import com.jetbrains.python.sdk.PythonSdkType;
import com.jetbrains.python.remote.*;
import com.jetbrains.python.sdk.PySdkUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -33,18 +35,40 @@ import javax.swing.*;
import java.awt.event.MouseListener;
import java.io.File;
import java.util.List;
import java.util.function.Consumer;
/**
* This class encapsulates remote settings, so one should extend it for any python project that supports remote generation, at least
* Instead of {@link #generateProject(Project, VirtualFile, PyNewProjectSettings, Module)} inheritor shall use
* {@link #configureProject(Project, VirtualFile, PyNewProjectSettings, Module)}
* {@link #configureProject(Project, VirtualFile, PyNewProjectSettings, Module, PyProjectSynchronizer)}*
* <br/>
* If your project does not support remote projects generation, be sure to set flag in ctor:{@link #PythonProjectGenerator(boolean)}
* <br/>
*
* @param <T> project settings
*/
public abstract class PythonProjectGenerator<T extends PyNewProjectSettings> implements DirectoryProjectGenerator<T> {
private final List<SettingsListener> myListeners = ContainerUtil.newArrayList();
private final boolean myAllowRemoteProjectCreation;
@Nullable private MouseListener myErrorLabelMouseListener;
protected Consumer<String> myErrorCallback;
protected PythonProjectGenerator() {
this(false);
}
/**
* @param allowRemoteProjectCreation if project of this type could be created remotely
*/
protected PythonProjectGenerator(final boolean allowRemoteProjectCreation) {
myAllowRemoteProjectCreation = allowRemoteProjectCreation;
}
public final void setErrorCallback(@NotNull final Consumer<String> errorCallback) {
myErrorCallback = errorCallback;
}
@Nullable
public JComponent getSettingsPanel(File baseDir) throws ProcessCanceledException {
return null;
@@ -55,29 +79,113 @@ public abstract class PythonProjectGenerator<T extends PyNewProjectSettings> imp
return null;
}
/**
* Checks if project type and remote ask allows project creation.
* Throws exception with reason if can't
*
* @param sdk sdk to check
* @param projectDirectory base project directory
* @throws PyNoProjectAllowedOnSdkException project can't be created (check message)
*/
public void checkProjectCanBeCreatedOnSdk(@NotNull final Sdk sdk,
@NotNull final File projectDirectory) throws PyNoProjectAllowedOnSdkException {
// Check if project does not support remote creation at all
if (!myAllowRemoteProjectCreation && PySdkUtil.isRemote(sdk)) {
throw new PyNoProjectAllowedOnSdkException(
"Can't create project of this type on remote interpreter. Choose local interpreter.");
}
// Check if project synchronizer could be used with this project dir
// No project can be created remotely if project synchronizer can't work with it
final PythonRemoteInterpreterManager remoteManager = PythonRemoteInterpreterManager.getInstance();
if (remoteManager == null) {
return;
}
final PyProjectSynchronizer synchronizer = remoteManager.getSynchronizer(sdk);
if (synchronizer == null) {
return;
}
final String syncError = synchronizer.checkSynchronizationAvailable(new PySyncCheckOnly(projectDirectory));
if (syncError != null) {
throw new PyNoProjectAllowedOnSdkException(syncError);
}
}
@Override
public final void generateProject(@NotNull final Project project,
@NotNull final VirtualFile baseDir,
@Nullable final T settings,
@NotNull final Module module) {
assert settings != null : "No project settings provided";
/*Instead of this method overwrite ``configureProject``*/
// If we deal with remote project -- use remote manager to configure it
final PythonRemoteInterpreterManager remoteManager = PythonRemoteInterpreterManager.getInstance();
final Sdk sdk = (settings != null ? settings.getSdk() : null);
if (remoteManager != null && PythonSdkType.isRemote(sdk)) {
remoteManager.prepareRemoteSettingsIfNeeded(module, sdk);
final Sdk sdk = settings.getSdk();
final PyProjectSynchronizer synchronizer = (remoteManager != null ? remoteManager.getSynchronizer(sdk) : null);
if (synchronizer != null) {
// Before project creation we need to configure sync
// We call "checkSynchronizationAvailable" until it returns success (means sync is available)
// Or user confirms she does not need sync
String userProvidedPath = settings.getRemotePath();
while (true) {
final String syncError = synchronizer.checkSynchronizationAvailable(new PySyncCheckCreateIfPossible(module, userProvidedPath));
if (syncError == null) {
break;
}
userProvidedPath = null; // According to checkSynchronizationAvailable should be cleared
final String message = String.format("Local/Remote synchronization is not configured correctly.\n%s\n" +
"You may need to sync local and remote project manually.\n\n Do you want to continue? \n\n" +
"Say 'Yes' to stay with misconfigured mappings or 'No' to start manual configuration process.",
syncError);
if (Messages.showYesNoDialog(project,
message,
"Synchronization not Configured",
General.WarningDialog) == Messages.YES) {
break;
}
}
}
configureProject(project, baseDir, settings, module);
configureProject(project, baseDir, settings, module, synchronizer);
}
/**
* Does real work to generate project
* Does real work to generate project.
* Parent class does its best to handle remote interpreters.
* Inheritors should only create project.
* To support remote project creation, be sure to use {@link PyProjectSynchronizer}.
* <br/>
* When overwriting this method, <strong>be sure</strong> to call super() or call
* {@link PyProjectSynchronizer#syncProject(Module, PySyncDirection, Consumer)} at least once: automatic sync works only after it.
*
* @param synchronizer null if project is local and no sync required.
* Otherwise, be sure to use it move code between local (java) and remote (python) side.
* Remote interpreters can't be used with out of it. Contract is following:
* <ol>
* <li>Create some code on python (remote) side using helpers</li>
* <li>call {@link PyProjectSynchronizer#syncProject(Module, PySyncDirection, Consumer)}</li>
* <li>Change locally</li>
* <li>call {@link PyProjectSynchronizer#syncProject(Module, PySyncDirection, Consumer)} again in opposite direction</li>
* </ol>
*/
protected abstract void configureProject(@NotNull final Project project,
@NotNull final VirtualFile baseDir,
@Nullable final T settings,
@NotNull final Module module);
protected void configureProject(@NotNull final Project project,
@NotNull final VirtualFile baseDir,
@NotNull final T settings,
@NotNull final Module module,
@Nullable final PyProjectSynchronizer synchronizer) {
// Automatic deployment works only after first sync
if (synchronizer != null) {
synchronizer.syncProject(module, PySyncDirection.JAVA_TO_PYTHON, null);
}
}
public Object getProjectSettings() {
return new PyNewProjectSettings();
@@ -124,4 +232,17 @@ public abstract class PythonProjectGenerator<T extends PyNewProjectSettings> imp
public void createAndAddVirtualEnv(Project project, PyNewProjectSettings settings) {
}
/**
* To be thrown if project can't be created on this sdk
* @author Ilya.Kazakevich
*/
public static class PyNoProjectAllowedOnSdkException extends Exception {
/**
* @param reason why project can't be created
*/
PyNoProjectAllowedOnSdkException(@NotNull final String reason) {
super(reason);
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.remote
import com.intellij.openapi.module.Module
import java.io.File
import java.util.function.Consumer
/**
* ProjectSynchronizer is an engine that synchronize code between local and remote system or between java (which is local)
* and python (which may be remote).
* This engine is sdk-specific and used by [com.jetbrains.python.newProject.PythonProjectGenerator] (and friends).
*
* When generator creates remote project, it may use python helpers (with aid of tasks) and it may need some way
* to pull remote files, patch them and push 'em back. The way it does it is skd-specific and this interface encapsulates it.
*
* Using this engine makes your generator compatible with remote interpreters.
*
* Project synchronizer is also responsible for project configuration for sync: it cooperates with user to make sure remote project is
* configured correctly.
* @author Ilya.Kazakevich
*/
interface PyProjectSynchronizer {
/**
* Checks if sync is available.
* It supports several strategies: see concrete instance documentation.
*
* @param syncCheckStrategy strategy to check if sync is available.
* Several strategies are supported: see concrete instance documentation.
* @return null if sync is available or error message if something prevents project from sync.
*/
fun checkSynchronizationAvailable(syncCheckStrategy: PySyncCheckStrategy): String?
/**
* @return if remote box allows user to configure remote path, this method returns default path
* that should be shown to user.
* If returns null, user can't configure remote path and GUI should not provide such ability
*/
fun getDefaultRemotePath(): String?
/**
* Synchronizes project.
* @param module current module
* @param syncDirection local-to-remote (aka java-to-python) or opposite. See enum value doc.
* @param callback code to be called after sync completion. Argument tells if sync was success or not.
*/
fun syncProject(module: Module, syncDirection: PySyncDirection,
callback: Consumer<Boolean>?)
}
/**
* Several strategies to be used for [PyProjectSynchronizer.checkSynchronizationAvailable].
* See concrete impls.
*/
interface PySyncCheckStrategy
/**
* Checks if specific folder could be synced with remote interpreter.
* It does not cooperate with user but simply checks folder instead.
*
* Strategy should return "false" only if it is technically impossible to sync with this folder what ever user does.
* If it is possible but requires some aid from user should return true.
*
* No remote project creation would be allowed if this strategy returns "false".
*/
class PySyncCheckOnly(val projectBaseDir: File) : PySyncCheckStrategy
/**
* Checks if project with specific module could be synced with remote server.
* It may contact user taking one through some wizard steps to configure project to support remote interpreter.
* So, it does its best to make project synchronizable.*
*
* @param remotePath user provided remote path. Should only be provided if [PyProjectSynchronizer.getDefaultRemotePath] is not null.
* This argument should only be provided first time. On next call always provide null to prevent infinite loop because
* user will be asked for path only if this argument is null.
*/
class PySyncCheckCreateIfPossible(val module: Module, val remotePath: String? ) : PySyncCheckStrategy
/**
* Local-remote sync direction
*/
enum class PySyncDirection {
/**
* aka local-to-remote
*/
JAVA_TO_PYTHON,
/**
* aka remote-to-local
*/
PYTHON_TO_JAVA,
}

View File

@@ -22,12 +22,12 @@ import com.intellij.execution.configurations.ParamsGroup;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkAdditionalData;
import com.intellij.openapi.projectRoots.SdkModificator;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.remote.*;
@@ -44,10 +44,12 @@ import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;
/**
* @author traff
@@ -136,16 +138,33 @@ public abstract class PythonRemoteInterpreterManager {
RemoteSdkCredentials data);
/**
* Prepares project (i.e. sets appropriate mappings) if sdk is remote.
* Do not call this method if sdk is not remote: id does nothing
* @param sdk current sdk
* @return project synchronizer for this sdk. See {@link PyProjectSynchronizer} for more info
* @see PyProjectSynchronizer
*/
public abstract void prepareRemoteSettingsIfNeeded(@NotNull final Module module,
@NotNull final Sdk sdk);
@Nullable
public abstract PyProjectSynchronizer getSynchronizer(@NotNull final Sdk sdk);
public abstract void copyFromRemote(Sdk sdk, @NotNull Project project,
RemoteSdkCredentials data,
List<PathMappingSettings.PathMapping> mappings);
/**
* Creates form to browse remote box.
* You need to show it to user using dialog.
*
* @return null if remote sdk can't be browsed.
* First argument is consumer to get path, chosen by user.
* Second is panel to display to user
*
* @throws ExecutionException credentials can't be obtained due to remote server error
* @throws InterruptedException credentials can't be obtained due to remote server error
*/
@Nullable
public abstract Pair<Supplier<String>, JPanel> createServerBrowserForm(@NotNull final Sdk remoteSdk)
throws ExecutionException, InterruptedException;
@Nullable
public static PythonRemoteInterpreterManager getInstance() {
if (EP_NAME.getExtensions().length > 0) {