Skip to content

Commit f911b1e

Browse files
authored
Make HLS path configurable (#20) (#22)
* Make HLS path configurable (#20) * configurable-hls-path: tests * configurable-hls-path: indicate default better
1 parent 3d11b16 commit f911b1e

11 files changed

+483
-2
lines changed

src/main/kotlin/boo/fox/haskelllsp/HaskellLanguageServerFactory.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,25 @@ class HaskellLanguageServer(project: Project) : ProcessStreamConnectionProvider(
2626
}?.path
2727

2828
init {
29-
val hlsPath = findExecutableInPATH()
29+
val settings = boo.fox.haskelllsp.settings.HaskellLspSettings.getInstance()
30+
val configuredPath = settings.hlsPath.takeIf { it.isNotEmpty() }
31+
val hlsPath = when {
32+
configuredPath != null && File(configuredPath).canExecute() -> configuredPath
33+
else -> findExecutableInPATH()
34+
}
35+
3036
if (!hlsPath.isNullOrEmpty()) {
3137
super.setCommands(listOf(hlsPath, "--lsp"))
3238
super.setWorkingDirectory(project.basePath)
3339
} else {
40+
val message = if (configuredPath != null) {
41+
"Configured Haskell Language Server path is invalid or not executable. Please check the path in Settings | Tools | Haskell LSP."
42+
} else {
43+
"Haskell Language Server not found. Configure the path in Settings | Tools | Haskell LSP or make sure it is installed properly and available in PATH."
44+
}
3445
NotificationGroupManager.getInstance().getNotificationGroup("Haskell LSP").createNotification(
3546
"Haskell LSP",
36-
"Haskell Language Server not found. Make sure it is installed properly (and is available in PATH), and restart the IDE.",
47+
message,
3748
NotificationType.ERROR
3849
).notify(project)
3950
LanguageServerManager.getInstance(project).stop("haskellLanguageServer")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package boo.fox.haskelllsp.settings
2+
3+
import com.intellij.openapi.application.ApplicationManager
4+
import com.intellij.openapi.components.PersistentStateComponent
5+
import com.intellij.openapi.components.State
6+
import com.intellij.openapi.components.Storage
7+
import com.intellij.util.xmlb.XmlSerializerUtil
8+
9+
@State(
10+
name = "boo.fox.haskelllsp.settings.HaskellLspSettings",
11+
storages = [Storage("HaskellLspSettings.xml")]
12+
)
13+
class HaskellLspSettings : PersistentStateComponent<HaskellLspSettings> {
14+
var hlsPath: String = ""
15+
16+
override fun getState(): HaskellLspSettings = this
17+
18+
override fun loadState(state: HaskellLspSettings) {
19+
XmlSerializerUtil.copyBean(state, this)
20+
}
21+
22+
companion object {
23+
fun getInstance(): HaskellLspSettings = ApplicationManager.getApplication().getService(HaskellLspSettings::class.java)
24+
}
25+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package boo.fox.haskelllsp.settings
2+
3+
import com.intellij.openapi.fileChooser.FileChooserDescriptor
4+
import com.intellij.openapi.ui.TextFieldWithBrowseButton
5+
import com.intellij.ui.components.JBLabel
6+
import com.intellij.util.ui.FormBuilder
7+
import javax.swing.JPanel
8+
9+
class HaskellLspSettingsComponent {
10+
private val hlsPathField = TextFieldWithBrowseButton()
11+
val panel: JPanel
12+
13+
init {
14+
hlsPathField.addBrowseFolderListener(
15+
"Select Haskell Language Server Path",
16+
"Choose the path to haskell-language-server-wrapper",
17+
null,
18+
FileChooserDescriptor(true, false, false, false, false, false)
19+
)
20+
21+
// Set default placeholder text
22+
hlsPathField.text = DEFAULT_DISPLAY_TEXT
23+
24+
// Add focus listener to clear default text when user starts editing
25+
hlsPathField.textField.addFocusListener(object : java.awt.event.FocusAdapter() {
26+
override fun focusGained(e: java.awt.event.FocusEvent?) {
27+
if (hlsPathField.text == DEFAULT_DISPLAY_TEXT) {
28+
hlsPathField.text = ""
29+
}
30+
}
31+
32+
override fun focusLost(e: java.awt.event.FocusEvent?) {
33+
if (hlsPathField.text.isEmpty()) {
34+
hlsPathField.text = DEFAULT_DISPLAY_TEXT
35+
}
36+
}
37+
})
38+
39+
panel = FormBuilder.createFormBuilder()
40+
.addLabeledComponent(JBLabel("Haskell Language Server path:"), hlsPathField)
41+
.addComponentFillVertically(JPanel(), 0)
42+
.panel
43+
}
44+
45+
fun getHlsPath(): String =
46+
if (hlsPathField.text == DEFAULT_DISPLAY_TEXT) "" else hlsPathField.text
47+
48+
fun setHlsPath(path: String) {
49+
hlsPathField.text = if (path.isEmpty()) DEFAULT_DISPLAY_TEXT else path
50+
}
51+
52+
companion object {
53+
private const val DEFAULT_DISPLAY_TEXT = "Default (search in PATH)"
54+
}
55+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package boo.fox.haskelllsp.settings
2+
3+
import com.intellij.openapi.options.Configurable
4+
import com.intellij.openapi.project.Project
5+
import com.redhat.devtools.lsp4ij.LanguageServerManager
6+
import javax.swing.JComponent
7+
8+
class HaskellLspSettingsConfigurable(private val project: Project) : Configurable {
9+
private var settingsComponent: HaskellLspSettingsComponent? = null
10+
11+
override fun getDisplayName(): String = "Haskell LSP"
12+
13+
override fun createComponent(): JComponent {
14+
settingsComponent = HaskellLspSettingsComponent()
15+
return settingsComponent!!.panel
16+
}
17+
18+
override fun isModified(): Boolean {
19+
val settings = HaskellLspSettings.getInstance()
20+
return settingsComponent?.getHlsPath() != settings.hlsPath
21+
}
22+
23+
override fun apply() {
24+
val settings = HaskellLspSettings.getInstance()
25+
val oldPath = settings.hlsPath
26+
settings.hlsPath = settingsComponent?.getHlsPath() ?: ""
27+
28+
// Restart language server if path changed
29+
if (oldPath != settings.hlsPath) {
30+
restartLanguageServer()
31+
}
32+
}
33+
34+
private fun restartLanguageServer() {
35+
val manager = LanguageServerManager.getInstance(project)
36+
// Stop the server first, then restart it
37+
manager.stop("haskellLanguageServer")
38+
manager.start("haskellLanguageServer")
39+
}
40+
41+
override fun reset() {
42+
val settings = HaskellLspSettings.getInstance()
43+
settingsComponent?.setHlsPath(settings.hlsPath)
44+
}
45+
46+
override fun disposeUIResources() {
47+
settingsComponent = null
48+
}
49+
}

src/main/resources/META-INF/plugin.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@
55

66
<vendor email="[email protected]" url="https://fox.boo">RockoFox</vendor>
77

8+
<depends>com.intellij.modules.platform</depends>
9+
10+
<applicationListeners>
11+
<listener class="boo.fox.haskelllsp.settings.HaskellLspSettings"
12+
topic="com.intellij.openapi.components.PersistentStateComponent"/>
13+
</applicationListeners>
14+
15+
<extensions defaultExtensionNs="com.intellij">
16+
<applicationService serviceImplementation="boo.fox.haskelllsp.settings.HaskellLspSettings"/>
17+
<projectConfigurable
18+
parentId="tools"
19+
instance="boo.fox.haskelllsp.settings.HaskellLspSettingsConfigurable"
20+
id="boo.fox.haskelllsp.settings.HaskellLspSettingsConfigurable"
21+
displayName="Haskell LSP"/>
22+
</extensions>
23+
824
<description>
925
<![CDATA[
1026
<p>Haskell LSP is a plugin that provides Haskell language support for IntelliJ IDEA.</p>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package boo.fox.haskelllsp
2+
3+
import boo.fox.haskelllsp.settings.HaskellLspSettings
4+
import com.intellij.testFramework.fixtures.BasePlatformTestCase
5+
import com.intellij.util.EnvironmentUtil
6+
7+
import java.io.File
8+
import kotlin.test.assertEquals
9+
import kotlin.test.assertTrue
10+
11+
class HaskellLanguageServerErrorHandlingTest : BasePlatformTestCase() {
12+
13+
private lateinit var settings: HaskellLspSettings
14+
15+
override fun setUp() {
16+
super.setUp()
17+
settings = HaskellLspSettings.getInstance()
18+
settings.hlsPath = ""
19+
}
20+
21+
fun testErrorHandlingWithNonExistentConfiguredPath() {
22+
settings.hlsPath = "/completely/nonexistent/path/haskell-language-server-wrapper"
23+
24+
// Server creation should not crash even with invalid path
25+
val server = HaskellLanguageServer(project)
26+
27+
// Test passes if server creation doesn't crash
28+
assertTrue(true)
29+
}
30+
31+
fun testErrorHandlingWithNonExecutableConfiguredPath() {
32+
// Create a file that exists but is not executable
33+
val tempFile = File.createTempFile("non-executable-hls", "")
34+
tempFile.setExecutable(false)
35+
tempFile.deleteOnExit()
36+
37+
settings.hlsPath = tempFile.absolutePath
38+
39+
// Server creation should not crash even with non-executable file
40+
val server = HaskellLanguageServer(project)
41+
42+
// Test passes if server creation doesn't crash
43+
assertTrue(true)
44+
}
45+
46+
fun testFallbackToPathWhenConfiguredPathInvalid() {
47+
// Set invalid configured path
48+
settings.hlsPath = "/invalid/path"
49+
50+
// Test fallback behavior - in a clean environment this will likely not find
51+
// anything in PATH either, but the important thing is that it doesn't crash
52+
val server = HaskellLanguageServer(project)
53+
54+
// The server should handle invalid configured path gracefully
55+
// without crashing
56+
assertTrue(true) // Test passes if no exception is thrown
57+
}
58+
59+
fun testEmptyPathAndNoPathFallback() {
60+
settings.hlsPath = ""
61+
62+
val server = HaskellLanguageServer(project)
63+
64+
// Should have no commands when both configured path and PATH fail
65+
// This should not crash the server initialization
66+
assertTrue(true) // Test passes if no exception is thrown
67+
}
68+
69+
fun testNullPathAndNoPathFallback() {
70+
settings.hlsPath = ""
71+
72+
val server = HaskellLanguageServer(project)
73+
74+
// Should have no commands when both configured path and PATH fail
75+
// This should not crash the server initialization
76+
assertTrue(true) // Test passes if no exception is thrown
77+
}
78+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package boo.fox.haskelllsp
2+
3+
import boo.fox.haskelllsp.settings.HaskellLspSettings
4+
import com.intellij.testFramework.fixtures.BasePlatformTestCase
5+
import java.io.File
6+
import kotlin.test.assertEquals
7+
import kotlin.test.assertNotNull
8+
import kotlin.test.assertTrue
9+
10+
class HaskellLanguageServerFactoryTest : BasePlatformTestCase() {
11+
12+
private lateinit var settings: HaskellLspSettings
13+
14+
override fun setUp() {
15+
super.setUp()
16+
settings = HaskellLspSettings.getInstance()
17+
settings.hlsPath = ""
18+
}
19+
20+
fun testFactoryCreation() {
21+
val factory = HaskellLanguageServerFactory()
22+
val connectionProvider = factory.createConnectionProvider(project)
23+
assertNotNull(connectionProvider)
24+
assertTrue(connectionProvider is HaskellLanguageServer)
25+
26+
val languageClient = factory.createLanguageClient(project)
27+
assertNotNull(languageClient)
28+
assertTrue(languageClient is HaskellLanguageClient)
29+
}
30+
31+
fun testHaskellLanguageServerWithValidConfiguredPath() {
32+
// Create a temporary executable file
33+
val tempFile = File.createTempFile("hls-test", "")
34+
tempFile.setExecutable(true)
35+
tempFile.deleteOnExit()
36+
37+
settings.hlsPath = tempFile.absolutePath
38+
39+
// Should not throw an exception when creating the server with valid path
40+
val server = HaskellLanguageServer(project)
41+
42+
// Test passes if server creation doesn't crash
43+
assertTrue(true)
44+
}
45+
46+
fun testHaskellLanguageServerWithInvalidConfiguredPath() {
47+
settings.hlsPath = "/nonexistent/path/to/hls"
48+
49+
// The server should not crash, but should show error notification
50+
// We can't easily test notifications in unit tests, but we can verify
51+
// that server creation doesn't crash
52+
val server = HaskellLanguageServer(project)
53+
54+
// Test passes if server creation doesn't crash
55+
assertTrue(true)
56+
}
57+
58+
fun testHaskellLanguageServerWithEmptyConfiguredPath() {
59+
settings.hlsPath = ""
60+
61+
// Test with empty PATH - should not find executable
62+
// Note: This test will not find the executable in a clean test environment
63+
// but verifies the fallback behavior
64+
val server = HaskellLanguageServer(project)
65+
66+
// The server should handle the case where no executable is found
67+
// gracefully without crashing
68+
assertTrue(true) // Test passes if no exception is thrown
69+
}
70+
71+
fun testHlsExecutableName() {
72+
// Test that the executable name is correctly determined
73+
assertEquals("haskell-language-server-wrapper", HaskellLanguageServer.HLS_EXECUTABLE_NAME)
74+
}
75+
}

0 commit comments

Comments
 (0)