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..6efc350 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -32,7 +32,7 @@ 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"))
}
tasks {
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..f54ddf6
--- /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 = service()
+ val pluginState = lsStateService.getPluginState(project) { CobolPluginState(project) }
+
+ if (isLastProjectClosing()) {
+ 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..75f8437 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
diff --git a/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactory.kt b/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactory.kt
index ff3907e..b8e8095 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 = service()
+ 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 = service()
+ 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..b2d8ea6
--- /dev/null
+++ b/src/main/kotlin/org/zowe/cobol/state/CobolPluginState.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.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"
+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
+ * @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()
+ }
+
+ /**
+ * 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) {
+ 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)
+ }
+ }
+ }
+ prepFun()
+ }
+ }
+
+ /**
+ * 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 = JavaProcessCommandBuilder(project, "cobol")
+ .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"
+ 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()
+ }
+ 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()
+ var existingBundles = TextMateUserBundlesSettings.instance?.bundles
+ existingBundles = existingBundles?.filter { it.value.name.contains(TEXTMATE_BUNDLE_NAME) } ?: emptyMap()
+ TextMateUserBundlesSettings.instance?.setBundlesConfig(existingBundles)
+ TextMateService.getInstance().reloadEnabledBundles()
+ }
+ }
+
+}
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..1df463f
--- /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,
+ "TextMate bundle 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..63c36fa
--- /dev/null
+++ b/src/main/kotlin/org/zowe/cobol/state/LanguageSupportStateService.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.project.Project
+
+/** Service to provide language support states storage */
+@Service
+class LanguageSupportStateService {
+
+ 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.lsp4ijorg.jetbrains.plugins.textmate
+
+
+
+
+
+
+
+