diff --git a/python/ide/impl/resources/META-INF/pycharm-community-customization.xml b/python/ide/impl/resources/META-INF/pycharm-community-customization.xml
index 204543648e70..d1d22990df92 100644
--- a/python/ide/impl/resources/META-INF/pycharm-community-customization.xml
+++ b/python/ide/impl/resources/META-INF/pycharm-community-customization.xml
@@ -57,6 +57,9 @@
+
+
diff --git a/python/ide/impl/src/com/jetbrains/python/PyIdeCommonFragmentsBuilder.kt b/python/ide/impl/src/com/jetbrains/python/PyIdeCommonFragmentsBuilder.kt
new file mode 100644
index 000000000000..90ce09d67b5a
--- /dev/null
+++ b/python/ide/impl/src/com/jetbrains/python/PyIdeCommonFragmentsBuilder.kt
@@ -0,0 +1,75 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python
+
+import com.intellij.application.options.ModulesComboBox
+import com.intellij.execution.ui.CommandLinePanel
+import com.intellij.execution.ui.SettingsEditorFragment
+import com.intellij.execution.ui.SettingsEditorFragmentType
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.module.ModuleManager
+import com.jetbrains.python.run.AbstractPythonRunConfiguration
+import com.jetbrains.python.run.PyCommonFragmentsBuilder
+import com.jetbrains.python.run.configuration.PyPathMappingsEditorFragment
+import com.jetbrains.python.run.configuration.PySdkComboBox
+import com.jetbrains.python.sdk.PySdkListCellRenderer
+import java.awt.event.ItemEvent
+
+class PyIdeCommonFragmentsBuilder : PyCommonFragmentsBuilder() {
+
+ override fun > createEnvironmentFragments(fragments: MutableList>,
+ config: T) {
+ val modules = ModuleManager.getInstance(config.project).modules
+ var modulesComboBox: ModulesComboBox? = null
+ if (modules.size > 1) {
+ modulesComboBox = ModulesComboBox()
+ val modulesFragment = SettingsEditorFragment(
+ "py.ide.project",
+ PyBundle.message("python.run.configuration.fragments.project"),
+ null,
+ modulesComboBox, SettingsEditorFragmentType.COMMAND_LINE,
+ { s: T, modulesCombo: ModulesComboBox ->
+ val validModules: List = config.validModules
+ modulesCombo.setModules(validModules)
+ modulesCombo.selectedModule = config.module
+ },
+ { s: T, modulesCombo: ModulesComboBox ->
+ modulesCombo.selectedModule?.let { selected ->
+ s.module = selected
+ }
+ },
+ { true }
+ )
+ modulesFragment.isRemovable = false
+ modulesFragment.setHint(PyBundle.message("python.run.configuration.fragments.project.hint"))
+ fragments.add(modulesFragment)
+ }
+
+ val sdkComboBox = PySdkComboBox(true) { modulesComboBox?.selectedModule }
+ val minimumSize = CommandLinePanel.setMinimumWidth(sdkComboBox, 400)
+ sdkComboBox.preferredSize = minimumSize
+ sdkComboBox.renderer = PySdkListCellRenderer()
+ val interpreterFragment: SettingsEditorFragment = SettingsEditorFragment(
+ "py.ide.interpreter",
+ PyBundle.message("python.run.configuration.fragments.interpreter.field"),
+ null,
+ sdkComboBox, SettingsEditorFragmentType.COMMAND_LINE,
+ { s: T, c: PySdkComboBox -> c.reset(s) },
+ { s: T, c: PySdkComboBox -> c.apply(s) },
+ { true })
+ interpreterFragment.isRemovable = false
+ interpreterFragment.setHint(PyBundle.message("python.run.configuration.fragments.interpreter.field"))
+ fragments.add(interpreterFragment)
+
+ modulesComboBox?.addItemListener { e ->
+ if (e?.stateChange == ItemEvent.SELECTED) {
+ (e.item as? Module)?.let {
+ sdkComboBox.setModule(it)
+ }
+ }
+ }
+
+ fragments.add(createWorkingDirectoryFragment(config.project))
+ fragments.add(createEnvParameters())
+ fragments.add(PyPathMappingsEditorFragment(sdkComboBox))
+ }
+}
\ No newline at end of file
diff --git a/python/pluginCore/impl/resources/META-INF/python-community-plugin-core.xml b/python/pluginCore/impl/resources/META-INF/python-community-plugin-core.xml
index a9b9537b72ac..c24f1f3f2fb3 100644
--- a/python/pluginCore/impl/resources/META-INF/python-community-plugin-core.xml
+++ b/python/pluginCore/impl/resources/META-INF/python-community-plugin-core.xml
@@ -5,6 +5,8 @@
+
diff --git a/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginCommonFragmentsBuilder.kt b/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginCommonFragmentsBuilder.kt
new file mode 100644
index 000000000000..60d1943e5b86
--- /dev/null
+++ b/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginCommonFragmentsBuilder.kt
@@ -0,0 +1,19 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.run
+
+import com.intellij.execution.ui.SettingsEditorFragment
+import com.jetbrains.python.run.configuration.PyInterpreterModeNotifier
+import com.jetbrains.python.run.configuration.PyPathMappingsEditorFragment
+import javax.swing.JPanel
+
+class PyPluginCommonFragmentsBuilder: PyCommonFragmentsBuilder() {
+ override fun > createEnvironmentFragments(fragments: MutableList>,
+ config: T) {
+ val sdkFragment: SettingsEditorFragment = PyPluginSdkFragment()
+ fragments.add(sdkFragment)
+
+ fragments.add(createWorkingDirectoryFragment(config.project))
+ fragments.add(createEnvParameters())
+ fragments.add(PyPathMappingsEditorFragment(sdkFragment as PyInterpreterModeNotifier))
+ }
+}
\ No newline at end of file
diff --git a/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginSdkFragment.kt b/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginSdkFragment.kt
new file mode 100644
index 000000000000..d8bdaef94508
--- /dev/null
+++ b/python/pluginCore/impl/src/com/jetbrains/python/run/PyPluginSdkFragment.kt
@@ -0,0 +1,165 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.run
+
+import com.intellij.application.options.ModulesComboBox
+import com.intellij.execution.ui.CommandLinePanel
+import com.intellij.execution.ui.SettingsEditorFragment
+import com.intellij.execution.ui.SettingsEditorFragmentType
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.roots.ModuleRootManager
+import com.intellij.openapi.ui.ComboBox
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.run.configuration.PyInterpreterModeNotifier
+import com.jetbrains.python.run.configuration.PySdkComboBox
+import com.jetbrains.python.sdk.PythonSdkUtil
+import java.awt.GridBagConstraints
+import java.awt.GridBagLayout
+import java.awt.event.ItemEvent
+import java.util.function.Consumer
+import javax.swing.JComboBox
+import javax.swing.JComponent
+import javax.swing.JPanel
+
+class PyPluginSdkFragment> : SettingsEditorFragment(
+ "py.plugin.interpreter", null, null, JPanel(GridBagLayout()), SettingsEditorFragmentType.COMMAND_LINE, null, null,
+ { true }), PyInterpreterModeNotifier {
+
+ private val typeNames = listOf(PyBundle.message("python.run.configuration.fragments.plugin.sdk.of.module"),
+ PyBundle.message("python.run.configuration.fragments.plugin.specified.interpreter"))
+ private val SDK_OF_MODULE = 0
+ private val SDK_FROM_LIST = 1
+ private var currentMode: Int? = null
+
+ private var modulesCombo: ModulesComboBox? = null
+ private var interpreterCombo: PySdkComboBox? = null
+
+ val fields: MutableList = MutableList(SDK_FROM_LIST + 1) { null }
+ private val hints: MutableMap = mutableMapOf()
+ private val sdkChooser: JComboBox
+ private val interpreterModeListeners: MutableList> = mutableListOf()
+
+ init {
+ sdkChooser = JComboBox()
+ sdkChooser.addItem(typeNames[SDK_OF_MODULE])
+ sdkChooser.addItem(typeNames[SDK_FROM_LIST])
+ CommandLinePanel.setMinimumWidth(sdkChooser, 200)
+ component().add(sdkChooser, GridBagConstraints())
+ hints[sdkChooser] = PyBundle.message("python.run.configuration.fragments.plugin.sdk.chooser.hint")
+
+ sdkChooser.addItemListener { e ->
+ if (e?.stateChange == ItemEvent.SELECTED) {
+ if ((e.item as? String) == typeNames[SDK_FROM_LIST]) {
+ showField(SDK_FROM_LIST)
+ currentMode = SDK_FROM_LIST
+ }
+ else {
+ showField(SDK_OF_MODULE)
+ currentMode = SDK_OF_MODULE
+ }
+ }
+ }
+ sdkChooser.addActionListener { updateRemoteInterpreterMode() }
+ }
+
+ override fun resetEditorFrom(config: T) {
+ if (currentMode == null) {
+ initComponents(config)
+ }
+ val type = if (config.isUseModuleSdk) SDK_OF_MODULE else SDK_FROM_LIST
+ if (currentMode != type) {
+ sdkChooser.selectedItem = typeNames[type]
+ (fields[SDK_OF_MODULE] as? ComboBox<*>)?.selectedItem = config.module
+ interpreterCombo?.reset(config)
+ showField(type)
+ }
+ updateRemoteInterpreterMode()
+ }
+
+ private fun showField(type: Int) {
+ for (i in 0 until fields.size) {
+ fields[i]?.isVisible = i == type
+ }
+ }
+
+ private fun initComponents(config: T) {
+ val modulesComponent = ModulesComboBox()
+ initComponent(modulesComponent, SDK_OF_MODULE)
+ val validModules: List = config.commonOptionsFormData.validModules
+ modulesComponent.setModules(validModules)
+ modulesComponent.selectedModule = config.module
+ fields[SDK_OF_MODULE] = modulesComponent
+ modulesCombo = modulesComponent
+ CommandLinePanel.setMinimumWidth(modulesCombo, 400)
+ hints[modulesComponent] = PyBundle.message("python.run.configuration.fragments.plugin.sdk.of.module.hint")
+ modulesCombo?.addActionListener { updateRemoteInterpreterMode() }
+
+ val interpreterComponent = PySdkComboBox(false) { modulesComponent.selectedModule }
+ initComponent(interpreterComponent, SDK_FROM_LIST)
+ interpreterComponent.reset(config)
+ fields[SDK_FROM_LIST] = interpreterComponent
+ interpreterCombo = interpreterComponent
+ CommandLinePanel.setMinimumWidth(interpreterCombo, 400)
+ hints[interpreterComponent] = PyBundle.message("python.run.configuration.fragments.plugin.specified.interpreter.hint")
+ interpreterCombo?.addActionListener { updateRemoteInterpreterMode() }
+ }
+
+ private fun initComponent(field: JComponent, index: Int) {
+ val constraints = GridBagConstraints()
+ constraints.fill = GridBagConstraints.BOTH
+ constraints.weightx = 1.0
+ component().add(field, constraints)
+ fields[index] = field
+ }
+
+ override fun applyEditorTo(s: T) {
+ if (currentMode == SDK_OF_MODULE) {
+ s.isUseModuleSdk = true
+ val module = (modulesCombo?.selectedItem) as? Module
+ s.module = module
+ module?.let {
+ val moduleSdk = ModuleRootManager.getInstance(it).sdk
+ s.setSdk(moduleSdk)
+ }
+ }
+ else if (currentMode == SDK_FROM_LIST) {
+ s.isUseModuleSdk = false
+ interpreterCombo?.apply(s)
+ }
+ }
+
+ override fun isRemovable(): Boolean = false
+
+ override fun getAllComponents(): Array {
+ return fields.filterNotNull().toTypedArray() + sdkChooser
+ }
+
+ override fun getHint(component: JComponent?): String? {
+ return if (component == null) null else hints[component]
+ }
+
+ override fun isRemoteSelected(): Boolean = PythonSdkUtil.isRemote(getSelectedSdk())
+
+ override fun addInterpreterModeListener(listener: Consumer) {
+ interpreterModeListeners.add(listener)
+ }
+
+ private fun updateRemoteInterpreterMode() {
+ val isRemote = isRemoteSelected()
+ for (listener in interpreterModeListeners) {
+ listener.accept(isRemote)
+ }
+ }
+
+ private fun getSelectedSdk(): Sdk? {
+ if (currentMode == SDK_OF_MODULE) {
+ ((modulesCombo?.selectedItem) as? Module)?.let {
+ return ModuleRootManager.getInstance(it).sdk
+ }
+ }
+ else if (currentMode == SDK_FROM_LIST) {
+ return interpreterCombo?.getSelectedSdk()
+ }
+ return null
+ }
+}
\ No newline at end of file
diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties
index 4186388c6126..a028739818cf 100644
--- a/python/pluginResources/messages/PyBundle.properties
+++ b/python/pluginResources/messages/PyBundle.properties
@@ -1030,6 +1030,31 @@ form.tox.configuration.environments.to.run=Environments to run:
form.tox.configuration.runcfg.tox=tox
py.module.dependencies.configurable.list.title=Project depends on these projects:
py.sdk.editor.python.interpreter.label.text=Python Interpreter:
+
+python.run.configuration.fragments.chooser.hint=Run Python script or module
+python.run.configuration.fragments.script.path.hint=Path to a Python script
+python.run.configuration.fragments.module.name.hint=Name of a module
+python.run.configuration.fragments.project=Project
+python.run.configuration.fragments.project.hint=Select a project
+python.run.configuration.fragments.interpreter.field=Python interpreter
+python.run.configuration.fragments.interpreter.options=Interpreter options
+python.run.configuration.fragments.interpreter.options.hint=Python interpreter options
+python.run.configuration.fragments.script.parameters=Parameters
+python.run.configuration.fragments.script.parameters.hint=Script parameters
+python.run.configuration.fragments.run.with.python.console=Run with Python Console
+python.run.configuration.fragments.run.with.python.console.hint=Execute run configuration in Python Console
+python.run.configuration.fragments.emulate.terminal=Emulate terminal in output console
+python.run.configuration.fragments.emulate.terminal.hint=Emulate terminal in output console
+python.run.configuration.fragments.content.roots=Add content roots to PYTHONPATH
+python.run.configuration.fragments.content.roots.hint=Add content roots to PYTHONPATH
+python.run.configuration.fragments.source.roots=Add source roots to PYTHONPATH
+python.run.configuration.fragments.source.roots.hint=Add source roots to PYTHONPATH
+python.run.configuration.fragments.python.group=Python
+python.run.configuration.fragments.plugin.sdk.chooser.hint=Python interpreter
+python.run.configuration.fragments.plugin.sdk.of.module=Use SDK of module
+python.run.configuration.fragments.plugin.sdk.of.module.hint=Select module
+python.run.configuration.fragments.plugin.specified.interpreter=Use specified interpreter
+python.run.configuration.fragments.plugin.specified.interpreter.hint=Select Python interpreter
# Python reStructuredText forms
rest.configuration.editor.open.output.file.in.browser.label.text=Open output file in browser
sphinx.ask.for.working.directory.label.text=Sphinx working directory
diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml
index ac8a734cdf38..b5e79fa2735e 100644
--- a/python/src/META-INF/python-core-common.xml
+++ b/python/src/META-INF/python-core-common.xml
@@ -508,6 +508,8 @@
+
+
diff --git a/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java b/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java
index 4f76634eef13..92f794ce395f 100644
--- a/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java
+++ b/python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java
@@ -19,6 +19,7 @@ import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.JDOMExternalizerUtil;
import com.intellij.openapi.util.WriteExternalException;
+import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.PathMappingSettings;
@@ -119,6 +120,11 @@ public abstract class AbstractPythonRunConfiguration getConfigurationEditor() {
+ if (Registry.is("ide.new.run.config", false)) {
+ // TODO: actually, we should return result of `PythonExtendedConfigurationEditor.create()` call, but it produces side effects
+ // investigation needed PY-17716
+ return createConfigurationEditor();
+ }
final SettingsEditor runConfigurationEditor = PythonExtendedConfigurationEditor.create(createConfigurationEditor());
final SettingsEditorGroup group = new SettingsEditorGroup<>();
diff --git a/python/src/com/jetbrains/python/run/PyCommonFragmentsBuilder.kt b/python/src/com/jetbrains/python/run/PyCommonFragmentsBuilder.kt
new file mode 100644
index 000000000000..9b667c78cf71
--- /dev/null
+++ b/python/src/com/jetbrains/python/run/PyCommonFragmentsBuilder.kt
@@ -0,0 +1,80 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.run
+
+import com.intellij.execution.ExecutionBundle
+import com.intellij.execution.configuration.EnvironmentVariablesComponent
+import com.intellij.execution.ui.CommonParameterFragments
+import com.intellij.execution.ui.SettingsEditorFragment
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.LabeledComponent
+import com.intellij.openapi.ui.TextComponentAccessor
+import com.intellij.openapi.ui.TextFieldWithBrowseButton
+import com.intellij.openapi.util.Predicates
+import com.intellij.ui.components.fields.ExtendableTextField
+import java.awt.BorderLayout
+import javax.swing.JComponent
+
+@Service
+abstract class PyCommonFragmentsBuilder {
+ abstract fun > createEnvironmentFragments(fragments: MutableList>,
+ config: T)
+
+ fun > createWorkingDirectoryFragment(project: Project):
+ SettingsEditorFragment> {
+ val textField = ExtendableTextField(10)
+ val workingDirectoryField = TextFieldWithBrowseButton(textField)
+ workingDirectoryField.addBrowseFolderListener(
+ ExecutionBundle.message("select.working.directory.message"),
+ null,
+ project,
+ FileChooserDescriptorFactory.createSingleFolderDescriptor(),
+ TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT
+ )
+ val field = LabeledComponent.create(
+ workingDirectoryField,
+ ExecutionBundle.message("run.configuration.working.directory.label"),
+ BorderLayout.WEST
+ )
+ val workingDirectorySettings = SettingsEditorFragment(
+ "workingDirectory",
+ ExecutionBundle.message("run.configuration.working.directory.name"), null, field,
+ { config: T, component: LabeledComponent -> component.component.text = config.workingDirectory },
+ { config: T, component: LabeledComponent -> config.workingDirectory = component.component.text },
+ Predicates.alwaysTrue()
+ )
+ workingDirectorySettings.isRemovable = false
+ return workingDirectorySettings
+ }
+
+ fun > createEnvParameters(): SettingsEditorFragment {
+ val env = EnvironmentVariablesComponent()
+ env.labelLocation = BorderLayout.WEST
+ CommonParameterFragments.setMonospaced(env.component.textField)
+ val fragment = SettingsEditorFragment(
+ "environmentVariables",
+ ExecutionBundle.message("environment.variables.fragment.name"),
+ ExecutionBundle.message("group.operating.system"), env,
+ { config: T, c: JComponent? ->
+ env.envs = config.envs
+ env.isPassParentEnvs = config.isPassParentEnvs
+ },
+ { config: T, _: JComponent? ->
+ if (!env.isVisible) {
+ config.envs = emptyMap()
+ config.isPassParentEnvs = true
+ }
+ else {
+ config.envs = env.envs
+ config.isPassParentEnvs = env.isPassParentEnvs
+ }
+ },
+ { true })
+ fragment.isCanBeHidden = true
+ fragment.setHint(ExecutionBundle.message("environment.variables.fragment.hint"))
+ fragment.actionHint = ExecutionBundle.message("set.custom.environment.variables.for.the.process")
+ return fragment
+ }
+
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/run/PythonRunConfiguration.java b/python/src/com/jetbrains/python/run/PythonRunConfiguration.java
index e1a40ca7dc11..8ec03a1fd903 100644
--- a/python/src/com/jetbrains/python/run/PythonRunConfiguration.java
+++ b/python/src/com/jetbrains/python/run/PythonRunConfiguration.java
@@ -11,6 +11,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.JDOMExternalizerUtil;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
@@ -18,6 +19,7 @@ import com.intellij.psi.PsiFile;
import com.intellij.refactoring.listeners.RefactoringElementAdapter;
import com.intellij.refactoring.listeners.RefactoringElementListener;
import com.jetbrains.python.PyBundle;
+import com.jetbrains.python.run.configuration.PythonConfigurationFragmentedEditor;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -54,6 +56,9 @@ public class PythonRunConfiguration extends AbstractPythonRunConfiguration
@Override
protected SettingsEditor extends RunConfiguration> createConfigurationEditor() {
+ if (Registry.is("python.new.run.config", false)) {
+ return new PythonConfigurationFragmentedEditor(this);
+ }
return new PythonRunConfigurationEditor(this);
}
diff --git a/python/src/com/jetbrains/python/run/configuration/AbstractPythonConfigurationFragmentedEditor.kt b/python/src/com/jetbrains/python/run/configuration/AbstractPythonConfigurationFragmentedEditor.kt
new file mode 100644
index 000000000000..bf5ae3fb0a5a
--- /dev/null
+++ b/python/src/com/jetbrains/python/run/configuration/AbstractPythonConfigurationFragmentedEditor.kt
@@ -0,0 +1,76 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.run.configuration
+
+import com.intellij.diagnostic.logging.LogsGroupFragment
+import com.intellij.execution.ui.*
+import com.intellij.openapi.components.service
+import com.intellij.ui.RawCommandLineEditor
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.run.AbstractPythonRunConfiguration
+import com.jetbrains.python.run.PyCommonFragmentsBuilder
+import com.jetbrains.python.run.PythonRunConfigurationExtensionsManager
+
+abstract class AbstractPythonConfigurationFragmentedEditor>(val runConfiguration: T) :
+ RunConfigurationFragmentedEditor(runConfiguration, PythonRunConfigurationExtensionsManager.instance) {
+
+ override fun createRunFragments(): MutableList> {
+ val fragments: MutableList> = ArrayList()
+ val beforeRunComponent = BeforeRunComponent(this)
+ fragments.add(BeforeRunFragment.createBeforeRun(beforeRunComponent, null))
+ fragments.addAll(BeforeRunFragment.createGroup())
+ fragments.add(CommonParameterFragments.createRunHeader())
+
+ createEnvironmentFragments(fragments, runConfiguration)
+ addInterpreterOptions(fragments)
+ addContentSourceRoots(fragments)
+
+ customizeFragments(fragments)
+
+ fragments.add(LogsGroupFragment())
+ return fragments
+ }
+
+ private fun > createEnvironmentFragments(fragments: MutableList>,
+ runConfiguration: T) {
+ service().createEnvironmentFragments(fragments, runConfiguration)
+ }
+
+ abstract fun customizeFragments(fragments: MutableList>)
+
+ private fun > addInterpreterOptions(fragments: MutableList>) {
+ val interpreterOptionsField = RawCommandLineEditor()
+ val interpreterOptionsFragment: SettingsEditorFragment = SettingsEditorFragment(
+ "py.interpreter.options",
+ PyBundle.message("python.run.configuration.fragments.interpreter.options"),
+ PyBundle.message("python.run.configuration.fragments.python.group"),
+ interpreterOptionsField, SettingsEditorFragmentType.COMMAND_LINE,
+ { config: T, field: RawCommandLineEditor -> field.text = config.interpreterOptions },
+ { config: T, field: RawCommandLineEditor -> config.interpreterOptions = field.text.trim() },
+ { config: T -> !config.interpreterOptions.trim().isEmpty() })
+ interpreterOptionsFragment.setHint(PyBundle.message("python.run.configuration.fragments.interpreter.options.hint"))
+ interpreterOptionsFragment.actionHint = PyBundle.message("python.run.configuration.fragments.interpreter.options.hint")
+ fragments.add(interpreterOptionsFragment)
+ }
+
+ private fun > addContentSourceRoots(fragments: MutableList>) {
+ val addContentRoots = SettingsEditorFragment.createTag(
+ "py.add.content.roots",
+ PyBundle.message("python.run.configuration.fragments.content.roots"),
+ PyBundle.message("python.run.configuration.fragments.python.group"),
+ { it.shouldAddContentRoots() },
+ { config, value -> config.setAddContentRoots(value) })
+
+ addContentRoots.actionHint = PyBundle.message("python.run.configuration.fragments.content.roots.hint")
+ fragments.add(addContentRoots)
+
+ val addSourceRoots = SettingsEditorFragment.createTag(
+ "py.add.content.roots",
+ PyBundle.message("python.run.configuration.fragments.source.roots"),
+ PyBundle.message("python.run.configuration.fragments.python.group"),
+ { it.shouldAddContentRoots() },
+ { config, value -> config.setAddContentRoots(value) })
+
+ addSourceRoots.actionHint = PyBundle.message("python.run.configuration.fragments.source.roots.hint")
+ fragments.add(addSourceRoots)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/run/configuration/PyInterpreterModeNotifier.kt b/python/src/com/jetbrains/python/run/configuration/PyInterpreterModeNotifier.kt
new file mode 100644
index 000000000000..d0d6aaa69c0a
--- /dev/null
+++ b/python/src/com/jetbrains/python/run/configuration/PyInterpreterModeNotifier.kt
@@ -0,0 +1,10 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.run.configuration
+
+import java.util.function.Consumer
+
+interface PyInterpreterModeNotifier {
+ fun addInterpreterModeListener(listener: Consumer)
+
+ fun isRemoteSelected(): Boolean
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/run/configuration/PyPathMappingsEditorFragment.kt b/python/src/com/jetbrains/python/run/configuration/PyPathMappingsEditorFragment.kt
new file mode 100644
index 000000000000..3bc7de22f3ae
--- /dev/null
+++ b/python/src/com/jetbrains/python/run/configuration/PyPathMappingsEditorFragment.kt
@@ -0,0 +1,49 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.run.configuration
+
+import com.intellij.execution.ui.CommandLinePanel
+import com.intellij.execution.ui.SettingsEditorFragment
+import com.intellij.execution.util.PathMappingsComponent
+import com.jetbrains.python.run.AbstractPythonRunConfiguration
+import java.awt.BorderLayout
+import java.awt.GridBagConstraints
+import java.awt.GridBagLayout
+import javax.swing.JComponent
+import javax.swing.JPanel
+
+class PyPathMappingsEditorFragment>(val notifier: PyInterpreterModeNotifier) :
+ SettingsEditorFragment("py.path.mappings", null, null, JPanel(GridBagLayout()), null, null, { true }) {
+ private val pathMappingsComponent = PathMappingsComponent()
+
+ init {
+ pathMappingsComponent.labelLocation = BorderLayout.WEST
+ CommandLinePanel.setMinimumWidth(component(), 500)
+ val constrains = GridBagConstraints()
+ constrains.fill = GridBagConstraints.HORIZONTAL
+ constrains.weightx = 1.0
+ constrains.gridx = 0
+
+ component().add(pathMappingsComponent, constrains)
+ pathMappingsComponent.isVisible = notifier.isRemoteSelected()
+ notifier.addInterpreterModeListener { isRemote ->
+ pathMappingsComponent.isVisible = isRemote
+ }
+ }
+
+ override fun getAllComponents(): Array {
+ return arrayOf(pathMappingsComponent)
+ }
+
+ override fun resetEditorFrom(config: T) {
+ pathMappingsComponent.setMappingSettings(config.mappingSettings)
+ }
+
+ override fun applyEditorTo(s: T) {
+ s.mappingSettings = pathMappingsComponent.mappingSettings
+ }
+
+ override fun isRemovable(): Boolean = false
+
+ override fun isSelected(): Boolean = true
+}
+
diff --git a/python/src/com/jetbrains/python/run/configuration/PyScriptOrModuleFragment.kt b/python/src/com/jetbrains/python/run/configuration/PyScriptOrModuleFragment.kt
new file mode 100644
index 000000000000..69a51781782d
--- /dev/null
+++ b/python/src/com/jetbrains/python/run/configuration/PyScriptOrModuleFragment.kt
@@ -0,0 +1,137 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.run.configuration
+
+import com.intellij.execution.ui.CommandLinePanel
+import com.intellij.execution.ui.SettingsEditorFragment
+import com.intellij.execution.ui.SettingsEditorFragmentType
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
+import com.intellij.openapi.ui.TextBrowseFolderListener
+import com.intellij.openapi.ui.TextFieldWithBrowseButton
+import com.intellij.openapi.util.io.FileUtil
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.openapi.vfs.LocalFileSystem
+import com.intellij.psi.PsiFileSystemItem
+import com.intellij.ui.TextAccessor
+import com.jetbrains.PySymbolFieldWithBrowseButton
+import com.jetbrains.extensions.ModuleBasedContextAnchor
+import com.jetbrains.extensions.ProjectSdkContextAnchor
+import com.jetbrains.isPythonModule
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.run.PythonRunConfiguration
+import java.awt.GridBagConstraints
+import java.awt.GridBagLayout
+import java.awt.event.ItemEvent
+import javax.swing.JComboBox
+import javax.swing.JComponent
+import javax.swing.JPanel
+
+
+class PyScriptOrModuleFragment : SettingsEditorFragment(
+ "py.script.field", null, null, JPanel(GridBagLayout()), SettingsEditorFragmentType.COMMAND_LINE, null, null, { true }) {
+
+ private val typeNames = listOf("script", "module")
+ private val SCRIPT_MODE = 0
+ private val MODULE_MODE = 1
+ private var currentMode: Int? = null
+
+ val fields: MutableList = MutableList(MODULE_MODE + 1) { null }
+ val hints: MutableMap = mutableMapOf()
+
+ private val moduleModeChooser: JComboBox
+
+ init {
+ CommandLinePanel.setMinimumWidth(component(), 500)
+ moduleModeChooser = JComboBox()
+ moduleModeChooser.addItem(typeNames[SCRIPT_MODE])
+ moduleModeChooser.addItem(typeNames[MODULE_MODE])
+ component().add(moduleModeChooser, GridBagConstraints())
+ hints[moduleModeChooser] = PyBundle.message("python.run.configuration.fragments.chooser.hint")
+
+ moduleModeChooser.addItemListener { e ->
+ if (e?.stateChange == ItemEvent.SELECTED) {
+ if ((e.item as? String) == typeNames[MODULE_MODE]) {
+ showField(MODULE_MODE)
+ currentMode = MODULE_MODE
+ }
+ else {
+ showField(SCRIPT_MODE)
+ currentMode = SCRIPT_MODE
+ }
+ }
+ }
+ }
+
+ override fun resetEditorFrom(config: PythonRunConfiguration) {
+ if (currentMode == null) {
+ initComponents(config)
+ }
+ val type = if (config.isModuleMode) MODULE_MODE else SCRIPT_MODE
+ if (currentMode != type) {
+ currentMode = type
+ moduleModeChooser.selectedItem = typeNames[type]
+ showField(type)
+ (fields[type] as TextAccessor).text = config.scriptName
+ }
+ }
+
+ private fun showField(type: Int) {
+ for (i in 0 until fields.size) {
+ fields[i]?.isVisible = i == type
+ }
+ }
+
+ private fun initComponents(config: PythonRunConfiguration) {
+ val scriptField = TextFieldWithBrowseButton()
+ scriptField.addBrowseFolderListener(TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFileDescriptor(), config.project))
+ initComponent(scriptField, SCRIPT_MODE)
+ fields[SCRIPT_MODE] = scriptField
+ hints[scriptField] = PyBundle.message("python.run.configuration.fragments.script.path.hint")
+
+ val module = config.module
+ val contentAnchor = if (module == null) ProjectSdkContextAnchor(config.project, config.sdk) else ModuleBasedContextAnchor(module)
+ val workingDirectory = config.workingDirectory
+ val moduleField = PySymbolFieldWithBrowseButton(
+ contentAnchor,
+ { element -> (element is PsiFileSystemItem && isPythonModule(element)) },
+ if (StringUtil.isEmpty(workingDirectory)) {
+ null
+ }
+ else {
+ { LocalFileSystem.getInstance().findFileByPath(workingDirectory)!! }
+ })
+ initComponent(moduleField, MODULE_MODE)
+ fields[MODULE_MODE] = moduleField
+ hints[moduleField] = PyBundle.message("python.run.configuration.fragments.module.name.hint")
+ }
+
+ private fun initComponent(field: JComponent, index: Int) {
+ val constraints = GridBagConstraints()
+ constraints.fill = GridBagConstraints.BOTH
+ constraints.weightx = 1.0
+ component().add(field, constraints)
+ fields[index] = field
+ }
+
+ override fun applyEditorTo(s: PythonRunConfiguration) {
+ val mode = currentMode
+ if (mode == null) return
+ s.isModuleMode = mode == MODULE_MODE
+ val text = (fields[mode] as? TextAccessor)?.text?.trim()
+ if (mode == MODULE_MODE) {
+ s.scriptName = text
+ }
+ else {
+ s.scriptName = text?.let { FileUtil.toSystemIndependentName(it) }
+ }
+ }
+
+ override fun isRemovable(): Boolean = false
+
+ override fun getAllComponents(): Array {
+ return fields.filterNotNull().toTypedArray() + moduleModeChooser
+ }
+
+ override fun getHint(component: JComponent?): String? {
+ return if (component == null) null else hints[component]
+ }
+}
diff --git a/python/src/com/jetbrains/python/run/configuration/PySdkComboBox.kt b/python/src/com/jetbrains/python/run/configuration/PySdkComboBox.kt
new file mode 100644
index 000000000000..e1ff8764128c
--- /dev/null
+++ b/python/src/com/jetbrains/python/run/configuration/PySdkComboBox.kt
@@ -0,0 +1,93 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.run.configuration
+
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.roots.ModuleRootManager
+import com.intellij.openapi.ui.ComboBox
+import com.intellij.openapi.util.Computable
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.run.AbstractPythonRunConfigurationParams
+import com.jetbrains.python.sdk.PySdkListCellRenderer
+import com.jetbrains.python.sdk.PythonSdkUtil
+import java.util.function.Consumer
+
+class PySdkComboBox(private val addDefault: Boolean,
+ private val moduleProvider: Computable) : ComboBox(), PyInterpreterModeNotifier {
+ private val interpreterModeListeners: MutableList> = mutableListOf()
+
+ fun reset(config: AbstractPythonRunConfigurationParams) {
+ initList()
+ selectedItem = config.sdk
+ setUseModuleSdk(config.isUseModuleSdk)
+ setModule(config.module)
+ addActionListener {
+ updateRemoteInterpreterMode()
+ }
+ updateRemoteInterpreterMode()
+ }
+
+ private fun initList() {
+ val pythonSdks: MutableList = PythonSdkUtil.getAllSdks().toMutableList()
+ if (addDefault) {
+ pythonSdks.add(0, null)
+ }
+ for (item in pythonSdks) {
+ addItem(item)
+ }
+ }
+
+ fun apply(config: AbstractPythonRunConfigurationParams) {
+ config.sdk = selectedItem as? Sdk
+ config.isUseModuleSdk = isUseModuleSdk()
+ }
+
+ fun setModule(module: Module?) {
+ updateDefaultInterpreter(module)
+ updateRemoteInterpreterMode()
+ }
+
+ private fun updateDefaultInterpreter(module: Module?) {
+ val sdk = if (module == null) null else ModuleRootManager.getInstance(module).sdk
+ setRenderer(
+ if (sdk == null) PySdkListCellRenderer()
+ else PySdkListCellRenderer(PyBundle.message("python.sdk.rendering.project.default.0", sdk.name), sdk)
+ )
+ }
+
+ private fun setUseModuleSdk(useModuleSdk: Boolean) {
+ if (selectedItem != null && useModuleSdk) {
+ selectedItem = null
+ }
+ }
+
+ private fun isUseModuleSdk(): Boolean = addDefault && selectedItem == null
+
+ fun getSelectedSdk(): Sdk? {
+ val selectedSdk = selectedItem as? Sdk
+ if (selectedSdk != null) {
+ return selectedSdk
+ }
+ else {
+ if (isUseModuleSdk()) {
+ moduleProvider.get()?.let {
+ return@getSelectedSdk PythonSdkUtil.findPythonSdk(it)
+ }
+ }
+ }
+ return null
+ }
+
+ override fun isRemoteSelected(): Boolean = PythonSdkUtil.isRemote(getSelectedSdk())
+
+ private fun updateRemoteInterpreterMode() {
+ val isRemote = isRemoteSelected()
+ for (listener in interpreterModeListeners) {
+ listener.accept(isRemote)
+ }
+ }
+
+ override fun addInterpreterModeListener(listener: Consumer) {
+ interpreterModeListeners.add(listener)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/run/configuration/PythonConfigurationFragmentedEditor.kt b/python/src/com/jetbrains/python/run/configuration/PythonConfigurationFragmentedEditor.kt
new file mode 100644
index 000000000000..80918abc91cb
--- /dev/null
+++ b/python/src/com/jetbrains/python/run/configuration/PythonConfigurationFragmentedEditor.kt
@@ -0,0 +1,100 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.run.configuration
+
+import com.intellij.execution.ExecutionBundle
+import com.intellij.execution.ui.SettingsEditorFragment
+import com.intellij.execution.ui.SettingsEditorFragmentType
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
+import com.intellij.openapi.ui.LabeledComponent
+import com.intellij.openapi.ui.TextBrowseFolderListener
+import com.intellij.openapi.ui.TextFieldWithBrowseButton
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.ui.RawCommandLineEditor
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.run.PythonRunConfiguration
+import java.awt.BorderLayout
+import java.awt.event.ComponentAdapter
+import java.awt.event.ComponentEvent
+
+class PythonConfigurationFragmentedEditor(runConfiguration: PythonRunConfiguration) :
+ AbstractPythonConfigurationFragmentedEditor(runConfiguration) {
+
+ override fun customizeFragments(fragments: MutableList>) {
+ fragments.add(PyScriptOrModuleFragment())
+
+ val scriptParametersFragment: SettingsEditorFragment = SettingsEditorFragment(
+ "py.script.parameters",
+ PyBundle.message("python.run.configuration.fragments.script.parameters"),
+ PyBundle.message("python.run.configuration.fragments.python.group"),
+ RawCommandLineEditor(), SettingsEditorFragmentType.COMMAND_LINE,
+ { config: PythonRunConfiguration, field: RawCommandLineEditor -> field.text = config.scriptParameters },
+ { config: PythonRunConfiguration, field: RawCommandLineEditor -> config.scriptParameters = field.text.trim() },
+ { config: PythonRunConfiguration -> !config.scriptParameters.trim().isEmpty() })
+ scriptParametersFragment.setHint(PyBundle.message("python.run.configuration.fragments.script.parameters.hint"))
+ scriptParametersFragment.actionHint = PyBundle.message("python.run.configuration.fragments.script.parameters.hint")
+ fragments.add(scriptParametersFragment)
+
+
+ val runWithConsole = SettingsEditorFragment.createTag(
+ "py.run.with.python.console",
+ PyBundle.message("python.run.configuration.fragments.run.with.python.console"),
+ PyBundle.message("python.run.configuration.fragments.python.group"),
+ { it.showCommandLineAfterwards() },
+ { config, value -> config.setShowCommandLineAfterwards(value) })
+ runWithConsole.actionHint = PyBundle.message("python.run.configuration.fragments.run.with.python.console.hint")
+ fragments.add(runWithConsole)
+
+ val emulateTerminal = SettingsEditorFragment.createTag(
+ "py.emulate.terminal",
+ PyBundle.message("python.run.configuration.fragments.emulate.terminal"),
+ PyBundle.message("python.run.configuration.fragments.python.group"),
+ { it.emulateTerminal() },
+ { config, value -> config.setEmulateTerminal(value) })
+ emulateTerminal.actionHint = PyBundle.message("python.run.configuration.fragments.emulate.terminal.hint")
+ fragments.add(emulateTerminal)
+
+ val inputFile = TextFieldWithBrowseButton()
+ inputFile.addBrowseFolderListener(TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFileDescriptor(),
+ runConfiguration.project))
+ val labeledComponent = LabeledComponent.create(inputFile, ExecutionBundle.message("redirect.input.from"))
+ labeledComponent.labelLocation = BorderLayout.WEST
+ val redirectInputFrom: SettingsEditorFragment> =
+ SettingsEditorFragment>(
+ "py.redirect.input",
+ ExecutionBundle.message("redirect.input.from.name"),
+ ExecutionBundle.message("group.operating.system"),
+ labeledComponent, SettingsEditorFragmentType.COMMAND_LINE,
+ { config: PythonRunConfiguration, component: LabeledComponent ->
+ component.component.text = config.inputFile
+ },
+ { config: PythonRunConfiguration, component: LabeledComponent ->
+ val filePath = component.component.text
+ config.isRedirectInput = component.isVisible && StringUtil.isNotEmpty(filePath)
+ config.inputFile = filePath
+ },
+ { config: PythonRunConfiguration -> config.isRedirectInput })
+ redirectInputFrom.actionHint = ExecutionBundle.message("read.input.from.the.specified.file")
+ redirectInputFrom.setHint(ExecutionBundle.message("read.input.from.the.specified.file"))
+ fragments.add(redirectInputFrom)
+
+ val editors = mutableListOf>()
+ editors.add(runWithConsole)
+ editors.add(emulateTerminal)
+ editors.add(redirectInputFrom)
+ addSingleSelectionListeners(editors)
+ }
+
+ private fun addSingleSelectionListeners(editors: MutableList>) {
+ for ((i, editor) in editors.withIndex()) {
+ editor.component().addComponentListener(object : ComponentAdapter() {
+ override fun componentShown(e: ComponentEvent?) {
+ for ((j, otherEditor) in editors.withIndex()) {
+ if (i != j) {
+ otherEditor.isSelected = false
+ }
+ }
+ }
+ })
+ }
+ }
+}
\ No newline at end of file