Skip to content

Commit 4c2a43b

Browse files
committed
feat: show editor hint for Python/Robot Framework issues instead of throwing error
When opening a Robot Framework file, issues related to the Python environment or Robot Framework version are now shown as an editor hint at the top of the file tab, instead of raising an exception. closes: #421
1 parent c7d3c5e commit 4c2a43b

13 files changed

+165
-51
lines changed

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

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@ import com.intellij.execution.configurations.GeneralCommandLine
44
import com.intellij.execution.util.ExecUtil
55
import com.intellij.openapi.application.ApplicationManager
66
import com.intellij.openapi.application.PathManager
7+
import com.intellij.openapi.components.Service
8+
import com.intellij.openapi.components.service
79
import com.intellij.openapi.diagnostic.thisLogger
810
import com.intellij.openapi.project.Project
911
import com.intellij.openapi.project.modules
1012
import com.intellij.openapi.util.Key
1113
import com.jetbrains.python.sdk.pythonSdk
14+
import dev.robotcode.robotcode4ij.lsp.langServerManager
15+
import dev.robotcode.robotcode4ij.testing.testManger
16+
import kotlinx.coroutines.CoroutineScope
17+
import kotlinx.coroutines.Dispatchers
18+
import kotlinx.coroutines.ExperimentalCoroutinesApi
19+
import kotlinx.coroutines.Job
20+
import kotlinx.coroutines.delay
21+
import kotlinx.coroutines.launch
1222
import java.nio.file.Path
1323
import kotlin.io.path.Path
1424
import kotlin.io.path.exists
@@ -23,7 +33,7 @@ class RobotCodeHelpers {
2333
val robotCodePath: Path = toolPath.resolve("robotcode")
2434
val checkRobotVersion: Path = toolPath.resolve("utils").resolve("check_robot_version.py")
2535

26-
val PYTHON_AND_ROBOT_OK_KEY = Key.create<Boolean?>("ROBOTCODE_PYTHON_AND_ROBOT_OK")
36+
val PYTHON_AND_ROBOT_OK_KEY = Key.create<CheckPythonAndRobotVersionResult?>("ROBOTCODE_PYTHON_AND_ROBOT_OK")
2737
}
2838
}
2939

@@ -34,28 +44,43 @@ val Project.robotPythonSdk: com.intellij.openapi.projectRoots.Sdk?
3444
}
3545
}
3646

37-
fun Project.checkPythonAndRobotVersion(reset: Boolean = false): Boolean {
38-
if (!reset && this.getUserData(RobotCodeHelpers.PYTHON_AND_ROBOT_OK_KEY) == true) {
39-
return true
47+
enum class CheckPythonAndRobotVersionResult(val errorMessage: String? = null) {
48+
OK(null),
49+
NO_PYTHON("No Python interpreter is configured for the project."),
50+
INVALID_PYTHON("The configured Python interpreter is invalid."),
51+
INVALID_PYTHON_VERSION("The Python version configured for the project is too old. Minimum required version is 3.9."),
52+
INVALID_ROBOT("The Robot Framework version is invalid or not installed. Version 5.0 or higher is required.")
53+
}
54+
55+
fun Project.resetPythonAndRobotVersionCache() {
56+
this.putUserData(RobotCodeHelpers.PYTHON_AND_ROBOT_OK_KEY, null)
57+
}
58+
59+
fun Project.checkPythonAndRobotVersion(reset: Boolean = false): CheckPythonAndRobotVersionResult {
60+
if (!reset) {
61+
val cachedResult = this.getUserData(RobotCodeHelpers.PYTHON_AND_ROBOT_OK_KEY)
62+
if (cachedResult != null) {
63+
return cachedResult
64+
}
4065
}
4166

42-
val result = ApplicationManager.getApplication().executeOnPooledThread<Boolean> {
67+
val result = ApplicationManager.getApplication().executeOnPooledThread<CheckPythonAndRobotVersionResult> {
4368

4469
val pythonInterpreter = this.robotPythonSdk?.homePath
4570

4671
if (pythonInterpreter == null) {
4772
thisLogger().info("No Python Interpreter defined for project '${this.name}'")
48-
return@executeOnPooledThread false
73+
return@executeOnPooledThread CheckPythonAndRobotVersionResult.NO_PYTHON
4974
}
5075

5176
if (!Path(pythonInterpreter).exists()) {
5277
thisLogger().warn("Python Interpreter $pythonInterpreter not exists")
53-
return@executeOnPooledThread false
78+
return@executeOnPooledThread CheckPythonAndRobotVersionResult.INVALID_PYTHON
5479
}
5580

5681
if (!Path(pythonInterpreter).isRegularFile()) {
5782
thisLogger().warn("Python Interpreter $pythonInterpreter is not a regular file")
58-
return@executeOnPooledThread false
83+
return@executeOnPooledThread CheckPythonAndRobotVersionResult.INVALID_PYTHON
5984
}
6085

6186
thisLogger().info("Use Python Interpreter $pythonInterpreter for project '${this.name}'")
@@ -67,7 +92,7 @@ fun Project.checkPythonAndRobotVersion(reset: Boolean = false): Boolean {
6792
)
6893
if (res.exitCode != 0 || res.stdout.trim() != "True") {
6994
thisLogger().warn("Invalid python version")
70-
return@executeOnPooledThread false
95+
return@executeOnPooledThread CheckPythonAndRobotVersionResult.INVALID_PYTHON_VERSION
7196
}
7297

7398
val res1 = ExecUtil.execAndGetOutput(
@@ -76,10 +101,10 @@ fun Project.checkPythonAndRobotVersion(reset: Boolean = false): Boolean {
76101
)
77102
if (res1.exitCode != 0 || res1.stdout.trim() != "True") {
78103
thisLogger().warn("Invalid Robot Framework version")
79-
return@executeOnPooledThread false
104+
return@executeOnPooledThread CheckPythonAndRobotVersionResult.INVALID_ROBOT
80105
}
81106

82-
return@executeOnPooledThread true
107+
return@executeOnPooledThread CheckPythonAndRobotVersionResult.OK
83108

84109
}.get()
85110

@@ -88,6 +113,7 @@ fun Project.checkPythonAndRobotVersion(reset: Boolean = false): Boolean {
88113
return result
89114
}
90115

116+
class InvalidPythonOrRobotVersionException(message: String) : Exception(message)
91117

92118
fun Project.buildRobotCodeCommandLine(
93119
args: Array<String> = arrayOf(),
@@ -97,8 +123,8 @@ fun Project.buildRobotCodeCommandLine(
97123
noColor: Boolean = true,
98124
noPager: Boolean = true
99125
): GeneralCommandLine {
100-
if (!this.checkPythonAndRobotVersion()) {
101-
throw IllegalArgumentException("PythonSDK is not defined or robot version is not valid for project ${this.name}")
126+
if (this.checkPythonAndRobotVersion() != CheckPythonAndRobotVersionResult.OK) {
127+
throw InvalidPythonOrRobotVersionException("PythonSDK is not defined or robot version is not valid for project ${this.name}")
102128
}
103129

104130
val pythonInterpreter = this.robotPythonSdk?.homePath
@@ -118,3 +144,50 @@ fun Project.buildRobotCodeCommandLine(
118144

119145
return commandLine
120146
}
147+
148+
@Service(Service.Level.PROJECT)
149+
private class RobotCodeRestartManager(private val project: Project) {
150+
companion object {
151+
private const val DEBOUNCE_DELAY = 500L
152+
}
153+
154+
private var refreshJob: Job? = null
155+
156+
fun restart(reset: Boolean = false) {
157+
project.checkPythonAndRobotVersion(reset)
158+
project.langServerManager.restart()
159+
project.testManger.refreshDebounced()
160+
}
161+
162+
@OptIn(ExperimentalCoroutinesApi::class)
163+
private val restartScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1))
164+
165+
fun restartDebounced(reset: Boolean = false) {
166+
if (!project.isOpen || project.isDisposed) {
167+
return
168+
}
169+
170+
refreshJob?.cancel()
171+
172+
refreshJob = restartScope.launch {
173+
delay(DEBOUNCE_DELAY)
174+
restart(reset)
175+
refreshJob = null
176+
}
177+
}
178+
179+
fun cancelRestart() {
180+
refreshJob?.cancel()
181+
refreshJob = null
182+
}
183+
}
184+
185+
fun Project.restartAll(reset: Boolean = false, debounced: Boolean = true) {
186+
val service = this.service<RobotCodeRestartManager>()
187+
if (debounced) {
188+
service.restartDebounced(reset)
189+
} else {
190+
service.cancelRestart()
191+
service.restart(reset)
192+
}
193+
}

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,25 @@ import com.intellij.openapi.startup.ProjectActivity
55
import com.intellij.openapi.vfs.VirtualFileManager
66
import com.intellij.platform.backend.workspace.workspaceModel
77
import com.intellij.platform.workspace.jps.entities.ModuleEntity
8-
import com.intellij.platform.workspace.storage.EntityChange
8+
import com.intellij.platform.workspace.jps.entities.SdkEntity
99
import dev.robotcode.robotcode4ij.listeners.RobotCodeVirtualFileListener
10-
import dev.robotcode.robotcode4ij.lsp.langServerManager
1110
import dev.robotcode.robotcode4ij.testing.testManger
1211
import kotlinx.coroutines.flow.collect
1312
import kotlinx.coroutines.flow.onEach
1413

1514
class RobotCodePostStartupActivity : ProjectActivity {
1615
override suspend fun execute(project: Project) {
17-
project.langServerManager.start()
18-
project.testManger.refreshDebounced()
16+
project.restartAll(reset = true, debounced = false)
1917

2018
VirtualFileManager.getInstance().addAsyncFileListener(RobotCodeVirtualFileListener(project), project.testManger)
2119

2220
project.workspaceModel.eventLog.onEach {
23-
val moduleChanges = it.getChanges(ModuleEntity::class.java)
24-
if (moduleChanges.filterIsInstance<EntityChange.Replaced<ModuleEntity>>().isNotEmpty()) {
25-
project.checkPythonAndRobotVersion(true)
26-
project.langServerManager.restart()
27-
project.testManger.refreshDebounced()
21+
val sdkChanged = it.getChanges(SdkEntity::class.java).isNotEmpty()
22+
val moduleChanged = it.getChanges(ModuleEntity::class.java).isNotEmpty()
23+
24+
if (moduleChanged || sdkChanged) {
25+
project.resetPythonAndRobotVersionCache()
26+
project.restartAll(reset = true)
2827
}
2928
}.collect()
3029
}

intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/actions/RobotCodeClearCacheAndRestartLanguageServerAction.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ package dev.robotcode.robotcode4ij.actions
33
import com.intellij.openapi.actionSystem.AnAction
44
import com.intellij.openapi.actionSystem.AnActionEvent
55
import dev.robotcode.robotcode4ij.lsp.langServerManager
6-
import dev.robotcode.robotcode4ij.testing.testManger
6+
import dev.robotcode.robotcode4ij.restartAll
77

88
class RobotCodeClearCacheAndRestartLanguageServerAction : AnAction() {
99
override fun actionPerformed(e: AnActionEvent) {
10-
e.project?.langServerManager?.clearCacheAndRestart()
11-
e.project?.testManger?.refreshDebounced()
10+
e.project?.langServerManager?.clearCache()
11+
e.project?.restartAll(reset = true, debounced = false)
1212
}
1313
}

intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/actions/RobotCodeRestartLanguageServerAction.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ package dev.robotcode.robotcode4ij.actions
22

33
import com.intellij.openapi.actionSystem.AnAction
44
import com.intellij.openapi.actionSystem.AnActionEvent
5-
import dev.robotcode.robotcode4ij.lsp.langServerManager
6-
import dev.robotcode.robotcode4ij.testing.testManger
5+
import dev.robotcode.robotcode4ij.restartAll
76

87
class RobotCodeRestartLanguageServerAction : AnAction() {
98
override fun actionPerformed(e: AnActionEvent) {
10-
e.project?.langServerManager?.restart()
11-
e.project?.testManger?.refreshDebounced()
9+
e.project?.restartAll(debounced = false)
1210
}
1311
}

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
package dev.robotcode.robotcode4ij.debugging
22

3-
import com.intellij.openapi.editor.Document
4-
import com.intellij.openapi.project.Project
5-
import com.intellij.openapi.util.TextRange
6-
import com.intellij.psi.PsiFile
73
import com.intellij.xdebugger.XSourcePosition
8-
import com.intellij.xdebugger.evaluation.EvaluationMode
9-
import com.intellij.xdebugger.evaluation.ExpressionInfo
104
import com.intellij.xdebugger.evaluation.XDebuggerEvaluator
115
import kotlinx.coroutines.future.await
126
import kotlinx.coroutines.runBlocking

intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/breakpoints/RobotCodeExceptionBreakpointType.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package dev.robotcode.robotcode4ij.debugging.breakpoints
22

33
import com.intellij.icons.AllIcons
4-
import com.intellij.openapi.project.Project
54
import com.intellij.xdebugger.breakpoints.XBreakpoint
65
import com.intellij.xdebugger.breakpoints.XBreakpointType
76
import org.jetbrains.annotations.Nls
87
import org.jetbrains.annotations.NonNls
98
import javax.swing.Icon
10-
import javax.swing.JComponent
119

1210
class RobotCodeExceptionBreakpointType :
1311
XBreakpointType<XBreakpoint<RobotCodeExceptionBreakpointProperties>, RobotCodeExceptionBreakpointProperties>(
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package dev.robotcode.robotcode4ij.editor
2+
3+
import com.intellij.openapi.diagnostic.thisLogger
4+
import com.intellij.openapi.fileEditor.FileEditor
5+
import com.intellij.openapi.options.ShowSettingsUtil
6+
import com.intellij.openapi.project.DumbAware
7+
import com.intellij.openapi.project.Project
8+
import com.intellij.openapi.vfs.VirtualFile
9+
import com.intellij.ui.EditorNotificationPanel
10+
import com.intellij.ui.EditorNotificationProvider
11+
import dev.robotcode.robotcode4ij.CheckPythonAndRobotVersionResult
12+
import dev.robotcode.robotcode4ij.RobotResourceFileType
13+
import dev.robotcode.robotcode4ij.RobotSuiteFileType
14+
import dev.robotcode.robotcode4ij.checkPythonAndRobotVersion
15+
import java.util.function.Function
16+
import javax.swing.JComponent
17+
18+
19+
@Suppress("DialogTitleCapitalization")
20+
class EditorNotificationProvider : EditorNotificationProvider, DumbAware {
21+
override fun collectNotificationData(
22+
project: Project,
23+
file: VirtualFile
24+
): Function<in FileEditor, out JComponent?>? {
25+
if (file.fileType == RobotSuiteFileType || file.fileType == RobotResourceFileType) {
26+
val result = project.checkPythonAndRobotVersion()
27+
if (result == CheckPythonAndRobotVersionResult.OK) {
28+
return null
29+
}
30+
31+
return Function { editor ->
32+
val panel = EditorNotificationPanel(editor, EditorNotificationPanel.Status.Warning)
33+
panel.text = result.errorMessage ?: "RobotCode: Python and Robot Framework version check failed"
34+
panel.createActionLabel("Configure Python Interpreter") {
35+
36+
ShowSettingsUtil.getInstance().showSettingsDialog(project, "Python Interpreter")
37+
}
38+
panel.setCloseAction {
39+
thisLogger().info("Close action clicked")
40+
}
41+
panel
42+
}
43+
}
44+
return null
45+
}
46+
47+
}

intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/editor/RobotCodeStatusBarWidgetFactory.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.intellij.openapi.util.NlsContexts
66
import com.intellij.openapi.wm.StatusBarWidget
77
import com.intellij.openapi.wm.StatusBarWidget.IconPresentation
88
import com.intellij.openapi.wm.StatusBarWidgetFactory
9+
import dev.robotcode.robotcode4ij.CheckPythonAndRobotVersionResult
910
import dev.robotcode.robotcode4ij.RobotIcons
1011
import dev.robotcode.robotcode4ij.checkPythonAndRobotVersion
1112
import org.jetbrains.annotations.NonNls
@@ -38,6 +39,6 @@ class RobotCodeStatusBarWidgetFactory : StatusBarWidgetFactory {
3839
}
3940

4041
override fun isAvailable(project: Project): Boolean {
41-
return project.checkPythonAndRobotVersion()
42+
return project.checkPythonAndRobotVersion() == CheckPythonAndRobotVersionResult.OK
4243
}
4344
}

intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/listeners/RobotCodeVirtualFileListener.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ package dev.robotcode.robotcode4ij.listeners
33
import com.intellij.openapi.project.Project
44
import com.intellij.openapi.vfs.AsyncFileListener
55
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
6-
import dev.robotcode.robotcode4ij.lsp.langServerManager
7-
import dev.robotcode.robotcode4ij.testing.testManger
6+
import dev.robotcode.robotcode4ij.restartAll
87

98
class RobotCodeVirtualFileListener(private val project: Project) : AsyncFileListener {
109
companion object {
@@ -15,8 +14,7 @@ class RobotCodeVirtualFileListener(private val project: Project) : AsyncFileList
1514
return object : AsyncFileListener.ChangeApplier {
1615
override fun afterVfsChange() {
1716
if (events.any { it.file?.name in PROJECT_FILES }) {
18-
project.langServerManager.restart()
19-
project.testManger.refreshDebounced()
17+
project.restartAll()
2018
}
2119
}
2220
}

intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerManager.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package dev.robotcode.robotcode4ij.lsp
33
import com.intellij.openapi.Disposable
44
import com.intellij.openapi.components.Service
55
import com.intellij.openapi.components.service
6+
import com.intellij.openapi.diagnostic.thisLogger
67
import com.intellij.openapi.project.Project
78
import com.intellij.openapi.util.Key
89
import com.intellij.openapi.util.removeUserData
910
import com.redhat.devtools.lsp4ij.LanguageServerManager
1011
import com.redhat.devtools.lsp4ij.ServerStatus
12+
import dev.robotcode.robotcode4ij.CheckPythonAndRobotVersionResult
1113
import dev.robotcode.robotcode4ij.checkPythonAndRobotVersion
1214
import kotlinx.coroutines.future.await
1315
import kotlinx.coroutines.runBlocking
@@ -22,7 +24,7 @@ class RobotCodeLanguageServerManager(private val project: Project) {
2224
fun tryConfigureProject(): Boolean {
2325
project.removeUserData(LANGUAGE_SERVER_ENABLED_KEY)
2426

25-
val result = project.checkPythonAndRobotVersion()
27+
val result = project.checkPythonAndRobotVersion() == CheckPythonAndRobotVersionResult.OK
2628

2729
project.putUserData(LANGUAGE_SERVER_ENABLED_KEY, result)
2830

@@ -58,16 +60,15 @@ class RobotCodeLanguageServerManager(private val project: Project) {
5860
}
5961

6062
fun restart() {
63+
thisLogger().info("Restarting language server")
6164
stop()
6265
start()
6366
}
6467

65-
fun clearCacheAndRestart() {
68+
fun clearCache() {
6669
runBlocking {
6770
val server = LanguageServerManager.getInstance(project).getLanguageServer(LANGUAGE_SERVER_ID).await()
6871
(server?.server as RobotCodeServerApi).clearCache()?.await()
69-
70-
restart()
7172
}
7273
}
7374

intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/features/RobotSemanticTokensFeature.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package dev.robotcode.robotcode4ij.lsp.features
22

33
import com.intellij.psi.PsiElement
4-
import com.intellij.psi.PsiFile
54
import com.redhat.devtools.lsp4ij.client.features.LSPSemanticTokensFeature
65
import dev.robotcode.robotcode4ij.psi.IRobotFrameworkElementType
76
import org.toml.lang.psi.ext.elementType

0 commit comments

Comments
 (0)