diff --git a/src/main/kotlin/boo/fox/haskelllsp/HaskellLanguageServerFactory.kt b/src/main/kotlin/boo/fox/haskelllsp/HaskellLanguageServerFactory.kt index b546c1e..f2658e5 100644 --- a/src/main/kotlin/boo/fox/haskelllsp/HaskellLanguageServerFactory.kt +++ b/src/main/kotlin/boo/fox/haskelllsp/HaskellLanguageServerFactory.kt @@ -26,14 +26,25 @@ class HaskellLanguageServer(project: Project) : ProcessStreamConnectionProvider( }?.path init { - val hlsPath = findExecutableInPATH() + val settings = boo.fox.haskelllsp.settings.HaskellLspSettings.getInstance() + val configuredPath = settings.hlsPath.takeIf { it.isNotEmpty() } + val hlsPath = when { + configuredPath != null && File(configuredPath).canExecute() -> configuredPath + else -> findExecutableInPATH() + } + if (!hlsPath.isNullOrEmpty()) { super.setCommands(listOf(hlsPath, "--lsp")) super.setWorkingDirectory(project.basePath) } else { + val message = if (configuredPath != null) { + "Configured Haskell Language Server path is invalid or not executable. Please check the path in Settings | Tools | Haskell LSP." + } else { + "Haskell Language Server not found. Configure the path in Settings | Tools | Haskell LSP or make sure it is installed properly and available in PATH." + } NotificationGroupManager.getInstance().getNotificationGroup("Haskell LSP").createNotification( "Haskell LSP", - "Haskell Language Server not found. Make sure it is installed properly (and is available in PATH), and restart the IDE.", + message, NotificationType.ERROR ).notify(project) LanguageServerManager.getInstance(project).stop("haskellLanguageServer") diff --git a/src/main/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettings.kt b/src/main/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettings.kt new file mode 100644 index 0000000..659c360 --- /dev/null +++ b/src/main/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettings.kt @@ -0,0 +1,25 @@ +package boo.fox.haskelllsp.settings + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.util.xmlb.XmlSerializerUtil + +@State( + name = "boo.fox.haskelllsp.settings.HaskellLspSettings", + storages = [Storage("HaskellLspSettings.xml")] +) +class HaskellLspSettings : PersistentStateComponent { + var hlsPath: String = "" + + override fun getState(): HaskellLspSettings = this + + override fun loadState(state: HaskellLspSettings) { + XmlSerializerUtil.copyBean(state, this) + } + + companion object { + fun getInstance(): HaskellLspSettings = ApplicationManager.getApplication().getService(HaskellLspSettings::class.java) + } +} \ No newline at end of file diff --git a/src/main/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsComponent.kt b/src/main/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsComponent.kt new file mode 100644 index 0000000..d4ffba4 --- /dev/null +++ b/src/main/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsComponent.kt @@ -0,0 +1,55 @@ +package boo.fox.haskelllsp.settings + +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.FormBuilder +import javax.swing.JPanel + +class HaskellLspSettingsComponent { + private val hlsPathField = TextFieldWithBrowseButton() + val panel: JPanel + + init { + hlsPathField.addBrowseFolderListener( + "Select Haskell Language Server Path", + "Choose the path to haskell-language-server-wrapper", + null, + FileChooserDescriptor(true, false, false, false, false, false) + ) + + // Set default placeholder text + hlsPathField.text = DEFAULT_DISPLAY_TEXT + + // Add focus listener to clear default text when user starts editing + hlsPathField.textField.addFocusListener(object : java.awt.event.FocusAdapter() { + override fun focusGained(e: java.awt.event.FocusEvent?) { + if (hlsPathField.text == DEFAULT_DISPLAY_TEXT) { + hlsPathField.text = "" + } + } + + override fun focusLost(e: java.awt.event.FocusEvent?) { + if (hlsPathField.text.isEmpty()) { + hlsPathField.text = DEFAULT_DISPLAY_TEXT + } + } + }) + + panel = FormBuilder.createFormBuilder() + .addLabeledComponent(JBLabel("Haskell Language Server path:"), hlsPathField) + .addComponentFillVertically(JPanel(), 0) + .panel + } + + fun getHlsPath(): String = + if (hlsPathField.text == DEFAULT_DISPLAY_TEXT) "" else hlsPathField.text + + fun setHlsPath(path: String) { + hlsPathField.text = if (path.isEmpty()) DEFAULT_DISPLAY_TEXT else path + } + + companion object { + private const val DEFAULT_DISPLAY_TEXT = "Default (search in PATH)" + } +} \ No newline at end of file diff --git a/src/main/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsConfigurable.kt b/src/main/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsConfigurable.kt new file mode 100644 index 0000000..9ee6214 --- /dev/null +++ b/src/main/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsConfigurable.kt @@ -0,0 +1,49 @@ +package boo.fox.haskelllsp.settings + +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.project.Project +import com.redhat.devtools.lsp4ij.LanguageServerManager +import javax.swing.JComponent + +class HaskellLspSettingsConfigurable(private val project: Project) : Configurable { + private var settingsComponent: HaskellLspSettingsComponent? = null + + override fun getDisplayName(): String = "Haskell LSP" + + override fun createComponent(): JComponent { + settingsComponent = HaskellLspSettingsComponent() + return settingsComponent!!.panel + } + + override fun isModified(): Boolean { + val settings = HaskellLspSettings.getInstance() + return settingsComponent?.getHlsPath() != settings.hlsPath + } + + override fun apply() { + val settings = HaskellLspSettings.getInstance() + val oldPath = settings.hlsPath + settings.hlsPath = settingsComponent?.getHlsPath() ?: "" + + // Restart language server if path changed + if (oldPath != settings.hlsPath) { + restartLanguageServer() + } + } + + private fun restartLanguageServer() { + val manager = LanguageServerManager.getInstance(project) + // Stop the server first, then restart it + manager.stop("haskellLanguageServer") + manager.start("haskellLanguageServer") + } + + override fun reset() { + val settings = HaskellLspSettings.getInstance() + settingsComponent?.setHlsPath(settings.hlsPath) + } + + override fun disposeUIResources() { + settingsComponent = null + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c336e77..3ffca9f 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -5,6 +5,22 @@ RockoFox + com.intellij.modules.platform + + + + + + + + + + Haskell LSP is a plugin that provides Haskell language support for IntelliJ IDEA.

diff --git a/src/test/kotlin/boo/fox/haskelllsp/HaskellLanguageServerErrorHandlingTest.kt b/src/test/kotlin/boo/fox/haskelllsp/HaskellLanguageServerErrorHandlingTest.kt new file mode 100644 index 0000000..0c77b70 --- /dev/null +++ b/src/test/kotlin/boo/fox/haskelllsp/HaskellLanguageServerErrorHandlingTest.kt @@ -0,0 +1,78 @@ +package boo.fox.haskelllsp + +import boo.fox.haskelllsp.settings.HaskellLspSettings +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.util.EnvironmentUtil + +import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class HaskellLanguageServerErrorHandlingTest : BasePlatformTestCase() { + + private lateinit var settings: HaskellLspSettings + + override fun setUp() { + super.setUp() + settings = HaskellLspSettings.getInstance() + settings.hlsPath = "" + } + + fun testErrorHandlingWithNonExistentConfiguredPath() { + settings.hlsPath = "/completely/nonexistent/path/haskell-language-server-wrapper" + + // Server creation should not crash even with invalid path + val server = HaskellLanguageServer(project) + + // Test passes if server creation doesn't crash + assertTrue(true) + } + + fun testErrorHandlingWithNonExecutableConfiguredPath() { + // Create a file that exists but is not executable + val tempFile = File.createTempFile("non-executable-hls", "") + tempFile.setExecutable(false) + tempFile.deleteOnExit() + + settings.hlsPath = tempFile.absolutePath + + // Server creation should not crash even with non-executable file + val server = HaskellLanguageServer(project) + + // Test passes if server creation doesn't crash + assertTrue(true) + } + + fun testFallbackToPathWhenConfiguredPathInvalid() { + // Set invalid configured path + settings.hlsPath = "/invalid/path" + + // Test fallback behavior - in a clean environment this will likely not find + // anything in PATH either, but the important thing is that it doesn't crash + val server = HaskellLanguageServer(project) + + // The server should handle invalid configured path gracefully + // without crashing + assertTrue(true) // Test passes if no exception is thrown + } + + fun testEmptyPathAndNoPathFallback() { + settings.hlsPath = "" + + val server = HaskellLanguageServer(project) + + // Should have no commands when both configured path and PATH fail + // This should not crash the server initialization + assertTrue(true) // Test passes if no exception is thrown + } + + fun testNullPathAndNoPathFallback() { + settings.hlsPath = "" + + val server = HaskellLanguageServer(project) + + // Should have no commands when both configured path and PATH fail + // This should not crash the server initialization + assertTrue(true) // Test passes if no exception is thrown + } +} \ No newline at end of file diff --git a/src/test/kotlin/boo/fox/haskelllsp/HaskellLanguageServerFactoryTest.kt b/src/test/kotlin/boo/fox/haskelllsp/HaskellLanguageServerFactoryTest.kt new file mode 100644 index 0000000..923248a --- /dev/null +++ b/src/test/kotlin/boo/fox/haskelllsp/HaskellLanguageServerFactoryTest.kt @@ -0,0 +1,75 @@ +package boo.fox.haskelllsp + +import boo.fox.haskelllsp.settings.HaskellLspSettings +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class HaskellLanguageServerFactoryTest : BasePlatformTestCase() { + + private lateinit var settings: HaskellLspSettings + + override fun setUp() { + super.setUp() + settings = HaskellLspSettings.getInstance() + settings.hlsPath = "" + } + + fun testFactoryCreation() { + val factory = HaskellLanguageServerFactory() + val connectionProvider = factory.createConnectionProvider(project) + assertNotNull(connectionProvider) + assertTrue(connectionProvider is HaskellLanguageServer) + + val languageClient = factory.createLanguageClient(project) + assertNotNull(languageClient) + assertTrue(languageClient is HaskellLanguageClient) + } + + fun testHaskellLanguageServerWithValidConfiguredPath() { + // Create a temporary executable file + val tempFile = File.createTempFile("hls-test", "") + tempFile.setExecutable(true) + tempFile.deleteOnExit() + + settings.hlsPath = tempFile.absolutePath + + // Should not throw an exception when creating the server with valid path + val server = HaskellLanguageServer(project) + + // Test passes if server creation doesn't crash + assertTrue(true) + } + + fun testHaskellLanguageServerWithInvalidConfiguredPath() { + settings.hlsPath = "/nonexistent/path/to/hls" + + // The server should not crash, but should show error notification + // We can't easily test notifications in unit tests, but we can verify + // that server creation doesn't crash + val server = HaskellLanguageServer(project) + + // Test passes if server creation doesn't crash + assertTrue(true) + } + + fun testHaskellLanguageServerWithEmptyConfiguredPath() { + settings.hlsPath = "" + + // Test with empty PATH - should not find executable + // Note: This test will not find the executable in a clean test environment + // but verifies the fallback behavior + val server = HaskellLanguageServer(project) + + // The server should handle the case where no executable is found + // gracefully without crashing + assertTrue(true) // Test passes if no exception is thrown + } + + fun testHlsExecutableName() { + // Test that the executable name is correctly determined + assertEquals("haskell-language-server-wrapper", HaskellLanguageServer.HLS_EXECUTABLE_NAME) + } +} \ No newline at end of file diff --git a/src/test/kotlin/boo/fox/haskelllsp/HaskellLspIntegrationTest.kt b/src/test/kotlin/boo/fox/haskelllsp/HaskellLspIntegrationTest.kt new file mode 100644 index 0000000..fe7ba6d --- /dev/null +++ b/src/test/kotlin/boo/fox/haskelllsp/HaskellLspIntegrationTest.kt @@ -0,0 +1,60 @@ +package boo.fox.haskelllsp + +import boo.fox.haskelllsp.settings.HaskellLspSettings +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import java.io.File +import kotlin.test.assertTrue + +class HaskellLspIntegrationTest : BasePlatformTestCase() { + + fun testSettingsIntegrationWithLanguageServer() { + val settings = HaskellLspSettings.getInstance() + + // Test with empty settings + settings.hlsPath = "" + val server1 = HaskellLanguageServer(project) + assertTrue(true) // Should not crash + + // Test with valid settings + val tempFile = File.createTempFile("hls-integration", "") + tempFile.setExecutable(true) + tempFile.deleteOnExit() + + settings.hlsPath = tempFile.absolutePath + val server2 = HaskellLanguageServer(project) + assertTrue(true) // Should not crash + + // Test with invalid settings + settings.hlsPath = "/invalid/path" + val server3 = HaskellLanguageServer(project) + assertTrue(true) // Should not crash + } + + fun testFactoryWithDifferentSettings() { + val settings = HaskellLspSettings.getInstance() + val originalPath = settings.hlsPath + + try { + // Test factory creation with different path settings + settings.hlsPath = "" + val factory1 = HaskellLanguageServerFactory() + val connection1 = factory1.createConnectionProvider(project) + assertTrue(connection1 is HaskellLanguageServer) + + val tempFile = File.createTempFile("hls-factory", "") + tempFile.setExecutable(true) + tempFile.deleteOnExit() + + settings.hlsPath = tempFile.absolutePath + val factory2 = HaskellLanguageServerFactory() + val connection2 = factory2.createConnectionProvider(project) + assertTrue(connection2 is HaskellLanguageServer) + + val client = factory2.createLanguageClient(project) + assertTrue(client is HaskellLanguageClient) + } finally { + // Restore original settings + settings.hlsPath = originalPath + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsComponentTest.kt b/src/test/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsComponentTest.kt new file mode 100644 index 0000000..134ac9b --- /dev/null +++ b/src/test/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsComponentTest.kt @@ -0,0 +1,34 @@ +package boo.fox.haskelllsp.settings + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class HaskellLspSettingsComponentTest : BasePlatformTestCase() { + + fun testComponentCreation() { + val component = HaskellLspSettingsComponent() + assertTrue(component.panel != null) + assertEquals("", component.getHlsPath()) // Should return empty string for actual value + } + + fun testHlsPathSetting() { + val component = HaskellLspSettingsComponent() + + // Initially should be empty (default shown in UI but empty value) + assertEquals("", component.getHlsPath()) + + // Test setting path + component.setHlsPath("/usr/bin/haskell-language-server") + assertEquals("/usr/bin/haskell-language-server", component.getHlsPath()) + + // Test setting empty path should return to default + component.setHlsPath("") + assertEquals("", component.getHlsPath()) + + // Test setting complex path + val complexPath = "/opt/custom/bin/haskell-language-server-wrapper" + component.setHlsPath(complexPath) + assertEquals(complexPath, component.getHlsPath()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsConfigurableTest.kt b/src/test/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsConfigurableTest.kt new file mode 100644 index 0000000..426e943 --- /dev/null +++ b/src/test/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsConfigurableTest.kt @@ -0,0 +1,36 @@ +package boo.fox.haskelllsp.settings + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class HaskellLspSettingsConfigurableTest : BasePlatformTestCase() { + + fun testConfigurableCreation() { + val configurable = HaskellLspSettingsConfigurable(project) + assertEquals("Haskell LSP", configurable.displayName) + } + + fun testConfigurableComponentCreation() { + val configurable = HaskellLspSettingsConfigurable(project) + + // Create component to test initial state + val component = configurable.createComponent() + assertTrue(component != null) + } + + fun testConfigurableApplyAndReset() { + val configurable = HaskellLspSettingsConfigurable(project) + + // Create component to ensure UI is initialized + configurable.createComponent() + + // Test basic functionality - these operations should not crash + configurable.reset() + configurable.apply() + + // Test passes if no exception is thrown + assertTrue(true) + } +} \ No newline at end of file diff --git a/src/test/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsTest.kt b/src/test/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsTest.kt new file mode 100644 index 0000000..06e51fe --- /dev/null +++ b/src/test/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettingsTest.kt @@ -0,0 +1,42 @@ +package boo.fox.haskelllsp.settings + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.assertNotNull + +class HaskellLspSettingsTest : BasePlatformTestCase() { + + fun testSettingsDefaults() { + val settings = HaskellLspSettings() + assertEquals("", settings.hlsPath) + } + + fun testSettingsPersistence() { + val originalSettings = HaskellLspSettings() + originalSettings.hlsPath = "/usr/bin/haskell-language-server-wrapper" + assertEquals("/usr/bin/haskell-language-server-wrapper", originalSettings.hlsPath) + + // Test loading state (simulate what happens when settings are loaded) + val newSettings = HaskellLspSettings() + newSettings.loadState(originalSettings) + assertEquals(originalSettings.hlsPath, newSettings.hlsPath) + } + + fun testSettingsStateCopy() { + val originalSettings = HaskellLspSettings() + originalSettings.hlsPath = "/custom/path/hls" + + val copiedSettings = HaskellLspSettings() + copiedSettings.loadState(originalSettings) + + assertEquals(originalSettings.hlsPath, copiedSettings.hlsPath) + assertTrue(originalSettings !== copiedSettings) + } + + fun testGetInstance() { + val instance = HaskellLspSettings.getInstance() + assertNotNull(instance) + assertTrue(instance is HaskellLspSettings) + } +} \ No newline at end of file