Skip to content

Commit e9961d8

Browse files
committed
feat(intellij): save and edit Robot Framework run configurations
Run configurations keep their target across restarts and can be stored as project files: "Files and folders" with paths that replace those of robot.toml, or "Tests and suites" with the full names that discovery reports, in a field that expands to one entry per line. The editor is PyCharm's run configuration editor, so the interpreter, interpreter options, working directory, environment variables, .env files and Run with uv take effect, gutter runs reuse their configuration instead of creating "(1)" copies, Profile ends with a message, and a configuration with a remote interpreter is refused (#418).
1 parent 36022e1 commit e9961d8

18 files changed

Lines changed: 839 additions & 234 deletions

File tree

‎intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeHelpers.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,15 @@ internal fun pluginPathOf(classResource: URL?): Path? {
5656
return jar.parent?.parent
5757
}
5858

59+
/** The module whose Python interpreter RobotCode uses: the first module that has one. */
60+
val Project.robotPythonModule: com.intellij.openapi.module.Module?
61+
get() {
62+
return this.modules.firstOrNull { PythonSdkUtil.findPythonSdk(it) != null }
63+
}
64+
5965
val Project.robotPythonSdk: com.intellij.openapi.projectRoots.Sdk?
6066
get() {
61-
return this.modules.firstNotNullOfOrNull { PythonSdkUtil.findPythonSdk(it) }
67+
return robotPythonModule?.let { PythonSdkUtil.findPythonSdk(it) }
6268
}
6369

6470
/**

‎intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProgramRunner.kt‎

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,33 @@ import com.intellij.execution.executors.DefaultDebugExecutor
77
import com.intellij.execution.runners.AsyncProgramRunner
88
import com.intellij.execution.runners.ExecutionEnvironment
99
import com.intellij.execution.ui.RunContentDescriptor
10-
import com.intellij.openapi.fileEditor.FileDocumentManager
1110
import com.intellij.xdebugger.XDebugProcess
1211
import com.intellij.xdebugger.XDebugProcessStarter
1312
import com.intellij.xdebugger.XDebugSession
1413
import com.intellij.xdebugger.XDebuggerManager
1514
import dev.robotcode.robotcode4ij.execution.RobotCodeRunConfiguration
1615
import dev.robotcode.robotcode4ij.execution.RobotCodeRunProfileState
16+
import dev.robotcode.robotcode4ij.execution.startInBackground
1717
import org.jetbrains.concurrency.Promise
18-
import org.jetbrains.concurrency.resolvedPromise
1918

2019
class RobotCodeDebugProgramRunner : AsyncProgramRunner<RunnerSettings>() {
2120
override fun getRunnerId(): String {
2221
return "dev.robotcode.robotcode4ij.execution.RobotCodeDebugProgramRunner"
2322
}
24-
23+
2524
override fun canRun(executorId: String, profile: RunProfile): Boolean {
2625
return (executorId == DefaultDebugExecutor.EXECUTOR_ID) && profile is RobotCodeRunConfiguration
2726
}
28-
27+
28+
// the session wraps the started run; the debug process subscribes to the handshake before the session starts it
2929
override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise<RunContentDescriptor?> {
30-
FileDocumentManager.getInstance().saveAllDocuments()
31-
32-
return resolvedPromise(doExecute(state as RobotCodeRunProfileState, environment))
33-
}
34-
35-
private fun doExecute(state: RobotCodeRunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? {
36-
val manager = XDebuggerManager.getInstance(environment.project)
37-
val started = manager.newSessionBuilder(object : XDebugProcessStarter() {
38-
override fun start(session: XDebugSession): XDebugProcess {
39-
val result = state.execute(environment.executor, this@RobotCodeDebugProgramRunner)
40-
41-
return RobotCodeDebugProcess(session, result, state)
42-
}
43-
}).environment(environment).startSession()
44-
45-
return started.runContentDescriptor
30+
val runState = state as RobotCodeRunProfileState
31+
return startInBackground(runState, environment) { result ->
32+
XDebuggerManager.getInstance(environment.project).newSessionBuilder(object : XDebugProcessStarter() {
33+
override fun start(session: XDebugSession): XDebugProcess {
34+
return RobotCodeDebugProcess(session, result, runState)
35+
}
36+
}).environment(environment).startSession().runContentDescriptor
37+
}
4638
}
4739
}
Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package dev.robotcode.robotcode4ij.execution
22

3+
import com.intellij.execution.ExecutionException
4+
import com.intellij.execution.ExecutionResult
35
import com.intellij.execution.configurations.RunProfile
46
import com.intellij.execution.configurations.RunProfileState
57
import com.intellij.execution.configurations.RunnerSettings
@@ -8,26 +10,78 @@ import com.intellij.execution.runners.AsyncProgramRunner
810
import com.intellij.execution.runners.ExecutionEnvironment
911
import com.intellij.execution.runners.showRunContent
1012
import com.intellij.execution.ui.RunContentDescriptor
13+
import com.intellij.openapi.application.ApplicationManager
14+
import com.intellij.openapi.application.ModalityState
1115
import com.intellij.openapi.fileEditor.FileDocumentManager
16+
import dev.robotcode.robotcode4ij.RobotCodeBundle
17+
import org.jetbrains.concurrency.AsyncPromise
1218
import org.jetbrains.concurrency.Promise
13-
import org.jetbrains.concurrency.resolvedPromise
19+
import org.jetbrains.concurrency.rejectedPromise
20+
21+
/**
22+
* Starts the run of [state] on a background thread, because preparing the interpreter and its environment must not run
23+
* on the UI thread, and passes the result to [show] on the UI thread.
24+
*/
25+
internal fun startInBackground(
26+
state: RobotCodeRunProfileState,
27+
environment: ExecutionEnvironment,
28+
show: (ExecutionResult) -> RunContentDescriptor?
29+
): Promise<RunContentDescriptor?> {
30+
FileDocumentManager.getInstance().saveAllDocuments()
31+
val promise = AsyncPromise<RunContentDescriptor?>()
32+
val application = ApplicationManager.getApplication()
33+
application.executeOnPooledThread {
34+
val result = try {
35+
state.execute(environment.executor)
36+
} catch (e: Throwable) {
37+
promise.setError(e)
38+
return@executeOnPooledThread
39+
}
40+
if (result == null) {
41+
promise.setResult(null)
42+
return@executeOnPooledThread
43+
}
44+
application.invokeLater({
45+
try {
46+
promise.setResult(show(result))
47+
} catch (e: Throwable) {
48+
result.processHandler?.destroyProcess()
49+
promise.setError(e)
50+
}
51+
}, ModalityState.nonModal())
52+
}
53+
return promise
54+
}
1455

1556
class RobotCodeProgramRunner : AsyncProgramRunner<RunnerSettings>() {
1657
override fun getRunnerId(): String {
1758
return "dev.robotcode.robotcode4ij.execution.RobotCodeProgramRunner"
1859
}
19-
60+
2061
override fun canRun(executorId: String, profile: RunProfile): Boolean {
2162
return (executorId == DefaultRunExecutor.EXECUTOR_ID) && profile is RobotCodeRunConfiguration
2263
}
23-
64+
2465
override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise<RunContentDescriptor?> {
25-
FileDocumentManager.getInstance().saveAllDocuments()
26-
27-
return resolvedPromise(doExecute(state as RobotCodeRunProfileState, environment))
66+
return startInBackground(state as RobotCodeRunProfileState, environment) { showRunContent(it, environment) }
67+
}
68+
}
69+
70+
/**
71+
* Takes PyCharm Professional's "Profile" for Robot Framework configurations, which its profiler runner would otherwise
72+
* accept as Python run configurations, and ends it with a message.
73+
*/
74+
class RobotCodeProfileRunner : AsyncProgramRunner<RunnerSettings>() {
75+
override fun getRunnerId(): String {
76+
return "dev.robotcode.robotcode4ij.execution.RobotCodeProfileRunner"
2877
}
2978

30-
private fun doExecute(state: RobotCodeRunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? {
31-
return showRunContent(state.execute(environment.executor, this), environment)
79+
// the id of PyCharm Professional's profiler executor
80+
override fun canRun(executorId: String, profile: RunProfile): Boolean {
81+
return executorId == "Profiler" && profile is RobotCodeRunConfiguration
82+
}
83+
84+
override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise<RunContentDescriptor?> {
85+
return rejectedPromise(ExecutionException(RobotCodeBundle.message("run.profile.notSupported")))
3286
}
3387
}

‎intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfiguration.kt‎

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,75 @@ package dev.robotcode.robotcode4ij.execution
22

33
import com.intellij.execution.Executor
44
import com.intellij.execution.configurations.ConfigurationFactory
5-
import com.intellij.execution.configurations.LocatableConfigurationBase
6-
import com.intellij.execution.configurations.RunConfiguration
75
import com.intellij.execution.configurations.RunProfileState
86
import com.intellij.execution.runners.ExecutionEnvironment
97
import com.intellij.execution.testframework.sm.runner.SMRunnerConsolePropertiesProvider
108
import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties
119
import com.intellij.openapi.options.SettingsEditor
1210
import com.intellij.openapi.project.Project
11+
import com.intellij.openapi.util.JDOMExternalizerUtil
12+
import com.jetbrains.python.run.AbstractPythonRunConfiguration
13+
import com.jetbrains.python.run.DebugAwareConfiguration
1314
import dev.robotcode.robotcode4ij.robotCodeEnvironment
14-
import dev.robotcode.robotcode4ij.testing.RobotCodeTestItem
15+
import dev.robotcode.robotcode4ij.robotPythonModule
1516
import org.jdom.Element
1617

18+
/**
19+
* A Robot Framework run configuration. It is a PyCharm Python run configuration, so it has PyCharm's interpreter,
20+
* environment and working-directory options, and stores what it runs, its target, in
21+
* [RobotCodeRunConfigurationOptions].
22+
*/
1723
class RobotCodeRunConfiguration(project: Project, factory: ConfigurationFactory) :
18-
LocatableConfigurationBase<ConfigurationFactory>
19-
(project, factory, "Robot Framework"), SMRunnerConsolePropertiesProvider {
20-
24+
AbstractPythonRunConfiguration<RobotCodeRunConfiguration>(project, factory), SMRunnerConsolePropertiesProvider,
25+
DebugAwareConfiguration {
26+
27+
init {
28+
applyRobotCodeDefaults()
29+
}
30+
31+
// runs as before: the interpreter that RobotCode uses for the language server, the project folder as working
32+
// directory and no PYTHONPATH entries from the project's roots; earlier versions stored no interpreter options,
33+
// which PyCharm's editor needs as a value
34+
private fun applyRobotCodeDefaults() {
35+
project.robotPythonModule?.let { setModule(it) }
36+
isUseModuleSdk = true
37+
interpreterOptions = ""
38+
workingDirectory = ""
39+
setAddContentRoots(false)
40+
setAddSourceRoots(false)
41+
}
42+
43+
public override fun getOptions(): RobotCodeRunConfigurationOptions {
44+
return super.getOptions() as RobotCodeRunConfigurationOptions
45+
}
46+
2147
// fails with the IDE's run error before a process or a debug session starts
2248
override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState {
2349
project.robotCodeEnvironment.ensureUsableForRun()
2450
return RobotCodeRunProfileState(this, environment)
2551
}
26-
27-
override fun getConfigurationEditor(): SettingsEditor<out RunConfiguration> {
28-
// TODO: Implement configuration editor
29-
return RobotCodeRunConfigurationEditor()
52+
53+
override fun createConfigurationEditor(): SettingsEditor<RobotCodeRunConfiguration> {
54+
return RobotCodeRunConfigurationEditor(this)
3055
}
31-
32-
var includedTestItems: List<RobotCodeTestItem> = emptyList()
33-
34-
var paths: List<String> = emptyList()
35-
56+
57+
override fun isNewUiSupported(): Boolean = true
58+
59+
// Run and Debug use RobotCode's runners
60+
override fun canRunWithCoverage(): Boolean = false
61+
62+
override fun canRunUnderDebug(): Boolean = false
63+
3664
override fun createTestConsoleProperties(executor: Executor): SMTRunnerConsoleProperties {
3765
return RobotRunnerConsoleProperties(this, "Robot Framework", executor)
3866
}
39-
40-
override fun writeExternal(element: Element) {
41-
super.writeExternal(element)
42-
// TODO: Implement serialization
43-
// XmlSerializer.serializeInto(this, element)
44-
}
45-
67+
4668
override fun readExternal(element: Element) {
47-
super.readExternal(element)
48-
// TODO: Implement deserialization
49-
// XmlSerializer.deserializeInto(this, element)
69+
// PyCharm writes this field for every configuration; earlier versions of the plugin did not
70+
val earlierVersion = JDOMExternalizerUtil.readField(element, "ADD_CONTENT_ROOTS") == null
71+
super<AbstractPythonRunConfiguration>.readExternal(element)
72+
if (earlierVersion) {
73+
applyRobotCodeDefaults()
74+
}
5075
}
5176
}

0 commit comments

Comments
 (0)