From e7cf814dc53dae561552891e25561b5cebeb3885 Mon Sep 17 00:00:00 2001 From: Uladzislau Date: Thu, 27 Jun 2024 14:20:42 +0200 Subject: [PATCH] GH-1 Plug-in uninstallation is corrected, LanguageSupportState added to manage the plug-in's state, some reworks for the plug-in's state management Signed-off-by: Uladzislau --- .idea/LanguageServersSettings.xml | 18 ++ .idea/copyright/profiles_settings.xml | 7 + .idea/copyright/zowe_ijmp.xml | 6 + build.gradle.kts | 2 +- .../zowe/cobol/CobolProjectManagerListener.kt | 48 ++++ .../org/zowe/cobol/init/CobolPluginState.kt | 238 ------------------ .../org/zowe/cobol/lsp/CobolLanguageClient.kt | 6 +- .../cobol/lsp/CobolLanguageServerFactory.kt | 44 +++- .../org/zowe/cobol/state/CobolPluginState.kt | 180 +++++++++++++ .../zowe/cobol/{init => state}/InitStates.kt | 26 +- .../{init => state}/InitializationOnly.kt | 8 +- .../zowe/cobol/state/LanguageSupportState.kt | 141 +++++++++++ .../state/LanguageSupportStateService.kt | 39 +++ src/main/resources/META-INF/plugin.xml | 10 + 14 files changed, 506 insertions(+), 267 deletions(-) create mode 100644 .idea/LanguageServersSettings.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/copyright/zowe_ijmp.xml create mode 100644 src/main/kotlin/org/zowe/cobol/CobolProjectManagerListener.kt delete mode 100644 src/main/kotlin/org/zowe/cobol/init/CobolPluginState.kt create mode 100644 src/main/kotlin/org/zowe/cobol/state/CobolPluginState.kt rename src/main/kotlin/org/zowe/cobol/{init => state}/InitStates.kt (53%) rename src/main/kotlin/org/zowe/cobol/{init => state}/InitializationOnly.kt (84%) create mode 100644 src/main/kotlin/org/zowe/cobol/state/LanguageSupportState.kt create mode 100644 src/main/kotlin/org/zowe/cobol/state/LanguageSupportStateService.kt 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.lsp4ij org.jetbrains.plugins.textmate + + + + + + + +