Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/main/kotlin/boo/fox/haskelllsp/HaskellLanguageServerFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
25 changes: 25 additions & 0 deletions src/main/kotlin/boo/fox/haskelllsp/settings/HaskellLspSettings.kt
Original file line number Diff line number Diff line change
@@ -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<HaskellLspSettings> {
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)
}
}
Original file line number Diff line number Diff line change
@@ -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)"
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
16 changes: 16 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@

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

<depends>com.intellij.modules.platform</depends>

<applicationListeners>
<listener class="boo.fox.haskelllsp.settings.HaskellLspSettings"
topic="com.intellij.openapi.components.PersistentStateComponent"/>
</applicationListeners>

<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="boo.fox.haskelllsp.settings.HaskellLspSettings"/>
<projectConfigurable
parentId="tools"
instance="boo.fox.haskelllsp.settings.HaskellLspSettingsConfigurable"
id="boo.fox.haskelllsp.settings.HaskellLspSettingsConfigurable"
displayName="Haskell LSP"/>
</extensions>

<description>
<![CDATA[
<p>Haskell LSP is a plugin that provides Haskell language support for IntelliJ IDEA.</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading