diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 48d4c49..c537241 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -24,7 +24,11 @@ jobs:
shell: bash
run: pwd && ls -la
- - name: Build plugin
+ - name: Test plugin
+ shell: bash
+ run: ./gradlew test
+
+ - name: Build plugin's binary
shell: bash
run: ./gradlew buildPlugin
diff --git a/.idea/LanguageServersSettings.xml b/.idea/LanguageServersSettings.xml
new file mode 100644
index 0000000..ff5bb67
--- /dev/null
+++ b/.idea/LanguageServersSettings.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..2b0747c
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/zowe_ijmp.xml b/.idea/copyright/zowe_ijmp.xml
new file mode 100644
index 0000000..3b5ccef
--- /dev/null
+++ b/.idea/copyright/zowe_ijmp.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index b2b3b86..fe0cee8 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -16,10 +16,14 @@ plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.9.21"
id("org.jetbrains.intellij") version "1.16.1"
+ id("org.jetbrains.kotlinx.kover") version "0.8.1"
}
group = properties("pluginGroup").get()
version = properties("pluginVersion").get()
+val kotestVersion = "5.9.1"
+val mockkVersion = "1.13.11"
+val junitVersion = ""
repositories {
mavenCentral()
@@ -32,7 +36,20 @@ intellij {
// pluginsRepositories {
// custom("https://plugins.jetbrains.com/plugins/nightly/23257")
// }
- plugins.set(listOf("org.jetbrains.plugins.textmate", "com.redhat.devtools.lsp4ij:0.0.1"))
+ plugins.set(listOf("org.jetbrains.plugins.textmate", "com.redhat.devtools.lsp4ij:0.0.2"))
+}
+
+dependencies {
+ // ===== Test env setup =====
+ // Kotest
+ testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
+ testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
+ // MockK
+ testImplementation("io.mockk:mockk:$mockkVersion")
+ // JUnit Platform (needed for Kotest)
+ testImplementation("org.junit.platform:junit-platform-launcher:1.10.2")
+ // ==========================
+
}
tasks {
@@ -50,4 +67,12 @@ tasks {
sinceBuild.set(properties("pluginSinceBuild").get())
untilBuild.set(properties("pluginUntilBuild").get())
}
+
+ test {
+ useJUnitPlatform()
+ testLogging {
+ events("passed", "skipped", "failed")
+ }
+ finalizedBy("koverHtmlReport")
+ }
}
diff --git a/src/main/kotlin/org/zowe/cobol/CobolProjectManagerListener.kt b/src/main/kotlin/org/zowe/cobol/CobolProjectManagerListener.kt
new file mode 100644
index 0000000..f8ffba1
--- /dev/null
+++ b/src/main/kotlin/org/zowe/cobol/CobolProjectManagerListener.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2024 IBA Group.
+ *
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBA Group
+ * Zowe Community
+ */
+
+package org.zowe.cobol
+
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.ProjectManager
+import com.intellij.openapi.project.ProjectManagerListener
+import org.zowe.cobol.state.CobolPluginState
+import org.zowe.cobol.state.InitializationOnly
+import org.zowe.cobol.state.LanguageSupportStateService
+
+/** COBOL project manager listener. Listens to projects changes and react to them respectively */
+class CobolProjectManagerListener : ProjectManagerListener {
+
+ /**
+ * Delete TextMate bundle if the last opened project is being closed
+ * (the only possible way to handle plug-in's TextMate bundle to be deleted when the plug-in is uninstalled)
+ */
+ @OptIn(InitializationOnly::class)
+ override fun projectClosing(project: Project) {
+ val lsStateService = LanguageSupportStateService.instance
+ val pluginState = lsStateService.getPluginState(project) { CobolPluginState(project) }
+
+ if (isLastProjectClosing() && (pluginState.isLSPClientReady() || pluginState.isLSPServerConnectionReady())) {
+ pluginState.unloadLSPClient {}
+ pluginState.finishDeinitialization {}
+ }
+ }
+
+ /** Check if the project being closed is the last one that was opened */
+ private fun isLastProjectClosing(): Boolean {
+ return ProjectManager.getInstance().openProjects.size == 1
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/org/zowe/cobol/init/CobolPluginState.kt b/src/main/kotlin/org/zowe/cobol/init/CobolPluginState.kt
deleted file mode 100644
index c27ff5e..0000000
--- a/src/main/kotlin/org/zowe/cobol/init/CobolPluginState.kt
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * This program and the accompanying materials are made available under the terms of the
- * Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * https://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- * Copyright Contributors to the Zowe Project
- */
-
-package org.zowe.cobol.init
-
-import com.intellij.openapi.Disposable
-import com.intellij.openapi.application.PathManager
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.util.Disposer
-import com.intellij.util.io.ZipUtil
-import com.jetbrains.rd.util.firstOrNull
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import org.jetbrains.plugins.textmate.TextMateService
-import org.jetbrains.plugins.textmate.configuration.TextMateUserBundlesSettings
-import org.jetbrains.plugins.textmate.plist.JsonPlistReader
-import com.intellij.openapi.util.io.FileUtil
-import com.redhat.devtools.lsp4ij.client.LanguageClientImpl
-import com.redhat.devtools.lsp4ij.server.JavaProcessCommandBuilder
-import com.redhat.devtools.lsp4ij.server.ProcessStreamConnectionProvider
-import org.zowe.cobol.lsp.CobolLanguageClient
-import java.nio.file.Path
-import kotlin.io.path.exists
-import kotlin.io.path.pathString
-import kotlin.io.path.readText
-
-// https://github.com/eclipse-che4z/che-che4z-lsp-for-cobol
-private const val VSIX_NAME = "cobol-language-support"
-private const val VSIX_VERSION = "2.1.2"
-private const val TEXTMATE_BUNDLE_NAME = "cobol"
-
-/**
- * State of the COBOL plug-in. Provides initialization methods to set up all the things before the correct usage of
- * the syntax highlighting and the LSP features
- */
-class CobolPluginState private constructor() : Disposable {
-
- companion object {
- private val projectToPluginState = mutableMapOf()
-
- /**
- * Get initialized plug-in state by the project. If there is no plugin state initialized for the project,
- * the new state is initialized
- * @param project the project to get or initialize the plug-in's state
- * @return initialized plug-in's state
- */
- fun getPluginState(project: Project): CobolPluginState {
- val pluginState = projectToPluginState[project] ?: CobolPluginState()
- projectToPluginState[project] = pluginState
- return pluginState
- }
-
- /** Get all initialized plug-in's states */
- fun getAllPluginStates() = projectToPluginState
- }
-
- private var currState: InitStates = InitStates.DOWN
- private lateinit var stateProject: Project
- private lateinit var vsixPlacingRootPath: Path
- private lateinit var vsixUnpackedPath: Path
- private lateinit var packageJsonPath: Path
- private lateinit var lspServerPath: Path
-
- /**
- * Compute all the paths needed for the plug-in's setup
- * @return boolean that indicates if the paths are already exist
- */
- private fun computeVSIXPlacingPaths(): Boolean {
- vsixPlacingRootPath = PathManager.getConfigDir().resolve(VSIX_NAME)
- vsixUnpackedPath = vsixPlacingRootPath.resolve("extension")
- packageJsonPath = vsixUnpackedPath.resolve("package.json")
- lspServerPath = vsixUnpackedPath.resolve("server").resolve("jar").resolve("server.jar")
- val syntaxesPath = vsixUnpackedPath.resolve("syntaxes")
- return vsixUnpackedPath.exists() && packageJsonPath.exists() && lspServerPath.exists() && syntaxesPath.exists()
- }
-
- /**
- * Unzip .vsix file in the 'resources' folder into the 'build' path, and later use the unzipped files to activate
- * a TextMate bundle and an LSP server. If the paths of the unzipped .vsix are already exist, the processing is skipped
- */
- @InitializationOnly
- suspend fun unpackVSIX() {
-// if (currState != InitStates.DOWN) throw IllegalStateException("Invalid plug-in state. Expected: ${InitStates.DOWN}, current: $currState")
- val doPathsAlreadyExist = computeVSIXPlacingPaths()
- if (!doPathsAlreadyExist) {
- val activeClassLoader = this::class.java.classLoader
- currState = InitStates.VSIX_UNPACK_TRIGGERED
- val vsixNameWithVersion = "$VSIX_NAME-$VSIX_VERSION"
- val vsixWithExt = "$vsixNameWithVersion.vsix"
- return withContext(Dispatchers.IO) {
- val vsixTempFile = FileUtil.createTempFile(VSIX_NAME, ".vsix")
- val vsixResource = activeClassLoader
- .getResourceAsStream(vsixWithExt)
- ?: throw Exception("No $vsixWithExt found")
- vsixTempFile.writeBytes(vsixResource.readAllBytes())
- ZipUtil.extract(vsixTempFile.toPath(), vsixPlacingRootPath, null)
- currState = InitStates.VSIX_UNPACKED
- }
- } else {
- currState = InitStates.VSIX_UNPACKED
- }
- }
-
- /**
- * Load a TextMate bundle from previously unzipped .vsix. The version of the bundle to activate is the same as the
- * .vsix package has. If there is an already activated version of the bundle with the same name, it will be deleted
- * if the version is less than the one it is trying to activate. If the versions are the same, or there are any
- * troubles unzipping/using the provided bundle, the processing does not continue, and the bundle that is already
- * loaded to the IDE stays there
- */
- @InitializationOnly
- fun loadLanguageClientDefinition(project: Project): LanguageClientImpl {
-// if (currState < InitStates.VSIX_UNPACKED) throw IllegalStateException("Invalid plug-in state. Expected: at least ${InitStates.VSIX_UNPACKED}, current: $currState")
- currState = InitStates.TEXTMATE_BUNDLE_LOAD_TRIGGERED
- val emptyBundleName = "$TEXTMATE_BUNDLE_NAME-0.0.0"
- val newBundleName = "$TEXTMATE_BUNDLE_NAME-$VSIX_VERSION"
- var existingBundles = TextMateUserBundlesSettings.instance?.bundles
- val existingBundle = existingBundles
- ?.filter { it.value.name.contains(TEXTMATE_BUNDLE_NAME) }
- ?.firstOrNull()
- val existingBundleName = existingBundle?.value?.name ?: emptyBundleName
- if (existingBundleName < newBundleName) {
- existingBundles = existingBundles?.filter { it.value.name != existingBundleName } ?: emptyMap()
- TextMateUserBundlesSettings.instance?.setBundlesConfig(existingBundles)
- TextMateUserBundlesSettings.instance?.addBundle(vsixUnpackedPath.toString(), newBundleName)
- TextMateService.getInstance().reloadEnabledBundles()
- }
- currState = InitStates.TEXTMATE_BUNDLE_LOADED
- return CobolLanguageClient(project)
- }
-
- /** Extract COBOL language extensions, supported for recognition, from package.json in resources */
- private fun extractExtensionsFromPackageJson(): List {
- val packageJsonContent = packageJsonPath.readText()
- val cobolExtensions = mutableListOf()
- try {
- val json = JsonPlistReader.createJsonReader()
- .readValue(packageJsonContent, Any::class.java)
- if (json is Map<*, *>) {
- val contributes = json["contributes"]
- if (contributes is Map<*, *>) {
- val languages = contributes["languages"]
- if (languages is ArrayList<*>) {
- for (language in languages) {
- if (language is Map<*, *>) {
- val id = language["id"]
- if (id is String && id == "cobol") {
- val extensions = language["extensions"]
- if (extensions is ArrayList<*>) {
- val extensionsStrs = extensions.map {
- ext: Any? -> if (ext is String) { ext.trimStart('.') } else { "" }
- }
- cobolExtensions.addAll(extensionsStrs)
- }
- val filenames = language["filenames"]
- if (filenames is ArrayList<*>) {
- val filenamesStrs = filenames.map {
- filename: Any? -> if (filename is String) { filename } else { "" }
- }
- cobolExtensions.addAll(filenamesStrs)
- }
- }
- }
- }
- }
- }
- }
- } catch (ignored: Exception) {
- }
- return cobolExtensions
- }
-
- /** Initialize language server definition. Will run the LSP server command */
- @InitializationOnly
- fun loadLanguageServerDefinition(project: Project): ProcessStreamConnectionProvider {
-// if (currState < InitStates.VSIX_UNPACKED) throw IllegalStateException("Invalid plug-in state. Expected: at least ${InitStates.VSIX_UNPACKED}, current: $currState")
- currState = InitStates.LSP_LOAD_TRIGGERED
- val lspServerPathString = lspServerPath.pathString
-// val extensions = extractExtensionsFromPackageJson()
- val commands: MutableList = JavaProcessCommandBuilder(project, "cobol")
- .setJar(lspServerPathString)
- .create()
- commands.add("pipeEnabled")
- currState = InitStates.LSP_LOADED
- return object : ProcessStreamConnectionProvider(commands) {}
- }
-
- /** Initialization final step, no direct purposes for now */
- @InitializationOnly
- fun finishInitialization(project: Project) {
- if (currState != InitStates.LSP_LOADED || currState != InitStates.TEXTMATE_BUNDLE_LOADED) throw IllegalStateException("Invalid plug-in state. Expected: at least ${InitStates.LSP_LOADED}, current: $currState")
- stateProject = project
- currState = InitStates.UP
- }
-
- /** Disable the COBOL plug-in TextMate bundle before the plug-in is unloaded */
- @InitializationOnly
- fun disableTextMateBundle() {
- if (currState != InitStates.UP) throw IllegalStateException("Invalid plug-in state. Expected: ${InitStates.UP}, current: $currState")
- currState = InitStates.TEXTMATE_BUNDLE_UNLOAD_TRIGGERED
- var existingBundles = TextMateUserBundlesSettings.instance?.bundles
- existingBundles = existingBundles?.filter { it.value.name.contains(TEXTMATE_BUNDLE_NAME) } ?: emptyMap()
- TextMateUserBundlesSettings.instance?.setBundlesConfig(existingBundles)
- TextMateService.getInstance().reloadEnabledBundles()
- currState = InitStates.TEXTMATE_BUNDLE_UNLOADED
- }
-
- // TODO: finish, doc
-// /** Disable LSP server wrappers together with LSP servers for the project before the plug-in's state is disposed */
-// @InitializationOnly
-// fun disableLSP() {
-// if (currState > InitStates.TEXTMATE_BUNDLE_UNLOADED) throw IllegalStateException("Invalid plug-in state. Expected: at most ${InitStates.TEXTMATE_BUNDLE_UNLOADED}, current: $currState")
-// currState = InitStates.LSP_UNLOAD_TRIGGERED
-// val projectPath = FileUtils.projectToUri(stateProject)
-// val serverWrappers = IntellijLanguageClient.getAllServerWrappersFor(projectPath)
-// serverWrappers.forEach { it.stop(true) }
-// currState = InitStates.LSP_UNLOADED
-// }
-
- /** Deinitialization final step, disposing purposes */
- @InitializationOnly
- fun finishDeinitialization() {
- if (currState > InitStates.LSP_UNLOADED) throw IllegalStateException("Invalid plug-in state. Expected: at most ${InitStates.LSP_UNLOADED}, current: $currState")
- currState = InitStates.DOWN
- this.dispose()
- }
-
- override fun dispose() {
- Disposer.dispose(this)
- }
-}
diff --git a/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageClient.kt b/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageClient.kt
index 9f58143..f509b14 100644
--- a/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageClient.kt
+++ b/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageClient.kt
@@ -1,11 +1,15 @@
/*
+ * Copyright (c) 2024 IBA Group.
+ *
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
- * Copyright Contributors to the Zowe Project
+ * Contributors:
+ * IBA Group
+ * Zowe Community
*/
package org.zowe.cobol.lsp
@@ -13,16 +17,23 @@ package org.zowe.cobol.lsp
import com.intellij.openapi.project.Project
import com.redhat.devtools.lsp4ij.client.LanguageClientImpl
import org.eclipse.lsp4j.ConfigurationParams
+import org.eclipse.lsp4j.MessageParams
+import org.eclipse.lsp4j.MessageType
import java.util.concurrent.CompletableFuture
-private const val DIALECT_REGISTRY_SECTION = "cobol-lsp.dialect.registry"
-private const val SETTINGS_DIALECT = "cobol-lsp.dialects"
-private const val SETTINGS_CPY_LOCAL_PATH = "cobol-lsp.cpy-manager.paths-local"
-private const val DIALECT_LIBS = "cobol-lsp.dialect.libs"
-private const val SETTINGS_CPY_EXTENSIONS = "cobol-lsp.cpy-manager.copybook-extensions"
-private const val SETTINGS_SQL_BACKEND = "cobol-lsp.target-sql-backend"
-private const val SETTINGS_CPY_FILE_ENCODING = "cobol-lsp.cpy-manager.copybook-file-encoding"
-private const val SETTINGS_COMPILE_OPTIONS = "cobol-lsp.compiler.options"
+const val DIALECT_REGISTRY_SECTION = "cobol-lsp.dialect.registry"
+const val SETTINGS_DIALECT = "cobol-lsp.dialects"
+const val SETTINGS_CPY_LOCAL_PATH = "cobol-lsp.cpy-manager.paths-local"
+const val DIALECT_LIBS = "cobol-lsp.dialect.libs"
+const val SETTINGS_CPY_EXTENSIONS = "cobol-lsp.cpy-manager.copybook-extensions"
+const val SETTINGS_SQL_BACKEND = "cobol-lsp.target-sql-backend"
+const val SETTINGS_CPY_FILE_ENCODING = "cobol-lsp.cpy-manager.copybook-file-encoding"
+const val SETTINGS_COMPILE_OPTIONS = "cobol-lsp.compiler.options"
+const val SETTINGS_CLIENT_LOGGING_LEVEL = "cobol-lsp.logging.level.root"
+const val SETTINGS_LOCALE = "cobol-lsp.locale"
+const val SETTINGS_COBOL_PROGRAM_LAYOUT = "cobol-lsp.cobol.program.layout"
+const val SETTINGS_SUBROUTINE_LOCAL_PATH = "cobol-lsp.subroutine-manager.paths-local"
+const val SETTINGS_CICS_TRANSLATOR = "cobol-lsp.cics.translator"
/** COBOL LSP client wrapper. Provides a comprehensive support for the COBOL LSP communications */
class CobolLanguageClient(project: Project) : LanguageClientImpl(project) {
@@ -39,7 +50,7 @@ class CobolLanguageClient(project: Project) : LanguageClientImpl(project) {
for (item in configurationParams?.items ?: emptyList()) {
try {
if (item.section == DIALECT_REGISTRY_SECTION) {
- System.err.println("${item.section} is not correctly recognized yet 1")
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 1"))
result.add(emptyList())
// val computed = DialectRegistry.getDialects()
// result.add(computed)
@@ -47,77 +58,77 @@ class CobolLanguageClient(project: Project) : LanguageClientImpl(project) {
// val cfg = vscode.workspace.getConfiguration().get(item.section)
when (item.section) {
SETTINGS_DIALECT -> {
- System.err.println("${item.section} is not correctly recognized yet 2")
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 2"))
result.add(emptyList())
// val computed = loadProcessorGroupDialectConfig(item, cfg)
// result.add(computed)
}
SETTINGS_CPY_LOCAL_PATH -> {
- System.err.println("${item.section} is not recognized yet 3")
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not recognized yet 3"))
// val computed = loadProcessorGroupCopybookPathsConfig(item, cfg as List)
// result.add(computed)
// } else if (item.section === DIALECT_LIBS && !!item.dialect) {
}
DIALECT_LIBS -> {
- System.err.println("${item.section} is not recognized yet 4")
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not recognized yet 4"))
// val dialectLibs = SettingsService.getCopybookLocalPath(item.scopeUri, item.dialect)
// result.add(dialectLibs)
}
SETTINGS_CPY_EXTENSIONS -> {
- System.err.println("${item.section} is not correctly recognized 5")
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized 5"))
result.add(listOf(".CPY", ".COPY", ".cpy", ".copy",""))
// val computed = loadProcessorGroupCopybookExtensionsConfig(item, cfg as List)
// result.add(computed)
}
SETTINGS_SQL_BACKEND -> {
- System.err.println("${item.section} is not correctly recognized yet 6")
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 6"))
result.add("DB2_SERVER")
// val computed = loadProcessorGroupSqlBackendConfig(item, cfg as String)
// result.add(computed)
}
SETTINGS_CPY_FILE_ENCODING -> {
- System.err.println("${item.section} is not recognized yet 7")
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not recognized yet 7"))
// val computed = loadProcessorGroupCopybookEncodingConfig(item, cfg as String)
// result.add(computed)
}
SETTINGS_COMPILE_OPTIONS -> {
- System.err.println("${item.section} is not correctly recognized yet 8")
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 8"))
result.add(null)
// val computed = loadProcessorGroupCompileOptionsConfig(item, cfg as String)
// result.add(computed)
}
- "cobol-lsp.logging.level.root" -> {
- System.err.println("${item.section} is not correctly recognized 11")
+ SETTINGS_CLIENT_LOGGING_LEVEL -> {
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized 11"))
result.add("ERROR")
}
- "cobol-lsp.locale" -> {
- System.err.println("${item.section} is not correctly recognized 12")
+ SETTINGS_LOCALE -> {
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized 12"))
result.add("en")
}
- "cobol-lsp.cobol.program.layout is not recognized yet 13" -> {
- System.err.println("${item.section} is not correctly recognized yet 12")
+ SETTINGS_COBOL_PROGRAM_LAYOUT -> {
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 12"))
result.add(null)
}
- "cobol-lsp.subroutine-manager.paths-local" -> {
- System.err.println("${item.section} is not correctly recognized yet 14")
+ SETTINGS_SUBROUTINE_LOCAL_PATH -> {
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 14"))
result.add(emptyList())
}
- "cobol-lsp.cics.translator" -> {
- System.err.println("${item.section} is not correctly recognized yet 15")
+ SETTINGS_CICS_TRANSLATOR -> {
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 15"))
result.add("true")
}
else -> {
// result.add(cfg)
- System.err.println("${item.section} is not recognized yet 9")
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not recognized yet 9"))
}
}
} else {
- System.err.println("${item.section} is not correctly recognized yet 10")
+ logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 10"))
result.add(emptyList())
// result.add(vscode.workspace.getConfiguration().get(item.section));
}
} catch (error: Throwable) {
- System.err.println(error)
+ logMessage(MessageParams(MessageType.Error, "${error.message}\n${error.stackTrace}"))
}
}
return CompletableFuture.completedFuture(result)
diff --git a/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactory.kt b/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactory.kt
index ff3907e..07f3a41 100644
--- a/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactory.kt
+++ b/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactory.kt
@@ -1,38 +1,56 @@
/*
+ * Copyright (c) 2024 IBA Group.
+ *
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
- * Copyright Contributors to the Zowe Project
+ * Contributors:
+ * IBA Group
+ * Zowe Community
*/
package org.zowe.cobol.lsp
+import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.redhat.devtools.lsp4ij.LanguageServerFactory
import com.redhat.devtools.lsp4ij.client.LanguageClientImpl
import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider
-import kotlinx.coroutines.runBlocking
-import org.zowe.cobol.init.CobolPluginState
-import org.zowe.cobol.init.InitializationOnly
+import org.zowe.cobol.state.COBOL_PLUGIN_NOTIFICATION_ID
+import org.zowe.cobol.state.CobolPluginState
+import org.zowe.cobol.state.InitializationOnly
+import org.zowe.cobol.state.LanguageSupportStateService
-// TODO: doc
-@OptIn(InitializationOnly::class)
+/** COBOL language server factory to provide all the necessary functionalities for COBOL language support in the IDE */
class CobolLanguageServerFactory : LanguageServerFactory {
override fun createConnectionProvider(project: Project): StreamConnectionProvider {
- val pliPluginState = CobolPluginState.getPluginState(project)
- runBlocking {
- pliPluginState.unpackVSIX()
+ val lsStateService = LanguageSupportStateService.instance
+ val pluginState = lsStateService.getPluginState(project) { CobolPluginState(project) }
+
+ @OptIn(InitializationOnly::class)
+ if (!pluginState.isLSPServerConnectionReady()) {
+ pluginState.prepareVSIX {}
+ pluginState.prepareLSPServerConnection {}
}
- return pliPluginState.loadLanguageServerDefinition(project)
+
+ return pluginState.getReadyLSPServerConnection() as StreamConnectionProvider
}
override fun createLanguageClient(project: Project): LanguageClientImpl {
- val pliPluginState = CobolPluginState.getPluginState(project)
- return pliPluginState.loadLanguageClientDefinition(project)
+ val lsStateService = LanguageSupportStateService.instance
+ val pluginState = lsStateService.getPluginState(project) { CobolPluginState(project) }
+
+ @OptIn(InitializationOnly::class)
+ if (!pluginState.isLSPClientReady()) {
+ pluginState.prepareLSPClient {}
+ pluginState.finishInitialization(COBOL_PLUGIN_NOTIFICATION_ID) {}
+ }
+
+ return pluginState.getReadyLSPClient() as LanguageClientImpl
}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/org/zowe/cobol/state/CobolPluginState.kt b/src/main/kotlin/org/zowe/cobol/state/CobolPluginState.kt
new file mode 100644
index 0000000..0827690
--- /dev/null
+++ b/src/main/kotlin/org/zowe/cobol/state/CobolPluginState.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (c) 2024 IBA Group.
+ *
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBA Group
+ * Zowe Community
+ */
+
+package org.zowe.cobol.state
+
+import com.intellij.notification.Notification
+import com.intellij.notification.NotificationType
+import com.intellij.notification.Notifications
+import com.intellij.openapi.application.PathManager
+import com.intellij.openapi.project.Project
+import com.intellij.util.io.ZipUtil
+import com.jetbrains.rd.util.firstOrNull
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.plugins.textmate.TextMateService
+import org.jetbrains.plugins.textmate.configuration.TextMateUserBundlesSettings
+import com.intellij.openapi.util.io.FileUtil
+import com.redhat.devtools.lsp4ij.client.LanguageClientImpl
+import com.redhat.devtools.lsp4ij.server.JavaProcessCommandBuilder
+import com.redhat.devtools.lsp4ij.server.ProcessStreamConnectionProvider
+import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider
+import kotlinx.coroutines.runBlocking
+import org.zowe.cobol.lsp.CobolLanguageClient
+import java.nio.file.Path
+import kotlin.io.path.exists
+import kotlin.io.path.pathString
+
+const val COBOL_PLUGIN_NOTIFICATION_ID = "org.zowe.cobol.CobolNotificationId"
+
+// https://github.com/eclipse-che4z/che-che4z-lsp-for-cobol
+private const val VSIX_NAME = "cobol-language-support"
+private const val VSIX_VERSION = "2.1.2"
+const val TEXTMATE_BUNDLE_NAME = "cobol"
+
+/**
+ * State of the COBOL plug-in. Provides initialization methods to set up all the things before the correct usage of
+ * the syntax highlighting and the LSP features
+ * @property project the project related to the plug-in's state
+ */
+@OptIn(InitializationOnly::class)
+class CobolPluginState(private val project: Project) : LanguageSupportState() {
+
+ private lateinit var vsixPlacingRootPath: Path
+ private lateinit var vsixUnpackedPath: Path
+ private lateinit var packageJsonPath: Path
+ private lateinit var lspServerPath: Path
+ private lateinit var lspServerConnection: StreamConnectionProvider
+ private lateinit var lspClient: LanguageClientImpl
+
+ override fun isLSPServerConnectionReady(): Boolean {
+ return ::lspServerConnection.isInitialized
+ }
+
+ override fun getReadyLSPServerConnection(): Any {
+ return if (isLSPServerConnectionReady()) lspServerConnection else throw IllegalStateException("LSP server connection is not ready")
+ }
+
+ override fun isLSPClientReady(): Boolean {
+ return ::lspClient.isInitialized
+ }
+
+ override fun getReadyLSPClient(): Any {
+ return if (isLSPClientReady()) lspClient else throw IllegalStateException("LSP client is not ready")
+ }
+
+ /**
+ * Compute all the paths needed for the plug-in's setup
+ * @return boolean that indicates if the paths are already exist
+ */
+ private fun computeVSIXPlacingPaths(): Boolean {
+ vsixPlacingRootPath = PathManager.getConfigDir().resolve(VSIX_NAME)
+ vsixUnpackedPath = vsixPlacingRootPath.resolve("extension")
+ packageJsonPath = vsixUnpackedPath.resolve("package.json")
+ lspServerPath = vsixUnpackedPath.resolve("server").resolve("jar").resolve("server.jar")
+ val syntaxesPath = vsixUnpackedPath.resolve("syntaxes")
+ return vsixUnpackedPath.exists() && packageJsonPath.exists() && lspServerPath.exists() && syntaxesPath.exists()
+ }
+
+ /** Unpack VSIX package and place it under temp directory */
+ private fun unpackVSIX() {
+ val activeClassLoader = this::class.java.classLoader
+ val vsixNameWithVersion = "$VSIX_NAME-$VSIX_VERSION"
+ val vsixWithExt = "$vsixNameWithVersion.vsix"
+ val vsixTempFile = FileUtil.createTempFile(VSIX_NAME, ".vsix")
+ val vsixResource = activeClassLoader
+ .getResourceAsStream(vsixWithExt)
+ ?: throw Exception("No $vsixWithExt found")
+ vsixTempFile.writeBytes(vsixResource.readAllBytes())
+ ZipUtil.extract(vsixTempFile.toPath(), vsixPlacingRootPath, null)
+ }
+
+ /**
+ * Unzip .vsix file in the 'resources' folder into the 'build' path,
+ * and later use the unzipped files to activate a TextMate bundle and an LSP server connection.
+ * If the paths of the unzipped .vsix are already exist, the processing is skipped
+ * @param prepFun the function for additional preparation steps after the VSIX package is prepared
+ * @see [LanguageSupportState.prepareVSIX]
+ */
+ @InitializationOnly
+ override fun prepareVSIX(prepFun: () -> Unit) {
+ super.prepareVSIX {
+ runBlocking {
+ withContext(Dispatchers.IO) {
+ val doPathsAlreadyExist = computeVSIXPlacingPaths()
+ if (!doPathsAlreadyExist) {
+ unpackVSIX()
+ }
+ }
+ }
+ prepFun()
+ }
+ }
+
+ /** Get instance of [JavaProcessCommandBuilder] for the project and language ID provided */
+ private fun getJavaProcessCommandBuilder(): JavaProcessCommandBuilder {
+ return JavaProcessCommandBuilder(project, "cobol")
+ }
+
+ /**
+ * Initialize language server definition. Will run the LSP server command
+ * @param prepFun the function for additional preparation steps after the LSP server connection instance is prepared
+ * @see [LanguageSupportState.prepareLSPServerConnection]
+ */
+ @InitializationOnly
+ override fun prepareLSPServerConnection(prepFun: () -> Unit) {
+ return super.prepareLSPServerConnection {
+ val lspRunCommands = getJavaProcessCommandBuilder()
+ .setJar(lspServerPath.pathString)
+ .create()
+ lspRunCommands.add("pipeEnabled")
+ lspServerConnection = object : ProcessStreamConnectionProvider(lspRunCommands) {}
+ prepFun()
+ }
+ }
+
+ /**
+ * Load a TextMate bundle from previously unzipped .vsix. The version of the bundle to activate is the same as the
+ * .vsix package has. If there is an already activated version of the bundle with the same name, it will be deleted
+ * if the version is less than the one it is trying to activate. If the versions are the same, or there are any
+ * troubles unzipping/using the provided bundle, the processing does not continue, and the bundle that is already
+ * loaded to the IDE stays there. As the finishing step, prepares the COBOL LSP client instance
+ * @param prepFun the function for additional preparation steps after the LSP client instance is prepared
+ * @see [LanguageSupportState.prepareLSPClient]
+ */
+ @InitializationOnly
+ override fun prepareLSPClient(prepFun: () -> Unit) {
+ super.prepareLSPClient {
+ val emptyBundleName = "$TEXTMATE_BUNDLE_NAME-0.0.0"
+ val newBundleName = "$TEXTMATE_BUNDLE_NAME-$VSIX_VERSION"
+ val textMateUserBundlesSettings = TextMateUserBundlesSettings.instance
+ if (textMateUserBundlesSettings != null) {
+ var existingBundles = textMateUserBundlesSettings.bundles
+ val existingBundle = existingBundles
+ .filter { it.value.name.contains(TEXTMATE_BUNDLE_NAME) }
+ .firstOrNull()
+ val existingBundleName = existingBundle?.value?.name ?: emptyBundleName
+ if (existingBundleName < newBundleName) {
+ existingBundles = existingBundles.filter { it.value.name != existingBundleName }
+ textMateUserBundlesSettings.setBundlesConfig(existingBundles)
+ textMateUserBundlesSettings.addBundle(vsixUnpackedPath.toString(), newBundleName)
+ TextMateService.getInstance().reloadEnabledBundles()
+ }
+ } else {
+ Notification(
+ COBOL_PLUGIN_NOTIFICATION_ID,
+ "TextMate bundle is not initialized",
+ "TextMate user settings is failed to load, thus it is not possible to initialize the COBOL TextMate bundle",
+ NotificationType.WARNING
+ ).let {
+ Notifications.Bus.notify(it)
+ }
+ }
+ lspClient = CobolLanguageClient(project)
+ prepFun()
+ }
+ }
+
+ /**
+ * Disable the COBOL plug-in TextMate bundle before the plug-in is unloaded
+ * @param unloadFun the function for additional unloading steps before the LSP client instance is unloaded
+ * @see [LanguageSupportState.unloadLSPClient]
+ */
+ @InitializationOnly
+ override fun unloadLSPClient(unloadFun: () -> Unit) {
+ super.unloadLSPClient {
+ unloadFun()
+ val textMateUserBundlesSettings = TextMateUserBundlesSettings.instance
+ if (textMateUserBundlesSettings != null) {
+ var existingBundles = textMateUserBundlesSettings.bundles
+ existingBundles = existingBundles.filter { it.value.name.contains(TEXTMATE_BUNDLE_NAME) }
+ textMateUserBundlesSettings.setBundlesConfig(existingBundles)
+ TextMateService.getInstance().reloadEnabledBundles()
+ } else {
+ Notification(
+ COBOL_PLUGIN_NOTIFICATION_ID,
+ "TextMate bundle is not uninitialized",
+ "TextMate user settings is failed to load, thus it is not possible to remove the COBOL TextMate bundle",
+ NotificationType.WARNING
+ ).let {
+ Notifications.Bus.notify(it)
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/main/kotlin/org/zowe/cobol/init/InitStates.kt b/src/main/kotlin/org/zowe/cobol/state/InitStates.kt
similarity index 53%
rename from src/main/kotlin/org/zowe/cobol/init/InitStates.kt
rename to src/main/kotlin/org/zowe/cobol/state/InitStates.kt
index 0094dcd..f088c7b 100644
--- a/src/main/kotlin/org/zowe/cobol/init/InitStates.kt
+++ b/src/main/kotlin/org/zowe/cobol/state/InitStates.kt
@@ -1,27 +1,29 @@
/*
+ * Copyright (c) 2024 IBA Group.
+ *
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
- * Copyright Contributors to the Zowe Project
+ * Contributors:
+ * IBA Group
+ * Zowe Community
*/
-package org.zowe.cobol.init
+package org.zowe.cobol.state
/** Initialization states enum class to represent available plug-in's states */
enum class InitStates {
DOWN,
- LSP_UNLOADED,
- LSP_UNLOAD_TRIGGERED,
- TEXTMATE_BUNDLE_UNLOADED,
- TEXTMATE_BUNDLE_UNLOAD_TRIGGERED,
- VSIX_UNPACK_TRIGGERED,
- VSIX_UNPACKED,
- TEXTMATE_BUNDLE_LOAD_TRIGGERED,
- TEXTMATE_BUNDLE_LOADED,
- LSP_LOAD_TRIGGERED,
- LSP_LOADED,
+ LSP_CLIENT_UNLOADED,
+ LSP_CLIENT_UNLOAD_TRIGGERED,
+ VSIX_PREPARE_TRIGGERED,
+ VSIX_PREPARED,
+ LSP_SERVER_CONNECTION_PREPARE_TRIGGERED,
+ LSP_SERVER_CONNECTION_PREPARED,
+ LSP_CLIENT_PREPARE_TRIGGERED,
+ LSP_CLIENT_PREPARED,
UP
}
diff --git a/src/main/kotlin/org/zowe/cobol/init/InitializationOnly.kt b/src/main/kotlin/org/zowe/cobol/state/InitializationOnly.kt
similarity index 84%
rename from src/main/kotlin/org/zowe/cobol/init/InitializationOnly.kt
rename to src/main/kotlin/org/zowe/cobol/state/InitializationOnly.kt
index 6874718..4b91ba9 100644
--- a/src/main/kotlin/org/zowe/cobol/init/InitializationOnly.kt
+++ b/src/main/kotlin/org/zowe/cobol/state/InitializationOnly.kt
@@ -1,14 +1,18 @@
/*
+ * Copyright (c) 2024 IBA Group.
+ *
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
- * Copyright Contributors to the Zowe Project
+ * Contributors:
+ * IBA Group
+ * Zowe Community
*/
-package org.zowe.cobol.init
+package org.zowe.cobol.state
/**
* Annotation for the restricted initialization methods.
diff --git a/src/main/kotlin/org/zowe/cobol/state/LanguageSupportState.kt b/src/main/kotlin/org/zowe/cobol/state/LanguageSupportState.kt
new file mode 100644
index 0000000..e3f1e45
--- /dev/null
+++ b/src/main/kotlin/org/zowe/cobol/state/LanguageSupportState.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2024 IBA Group.
+ *
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBA Group
+ * Zowe Community
+ */
+
+package org.zowe.cobol.state
+
+import com.intellij.notification.Notification
+import com.intellij.notification.NotificationType
+import com.intellij.notification.Notifications
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.util.Disposer
+
+/**
+ * Represents language support plug-in's state. Carries all the necessary instances
+ * to avoid double-initialization plus allows to initialize/de-initialize the plug-in's features.
+ * The main purpose is to provide a generic interface to handle the current state of the plug-in and manage
+ * it according to the previous and expected state
+ */
+abstract class LanguageSupportState : Disposable {
+
+ /** The current state of the plug-in for a specific project */
+ private var currState: InitStates = InitStates.DOWN
+
+ /** Check if the LSP server connection instance is ready */
+ abstract fun isLSPServerConnectionReady(): Boolean
+
+ /** Get the LSP server connection instance */
+ abstract fun getReadyLSPServerConnection(): Any
+
+ /** Check if the LSP client instance is ready */
+ abstract fun isLSPClientReady(): Boolean
+
+ /** Get the LSP client instance */
+ abstract fun getReadyLSPClient(): Any
+
+ /**
+ * Prepare VSIX package before all the other preparations
+ * @param prepFun the function to prepare the VSIX package
+ */
+ @InitializationOnly
+ open fun prepareVSIX(prepFun: () -> Unit) {
+ if (currState != InitStates.DOWN)
+ throw IllegalStateException("Invalid plug-in state. Expected: ${InitStates.DOWN}, current: $currState")
+ currState = InitStates.VSIX_PREPARE_TRIGGERED
+ prepFun()
+ currState = InitStates.VSIX_PREPARED
+ }
+
+ /**
+ * Prepare LSP server connection instance. Expects that the VSIX is prepared
+ * @param prepFun the function to prepare LSP server connection instance
+ */
+ @InitializationOnly
+ open fun prepareLSPServerConnection(prepFun: () -> Unit) {
+ if (currState < InitStates.VSIX_PREPARED)
+ throw IllegalStateException("Invalid plug-in state. Expected: at least ${InitStates.VSIX_PREPARED}, current: $currState")
+ currState = InitStates.LSP_SERVER_CONNECTION_PREPARE_TRIGGERED
+ prepFun()
+ currState = InitStates.LSP_SERVER_CONNECTION_PREPARED
+ }
+
+ /**
+ * Prepare LSP client instance. Expects that the VSIX is prepared
+ * @param prepFun the function to prepare LSP client instance
+ */
+ @InitializationOnly
+ open fun prepareLSPClient(prepFun: () -> Unit) {
+ if (currState < InitStates.VSIX_PREPARED)
+ throw IllegalStateException("Invalid plug-in state. Expected: at least ${InitStates.VSIX_PREPARED}, current: $currState")
+ currState = InitStates.LSP_CLIENT_PREPARE_TRIGGERED
+ prepFun()
+ currState = InitStates.LSP_CLIENT_PREPARED
+ }
+
+ /**
+ * Initialization final step, puts the plug-in in [InitStates.UP] state.
+ * Will throw an error when both LSP client and LSP server connection instances are not prepared,
+ * shows notification when an LSP client is prepared and LSP server connection is not.
+ * @param notificationId the notification group ID to show the notification
+ * @param finishFun the function to finish initialization
+ */
+ @InitializationOnly
+ open fun finishInitialization(notificationId: String, finishFun: () -> Unit) {
+ if (currState != InitStates.LSP_CLIENT_PREPARED) {
+ if (currState != InitStates.LSP_SERVER_CONNECTION_PREPARED)
+ throw IllegalStateException("Invalid plug-in state. Expected: at least ${InitStates.LSP_SERVER_CONNECTION_PREPARED}, current: $currState")
+ else
+ Notification(
+ notificationId,
+ "LSP client is not initialized",
+ "",
+ NotificationType.WARNING
+ ).let {
+ Notifications.Bus.notify(it)
+ }
+ }
+ finishFun()
+ currState = InitStates.UP
+ }
+
+ /**
+ * Unload LSP client. It is the starting point of the plug-in's shutdown
+ * @param unloadFun the function to perform unloading
+ */
+ @InitializationOnly
+ open fun unloadLSPClient(unloadFun: () -> Unit) {
+ if (currState != InitStates.UP)
+ throw IllegalStateException("Invalid plug-in state. Expected: ${InitStates.UP}, current: $currState")
+ currState = InitStates.LSP_CLIENT_UNLOAD_TRIGGERED
+ unloadFun()
+ currState = InitStates.LSP_CLIENT_UNLOADED
+ }
+
+ /**
+ * Deinitialization final step. Disposing purposes
+ * @param unloadFinishFun the function to perform final unloading processes
+ */
+ @InitializationOnly
+ open fun finishDeinitialization(unloadFinishFun: () -> Unit) {
+ if (currState > InitStates.LSP_CLIENT_UNLOADED)
+ throw IllegalStateException("Invalid plug-in state. Expected: at most ${InitStates.LSP_CLIENT_UNLOADED}, current: $currState")
+ unloadFinishFun()
+ this.dispose()
+ currState = InitStates.DOWN
+ }
+
+ @InitializationOnly
+ override fun dispose() {
+ Disposer.dispose(this)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/org/zowe/cobol/state/LanguageSupportStateService.kt b/src/main/kotlin/org/zowe/cobol/state/LanguageSupportStateService.kt
new file mode 100644
index 0000000..1af0a1e
--- /dev/null
+++ b/src/main/kotlin/org/zowe/cobol/state/LanguageSupportStateService.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2024 IBA Group.
+ *
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBA Group
+ * Zowe Community
+ */
+
+package org.zowe.cobol.state
+
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+
+/** Service to provide language support states storage */
+@Service
+class LanguageSupportStateService {
+
+ companion object {
+ @JvmStatic
+ val instance
+ get() = service()
+ }
+
+ private val projectToPluginState = mutableMapOf()
+
+ /**
+ * Get initialized plug-in state by the project. If there is no plugin state initialized for the project,
+ * the new state is initialized
+ * @param project the project to get or initialize the plug-in's state
+ * @param defaultStateProvider the function that initializes the [LanguageSupportState] if it is not yet exists
+ * @return initialized plug-in's state
+ */
+ fun getPluginState(project: Project, defaultStateProvider: () -> LanguageSupportState): LanguageSupportState {
+ return projectToPluginState.computeIfAbsent(project) {
+ defaultStateProvider()
+ }
+ }
+
+}
\ 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 a9a9630..8d0df3c 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -23,6 +23,11 @@
com.redhat.devtools.lsp4ij
org.jetbrains.plugins.textmate
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg
index ad741c9..f3aa8b6 100644
--- a/src/main/resources/META-INF/pluginIcon.svg
+++ b/src/main/resources/META-INF/pluginIcon.svg
@@ -1,10 +1,77 @@
-
\ No newline at end of file
+
+
+
diff --git a/src/test/kotlin/org/zowe/cobol/CobolProjectManagerListenerTestSpec.kt b/src/test/kotlin/org/zowe/cobol/CobolProjectManagerListenerTestSpec.kt
new file mode 100644
index 0000000..f13285f
--- /dev/null
+++ b/src/test/kotlin/org/zowe/cobol/CobolProjectManagerListenerTestSpec.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2024 IBA Group.
+ *
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBA Group
+ * Zowe Community
+ */
+
+package org.zowe.cobol
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.ProjectManager
+import io.kotest.assertions.assertSoftly
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+import io.mockk.*
+import org.zowe.cobol.state.InitializationOnly
+import org.zowe.cobol.state.LanguageSupportState
+import org.zowe.cobol.state.LanguageSupportStateService
+
+@OptIn(InitializationOnly::class)
+class CobolProjectManagerListenerTestSpec : FunSpec({
+
+ context("CobolProjectManagerListenerTestSpec.projectClosing") {
+ lateinit var cobolProjectManagerListener: CobolProjectManagerListener
+ lateinit var lsStateMock: LanguageSupportState
+
+ val projectMock = mockk()
+ var isUnloadLSPClientTriggered = false
+ var isFinishDeinitializationTriggered = false
+
+ beforeTest {
+ isUnloadLSPClientTriggered = false
+ isFinishDeinitializationTriggered = false
+
+ lsStateMock = mockk {
+ every { unloadLSPClient(any<() -> Unit>()) } answers {
+ firstArg<() -> Unit>().invoke()
+ isUnloadLSPClientTriggered = true
+ }
+ every { finishDeinitialization(any<() -> Unit>()) } answers {
+ firstArg<() -> Unit>().invoke()
+ isFinishDeinitializationTriggered = true
+ }
+ }
+
+ cobolProjectManagerListener = spyk(CobolProjectManagerListener())
+ }
+
+ afterTest {
+ unmockkAll()
+ clearAllMocks()
+ }
+
+ test("check that the plugin is fully unloaded when the last project is being closed") {
+ mockkStatic(ProjectManager::getInstance)
+ every { ProjectManager.getInstance() } returns mockk {
+ every { openProjects } returns arrayOf(projectMock)
+ }
+ every { lsStateMock.isLSPClientReady() } returns true
+
+ val lsStateServiceMock = mockk {
+ every { getPluginState(projectMock, any<() -> LanguageSupportState>()) } returns lsStateMock
+ }
+ mockkObject(LanguageSupportStateService)
+ every { LanguageSupportStateService.instance } returns lsStateServiceMock
+
+ cobolProjectManagerListener.projectClosing(projectMock)
+
+ assertSoftly { isUnloadLSPClientTriggered shouldBe true }
+ assertSoftly { isFinishDeinitializationTriggered shouldBe true }
+ }
+
+ test("check that the plugin is not unloaded when the project that is being closed is not the last one") {
+ mockkStatic(ProjectManager::getInstance)
+ every { ProjectManager.getInstance() } returns mockk {
+ every { openProjects } returns arrayOf(projectMock, mockk())
+ }
+
+ val lsStateServiceMock = mockk {
+ every { getPluginState(projectMock, any<() -> LanguageSupportState>()) } answers {
+ secondArg<() -> LanguageSupportState>().invoke()
+ }
+ }
+ mockkObject(LanguageSupportStateService)
+ every { LanguageSupportStateService.instance } returns lsStateServiceMock
+
+ cobolProjectManagerListener.projectClosing(projectMock)
+
+ assertSoftly { isUnloadLSPClientTriggered shouldBe false }
+ assertSoftly { isFinishDeinitializationTriggered shouldBe false }
+ }
+
+ test("check that the plugin is not unloaded when the last project is being closed but the LSP client and server are not initialized yet") {
+ mockkStatic(ProjectManager::getInstance)
+ every { ProjectManager.getInstance() } returns mockk {
+ every { openProjects } returns arrayOf(projectMock)
+ }
+ every { lsStateMock.isLSPClientReady() } returns false
+ every { lsStateMock.isLSPServerConnectionReady() } returns false
+
+ val lsStateServiceMock = mockk {
+ every { getPluginState(projectMock, any<() -> LanguageSupportState>()) } returns lsStateMock
+ }
+ mockkObject(LanguageSupportStateService)
+ every { LanguageSupportStateService.instance } returns lsStateServiceMock
+
+ cobolProjectManagerListener.projectClosing(projectMock)
+
+ assertSoftly { isUnloadLSPClientTriggered shouldBe false }
+ assertSoftly { isFinishDeinitializationTriggered shouldBe false }
+ }
+ }
+
+})
diff --git a/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientTestSpec.kt b/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientTestSpec.kt
new file mode 100644
index 0000000..2cfd147
--- /dev/null
+++ b/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientTestSpec.kt
@@ -0,0 +1,396 @@
+/*
+ * Copyright (c) 2024 IBA Group.
+ *
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBA Group
+ * Zowe Community
+ */
+
+package org.zowe.cobol.lsp
+
+import com.intellij.openapi.project.Project
+import io.kotest.assertions.assertSoftly
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.equals.shouldBeEqual
+import io.kotest.matchers.shouldBe
+import io.mockk.*
+import org.eclipse.lsp4j.ConfigurationItem
+import org.eclipse.lsp4j.ConfigurationParams
+import org.eclipse.lsp4j.MessageParams
+
+class CobolLanguageClientTestSpec : FunSpec({
+
+ context("CobolLanguageClientTestSpec.configuration") {
+ afterTest {
+ unmockkAll()
+ clearAllMocks()
+ }
+
+ test("process 'workspace/configuration' for dialect registry section request") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { section } returns DIALECT_REGISTRY_SECTION
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(DIALECT_REGISTRY_SECTION)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf(emptyList()) }
+ }
+
+ test("process 'workspace/configuration' for unrecognized request") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val someTestSection = "test-unrecognized-section"
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { section } returns someTestSection
+ every { scopeUri } returns ""
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(someTestSection)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf(emptyList()) }
+ }
+
+ test("process 'workspace/configuration' for dialects request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns SETTINGS_DIALECT
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(SETTINGS_DIALECT)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf(emptyList()) }
+ }
+
+ test("process 'workspace/configuration' for cpy-manager paths-local request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns SETTINGS_CPY_LOCAL_PATH
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(SETTINGS_CPY_LOCAL_PATH)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf() }
+ }
+
+ test("process 'workspace/configuration' for dialect libs request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns DIALECT_LIBS
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(DIALECT_LIBS)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf() }
+ }
+
+ test("process 'workspace/configuration' for cpy-manager copybook-extensions request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns SETTINGS_CPY_EXTENSIONS
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(SETTINGS_CPY_EXTENSIONS)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf(listOf(".CPY", ".COPY", ".cpy", ".copy", "")) }
+ }
+
+ test("process 'workspace/configuration' for target-sql-backend request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns SETTINGS_SQL_BACKEND
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(SETTINGS_SQL_BACKEND)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf("DB2_SERVER") }
+ }
+
+ test("process 'workspace/configuration' for cpy-manager copybook-file-encoding request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns SETTINGS_CPY_FILE_ENCODING
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(SETTINGS_CPY_FILE_ENCODING)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf() }
+ }
+
+ test("process 'workspace/configuration' for cobol-lsp compiler options request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns SETTINGS_COMPILE_OPTIONS
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(SETTINGS_COMPILE_OPTIONS)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf(null) }
+ }
+
+ test("process 'workspace/configuration' for cobol-lsp logging level root request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns SETTINGS_CLIENT_LOGGING_LEVEL
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(SETTINGS_CLIENT_LOGGING_LEVEL)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf("ERROR") }
+ }
+
+ test("process 'workspace/configuration' for cobol-lsp locale request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns SETTINGS_LOCALE
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(SETTINGS_LOCALE)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf("en") }
+ }
+
+ test("process 'workspace/configuration' for cobol-lsp cobol program layout request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns SETTINGS_COBOL_PROGRAM_LAYOUT
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(SETTINGS_COBOL_PROGRAM_LAYOUT)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf(null) }
+ }
+
+ test("process 'workspace/configuration' for cobol-lsp subroutine-manager paths-local request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns SETTINGS_SUBROUTINE_LOCAL_PATH
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(SETTINGS_SUBROUTINE_LOCAL_PATH)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf(emptyList()) }
+ }
+
+ test("process 'workspace/configuration' for cobol-lsp cics translator request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns SETTINGS_CICS_TRANSLATOR
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(SETTINGS_CICS_TRANSLATOR)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf("true") }
+ }
+ test("process 'workspace/configuration' for cobol-lsp unrecognized request with scope URI") {
+ var isLogMessageTriggeredCorrectly = false
+
+ val someTestSection = "test-unrecognized-section"
+ val projectMock = mockk()
+ val configurationItemMock = mockk {
+ every { scopeUri } returns "test"
+ every { section } returns someTestSection
+ }
+ val configurationParamsMock = mockk {
+ every { items } returns listOf(configurationItemMock)
+ }
+ val cobolLanguageClient = spyk(CobolLanguageClient(projectMock))
+ every { cobolLanguageClient.logMessage(any()) } answers {
+ if (firstArg().message.contains(someTestSection)) {
+ isLogMessageTriggeredCorrectly = true
+ }
+ }
+
+ val result = cobolLanguageClient.configuration(configurationParamsMock).join()
+
+ assertSoftly { isLogMessageTriggeredCorrectly shouldBe true }
+ assertSoftly { result shouldBeEqual listOf() }
+ }
+ }
+
+})
diff --git a/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactoryTestSpec.kt b/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactoryTestSpec.kt
new file mode 100644
index 0000000..1cfaf0e
--- /dev/null
+++ b/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactoryTestSpec.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (c) 2024 IBA Group.
+ *
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBA Group
+ * Zowe Community
+ */
+
+package org.zowe.cobol.lsp
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.ProjectManager
+import com.redhat.devtools.lsp4ij.client.LanguageClientImpl
+import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider
+import io.kotest.assertions.assertSoftly
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+import io.mockk.*
+import org.zowe.cobol.setPrivateFieldValue
+import org.zowe.cobol.state.*
+
+@OptIn(InitializationOnly::class)
+class CobolLanguageServerFactoryTestSpec : FunSpec({
+
+ context("CobolLanguageServerFactoryTestSpec.createConnectionProvider") {
+ afterTest {
+ unmockkAll()
+ clearAllMocks()
+ }
+
+ test("create a connection provider, initializing all the elements") {
+ var isPrepareVSIXTriggered = false
+ var isPrepareLSPServerConnectionTriggered = false
+
+ val projectMock = mockk()
+ val createdConnectionProvider = mockk()
+
+ val lsStateMock = spyk(CobolPluginState(projectMock))
+ every { lsStateMock.prepareVSIX(any<() -> Unit>()) } answers {
+ isPrepareVSIXTriggered = true
+ firstArg<() -> Unit>().invoke()
+ }
+ every { lsStateMock.prepareLSPServerConnection(any<() -> Unit>()) } answers {
+ isPrepareLSPServerConnectionTriggered = true
+ setPrivateFieldValue(
+ lsStateMock,
+ CobolPluginState::class.java,
+ "lspServerConnection",
+ createdConnectionProvider
+ )
+ firstArg<() -> Unit>().invoke()
+ }
+ val lsStateService = mockk {
+ every { getPluginState(projectMock, any<() -> LanguageSupportState>()) } returns lsStateMock
+ }
+ mockkObject(LanguageSupportStateService)
+ every { LanguageSupportStateService.instance } returns lsStateService
+
+ val cobolLanguageServerFactory = spyk(CobolLanguageServerFactory())
+
+ val result = cobolLanguageServerFactory.createConnectionProvider(projectMock)
+
+ assertSoftly { isPrepareVSIXTriggered shouldBe true }
+ assertSoftly { isPrepareLSPServerConnectionTriggered shouldBe true }
+ assertSoftly { result shouldBe createdConnectionProvider }
+ }
+
+ test("get a connection provider, that was already initialized") {
+ lateinit var defaultLSState: LanguageSupportState
+ var isPrepareVSIXTriggered = false
+ var isPrepareLSPServerConnectionTriggered = false
+
+ val projectMock = mockk()
+ val createdConnectionProvider = mockk()
+
+ val lsStateMock = spyk(CobolPluginState(projectMock))
+ every { lsStateMock.prepareVSIX(any<() -> Unit>()) } answers {
+ isPrepareVSIXTriggered = true
+ }
+ every { lsStateMock.prepareLSPServerConnection(any<() -> Unit>()) } answers {
+ isPrepareLSPServerConnectionTriggered = true
+ }
+ setPrivateFieldValue(
+ lsStateMock,
+ CobolPluginState::class.java,
+ "lspServerConnection",
+ createdConnectionProvider
+ )
+ val lsStateService = mockk {
+ every { getPluginState(projectMock, any<() -> LanguageSupportState>()) } answers {
+ defaultLSState = secondArg<() -> LanguageSupportState>().invoke()
+ lsStateMock
+ }
+ }
+ mockkObject(LanguageSupportStateService)
+ every { LanguageSupportStateService.instance } returns lsStateService
+
+ val cobolLanguageServerFactory = spyk(CobolLanguageServerFactory())
+
+ val result = cobolLanguageServerFactory.createConnectionProvider(projectMock)
+
+ assertSoftly { defaultLSState is CobolPluginState }
+ assertSoftly { isPrepareVSIXTriggered shouldBe false }
+ assertSoftly { isPrepareLSPServerConnectionTriggered shouldBe false }
+ assertSoftly { result shouldBe createdConnectionProvider }
+ }
+ }
+
+ context("CobolLanguageServerFactoryTestSpec.createLanguageClient") {
+ afterTest {
+ unmockkAll()
+ clearAllMocks()
+ }
+
+ test("create a language client, initializing all the elements") {
+ var isPrepareLSPClientTriggered = false
+ var isFinishInitializationTriggered = false
+
+ val projectMock = mockk()
+ val createdLanguageClient = mockk()
+
+ val lsStateMock = spyk(CobolPluginState(projectMock))
+ every { lsStateMock.prepareLSPClient(any<() -> Unit>()) } answers {
+ isPrepareLSPClientTriggered = true
+ firstArg<() -> Unit>().invoke()
+ }
+ every { lsStateMock.finishInitialization(any(), any<() -> Unit>()) } answers {
+ isFinishInitializationTriggered = true
+ setPrivateFieldValue(
+ lsStateMock,
+ CobolPluginState::class.java,
+ "lspClient",
+ createdLanguageClient
+ )
+ secondArg<() -> Unit>().invoke()
+ }
+ val lsStateService = mockk {
+ every { getPluginState(projectMock, any<() -> LanguageSupportState>()) } returns lsStateMock
+ }
+ mockkObject(LanguageSupportStateService)
+ every { LanguageSupportStateService.instance } returns lsStateService
+
+ val cobolLanguageServerFactory = spyk(CobolLanguageServerFactory())
+
+ val result = cobolLanguageServerFactory.createLanguageClient(projectMock)
+
+ assertSoftly { isPrepareLSPClientTriggered shouldBe true }
+ assertSoftly { isFinishInitializationTriggered shouldBe true }
+ assertSoftly { result shouldBe createdLanguageClient }
+ }
+
+ test("get a language client, that was already initialized") {
+ lateinit var defaultLSState: LanguageSupportState
+ var isPrepareLSPClientTriggered = false
+ var isFinishInitializationTriggered = false
+
+ val projectMock = mockk()
+ val createdLanguageClient = mockk()
+
+ val lsStateMock = spyk(CobolPluginState(projectMock))
+ every { lsStateMock.prepareLSPClient(any<() -> Unit>()) } answers {
+ isPrepareLSPClientTriggered = true
+ }
+ every { lsStateMock.finishInitialization(any(), any<() -> Unit>()) } answers {
+ isFinishInitializationTriggered = true
+ }
+ setPrivateFieldValue(
+ lsStateMock,
+ CobolPluginState::class.java,
+ "lspClient",
+ createdLanguageClient
+ )
+ val lsStateService = mockk {
+ every { getPluginState(projectMock, any<() -> LanguageSupportState>()) } answers {
+ defaultLSState = secondArg<() -> LanguageSupportState>().invoke()
+ lsStateMock
+ }
+ }
+ mockkObject(LanguageSupportStateService)
+ every { LanguageSupportStateService.instance } returns lsStateService
+
+ val cobolLanguageServerFactory = spyk(CobolLanguageServerFactory())
+
+ val result = cobolLanguageServerFactory.createLanguageClient(projectMock)
+
+ assertSoftly { defaultLSState is CobolPluginState }
+ assertSoftly { isPrepareLSPClientTriggered shouldBe false }
+ assertSoftly { isFinishInitializationTriggered shouldBe false }
+ assertSoftly { result shouldBe createdLanguageClient }
+ }
+ }
+
+})
diff --git a/src/test/kotlin/org/zowe/cobol/state/CobolPluginStateTestSpec.kt b/src/test/kotlin/org/zowe/cobol/state/CobolPluginStateTestSpec.kt
new file mode 100644
index 0000000..e24f1bb
--- /dev/null
+++ b/src/test/kotlin/org/zowe/cobol/state/CobolPluginStateTestSpec.kt
@@ -0,0 +1,573 @@
+/*
+ * Copyright (c) 2024 IBA Group.
+ *
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBA Group
+ * Zowe Community
+ */
+
+package org.zowe.cobol.state
+
+import com.intellij.notification.Notification
+import com.intellij.notification.Notifications
+import com.intellij.openapi.project.Project
+import com.redhat.devtools.lsp4ij.server.JavaProcessCommandBuilder
+import io.kotest.assertions.assertSoftly
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.string.shouldContain
+import io.mockk.*
+import org.jetbrains.plugins.textmate.TextMateService
+import org.jetbrains.plugins.textmate.configuration.TextMatePersistentBundle
+import org.jetbrains.plugins.textmate.configuration.TextMateUserBundlesSettings
+import org.junit.jupiter.api.assertThrows
+import org.zowe.cobol.getPrivateFieldValue
+import org.zowe.cobol.setPrivateFieldValue
+import java.nio.file.Path
+import kotlin.io.path.pathString
+import kotlin.reflect.KFunction
+
+@OptIn(InitializationOnly::class)
+class CobolPluginStateTestSpec : FunSpec({
+
+ context("CobolPluginStateTestSpec: lateinit vars") {
+ val cobolState = spyk(CobolPluginState(mockk()))
+
+ test("make a try to get not yet initialized lateinit var") {
+ val exception = assertThrows { cobolState.getReadyLSPServerConnection() }
+ assertSoftly { exception.message shouldContain "LSP server connection is not ready" }
+ }
+
+ test("make a try to get not yet initialized lateinit var") {
+ val exception = assertThrows { cobolState.getReadyLSPClient() }
+ assertSoftly { exception.message shouldContain "LSP client is not ready" }
+ }
+ }
+
+ context("CobolPluginStateTestSpec.prepareVSIX") {
+ lateinit var projectMock: Project
+ lateinit var cobolState: CobolPluginState
+
+ var isFinalPrepFunctionCalled = false
+ var isUnpackVSIXCalled = false
+
+ beforeTest {
+ projectMock = mockk()
+ cobolState = spyk(CobolPluginState(projectMock), recordPrivateCalls = true)
+
+ isFinalPrepFunctionCalled = false
+ isUnpackVSIXCalled = false
+
+ every {
+ cobolState["unpackVSIX"]()
+ } answers {
+ isUnpackVSIXCalled = true
+ Unit
+ }
+ }
+
+ afterTest {
+ unmockkAll()
+ clearAllMocks()
+ }
+
+ test("the state should change itself respectively after the VSIX unpacking process is finished") {
+ every { cobolState["computeVSIXPlacingPaths"]() } returns false
+
+ cobolState.prepareVSIX { isFinalPrepFunctionCalled = true }
+
+ val currState = getPrivateFieldValue(
+ cobolState,
+ LanguageSupportState::class.java,
+ "currState"
+ ) as InitStates
+ assertSoftly { isFinalPrepFunctionCalled shouldBe true }
+ assertSoftly { isUnpackVSIXCalled shouldBe true }
+ assertSoftly { currState shouldBe InitStates.VSIX_PREPARED }
+ }
+
+ test("the state should change itself respectively without additional steps") {
+ every { cobolState["computeVSIXPlacingPaths"]() } returns true
+
+ cobolState.prepareVSIX { isFinalPrepFunctionCalled = true }
+
+ val currState = getPrivateFieldValue(
+ cobolState,
+ LanguageSupportState::class.java,
+ "currState"
+ ) as InitStates
+ assertSoftly { isFinalPrepFunctionCalled shouldBe true }
+ assertSoftly { isUnpackVSIXCalled shouldBe false }
+ assertSoftly { currState shouldBe InitStates.VSIX_PREPARED}
+ }
+
+ test("the state should throw error cause the state before VSIX unpack is not correct") {
+ setPrivateFieldValue(
+ cobolState,
+ LanguageSupportState::class.java,
+ "currState",
+ InitStates.VSIX_PREPARED
+ )
+ val exception = assertThrows { cobolState.prepareVSIX { isFinalPrepFunctionCalled = true } }
+ assertSoftly { exception.message shouldContain "Invalid plug-in state" }
+ assertSoftly { isUnpackVSIXCalled shouldBe false }
+ }
+ }
+
+ context("CobolPluginStateTestSpec.prepareLSPServerConnection") {
+ lateinit var projectMock: Project
+ lateinit var cobolState: CobolPluginState
+
+ var isFinalPrepFunctionCalled = false
+
+ beforeTest {
+ projectMock = mockk()
+ cobolState = spyk(CobolPluginState(projectMock), recordPrivateCalls = true)
+
+ isFinalPrepFunctionCalled = false
+ }
+
+ afterTest {
+ unmockkAll()
+ clearAllMocks()
+ }
+
+ test("the state should change itself respectively after the LSP server connection instance preparation is finished") {
+ val commandsListMock = mockk>()
+ every { commandsListMock.add(any()) } returns true
+
+ val javaProcessCommandBuilderMock = mockk()
+ every { javaProcessCommandBuilderMock.setJar(any()) } returns javaProcessCommandBuilderMock
+ every { javaProcessCommandBuilderMock.create() } returns commandsListMock
+
+ every { cobolState["getJavaProcessCommandBuilder"]() } returns javaProcessCommandBuilderMock
+
+ setPrivateFieldValue(
+ cobolState,
+ LanguageSupportState::class.java,
+ "currState",
+ InitStates.VSIX_PREPARED
+ )
+
+ val lspServerPathMock = mockk()
+ every { lspServerPathMock.pathString } returns ""
+
+ setPrivateFieldValue(
+ cobolState,
+ CobolPluginState::class.java,
+ "lspServerPath",
+ lspServerPathMock
+ )
+
+ cobolState.prepareLSPServerConnection { isFinalPrepFunctionCalled = true }
+ val currState = getPrivateFieldValue(
+ cobolState,
+ LanguageSupportState::class.java,
+ "currState"
+ ) as InitStates
+ assertSoftly { isFinalPrepFunctionCalled shouldBe true }
+ assertSoftly { currState shouldBe InitStates.LSP_SERVER_CONNECTION_PREPARED }
+ }
+
+ test("the state should throw error cause the state before LSP server connection preparation is not correct") {
+ val exception = assertThrows { cobolState.prepareLSPServerConnection { isFinalPrepFunctionCalled = true } }
+ assertSoftly { exception.message shouldContain "Invalid plug-in state" }
+ }
+ }
+
+ context("CobolPluginStateTestSpec.prepareLSPClient") {
+ lateinit var projectMock: Project
+ lateinit var cobolState: CobolPluginState
+
+ var isFinalPrepFunctionCalled = false
+
+ beforeTest {
+ projectMock = mockk()
+ cobolState = spyk(CobolPluginState(projectMock), recordPrivateCalls = true)
+
+ isFinalPrepFunctionCalled = false
+ }
+
+ afterTest {
+ unmockkAll()
+ clearAllMocks()
+ }
+
+ test("the state should change itself respectively after the LSP client instance preparation is finished and a new bundle added") {
+ var isSetBundlesConfigTriggered = false
+ var isAddNewBundleTriggered = false
+ var isReloadEnabledBundlesTriggered = false
+
+ setPrivateFieldValue(
+ cobolState,
+ LanguageSupportState::class.java,
+ "currState",
+ InitStates.LSP_SERVER_CONNECTION_PREPARED
+ )
+
+ setPrivateFieldValue(
+ cobolState,
+ CobolPluginState::class.java,
+ "vsixUnpackedPath",
+ mockk()
+ )
+
+ val textMateBundle = mockk()
+ every { textMateBundle.name } returns "testBundle"
+
+ mockkObject(TextMateUserBundlesSettings)
+ every { TextMateUserBundlesSettings.instance } returns mockk {
+ every { bundles } returns mapOf(textMateBundle.name to textMateBundle)
+ every { setBundlesConfig(any