diff --git a/.gitignore b/.gitignore index b63da45..cc5d98b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ build/ out/ !**/src/main/**/out/ !**/src/test/**/out/ +.intellijPlatform ### Eclipse ### .apt_generated @@ -39,4 +40,4 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index ae3f30a..d310da5 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - - \ No newline at end of file + diff --git a/.idea/misc.xml b/.idea/misc.xml index d450a79..bce7180 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,10 @@ + + + + + + diff --git a/README.md b/README.md index df10c53..dc26f47 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,43 @@ Provides: - syntax highlighting support using TextMate bundle from [eclipse-che4z/che-che4z-lsp-for-cobol](https://github.com/eclipse-che4z/che-che4z-lsp-for-cobol) - code actions using LSP technology with client from [redhat-developer/lsp4ij](https://github.com/redhat-developer/lsp4ij) and server from [eclipse-che4z/che-che4z-lsp-for-cobol](https://github.com/eclipse-che4z/che-che4z-lsp-for-cobol) +## Supported features + +### Syntax Highlighting and Coloring + +The feature allows to inspect COBOL sources with highlighted instructions, recognized as COBOL language elements, colored respective to their type. + +### Syntax and Semantic Code Check + +The plug-in walks through the content of COBOL source files and checks it for mistakes and errors, highlighting it respectively with suggestions on how to fix them. + +### Code Autocompletion + +The feature provides a functionality to autocomplete instructions, suggesting the possible options to complete the words being typed (works for .cob/.cbl files, not for copybooks). + +### Copybooks Recognition + +The plug-in recognizes local copybooks, used in COBOL sources. The .cpy/.copy files content is highlighted as COBOL source code. +To use local copybooks: +1. Create **.vscode** folder in your opened workspace +2. Create **settings.json** in the **.vscode** folder +3. Enter relative or absolute paths of the folders, where copybooks are placed + +Example of the **settings.json** content: +```json5 +{ + //... + "cobol-lsp.cpy-manager.paths-local": [ + ".copybooks/zowe-profile-1/DATA.SET.PATH1", + ".copybooks/some-path", + "/some/absolute/path" + ], + //... +} +``` + +The plug-in will search through the paths to find related copybook files. Using "Go To Definition" functionality will open the found copybook. + ## Prerequisites - Java v17 diff --git a/build.gradle.kts b/build.gradle.kts index 7169a61..8c16fdd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,71 +12,89 @@ * Zowe Community */ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + +//!IMPORTANT!: to refer "libs", use ./gradle/libs.versions.toml fun properties(key: String) = providers.gradleProperty(key) 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" + alias(libs.plugins.gradle) // IntelliJ Platform Gradle Plugin + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.kover) + java } group = properties("pluginGroup").get() version = properties("pluginVersion").get() -val kotestVersion = "5.9.1" -val mockkVersion = "1.13.12" -val junitVersion = "1.10.3" +val lsp4ijVersion = "0.7.0" repositories { mavenCentral() + intellijPlatform { + defaultRepositories() + jetbrainsRuntime() + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(JavaVersion.VERSION_17.toString())) + } } // Configure Gradle IntelliJ Plugin -// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html -intellij { - version.set(properties("platformVersion").get()) -// pluginsRepositories { -// custom("https://plugins.jetbrains.com/plugins/nightly/23257") -// } - plugins.set(listOf("org.jetbrains.plugins.textmate", "com.redhat.devtools.lsp4ij:0.4.0")) +// Read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html +intellijPlatform { + pluginConfiguration { + version = properties("platformVersion").get() + ideaVersion { + sinceBuild = properties("pluginSinceBuild").get() + untilBuild = provider { null } + } + } } dependencies { + // ===== Runtime env setup === + // IntelliJ + intellijPlatform { + intellijIdeaCommunity(properties("platformVersion").get(), useInstaller = false) + jetbrainsRuntime() + instrumentationTools() + bundledPlugin("org.jetbrains.plugins.textmate") +//// pluginsRepositories { +//// custom("https://plugins.jetbrains.com/plugins/nightly/23257") +//// } + plugin("com.redhat.devtools.lsp4ij:$lsp4ijVersion") + testFramework(TestFrameworkType.Plugin.Java) + } + // Gson + implementation(libs.gson) // ===== Test env setup ===== // Kotest - testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") - testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotest.assertions.core) // MockK - testImplementation("io.mockk:mockk:$mockkVersion") + testImplementation(libs.mockk) // JUnit Platform (needed for Kotest) - testImplementation("org.junit.platform:junit-platform-launcher:$junitVersion") + testImplementation(libs.junit.platform.launcher) // ========================== } tasks { - // Set the JVM compatibility versions - withType { - sourceCompatibility = JavaVersion.VERSION_17.toString() - targetCompatibility = JavaVersion.VERSION_17.toString() - } - withType { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() - } - - patchPluginXml { - version.set(properties("pluginVersion").get()) - sinceBuild.set(properties("pluginSinceBuild").get()) - untilBuild.set(properties("pluginUntilBuild").get()) - } - test { useJUnitPlatform() testLogging { events("passed", "skipped", "failed") } finalizedBy("koverHtmlReport") +// testLogging.showStandardStreams = true } } diff --git a/gradle.properties b/gradle.properties index a1307ba..a2206b0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,3 +17,4 @@ pluginGroup = org.zowe # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 232 pluginUntilBuild = + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..14a9d35 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,26 @@ +[versions] +# libraries +gson = "2.11.0" +kotest = "5.9.1" +mockk = "1.13.13" +junit = "1.11.3" + +# plugins +gradle = "2.1.0" +kotlinJvm = "1.9.22" +kover = "0.8.3" + +[libraries] +# build deps +gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } + +# test deps +kotest-runner-junit5 = { group = "io.kotest", name = "kotest-runner-junit5", version.ref = "kotest" } +kotest-assertions-core = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotest" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version = "junit" } + +[plugins] +gradle = { id = "org.jetbrains.intellij.platform", version.ref = "gradle" } +kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlinJvm" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e583..d64cd49 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a595206..79eb9d0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd3..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/kotlin/org/zowe/cobol/CobolProjectManagerListener.kt b/src/main/kotlin/org/zowe/cobol/CobolProjectManagerListener.kt index f8ffba1..1a1c459 100644 --- a/src/main/kotlin/org/zowe/cobol/CobolProjectManagerListener.kt +++ b/src/main/kotlin/org/zowe/cobol/CobolProjectManagerListener.kt @@ -31,7 +31,7 @@ class CobolProjectManagerListener : ProjectManagerListener { */ @OptIn(InitializationOnly::class) override fun projectClosing(project: Project) { - val lsStateService = LanguageSupportStateService.instance + val lsStateService = LanguageSupportStateService.getService() val pluginState = lsStateService.getPluginState(project) { CobolPluginState(project) } if (isLastProjectClosing() && (pluginState.isLSPClientReady() || pluginState.isLSPServerConnectionReady())) { diff --git a/src/main/kotlin/org/zowe/cobol/Sections.kt b/src/main/kotlin/org/zowe/cobol/Sections.kt new file mode 100644 index 0000000..6de39a0 --- /dev/null +++ b/src/main/kotlin/org/zowe/cobol/Sections.kt @@ -0,0 +1,47 @@ +/* + * 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 + +/** + * Various config section options that could be found across configuration files + * @property section the section string representation + */ +enum class Sections(val section: String) { + DIALECTS_SECTION("cobol-lsp.dialects"), + DIALECT_REGISTRY("cobol-lsp.dialect.registry"), + DIALECT_LIBS("cobol-lsp.dialect.libs"), + CPY_SECTION("cobol-lsp.cpy-manager"), + CPY_LOCAL_PATH("cobol-lsp.cpy-manager.paths-local"), + CPY_EXTENSIONS("cobol-lsp.cpy-manager.copybook-extensions"), + CPY_FILE_ENCODING("cobol-lsp.cpy-manager.copybook-file-encoding"), + SQL_BACKEND("cobol-lsp.target-sql-backend"), + COMPILER_OPTIONS("cobol-lsp.compiler.options"), + LOGGIN_LEVEL_ROOT("cobol-lsp.logging.level.root"), + LOCALE("cobol-lsp.locale"), + COBOL_PROGRAM_LAYOUT("cobol-lsp.cobol.program.layout"), + SUBROUTINE_LOCAL_PATH("cobol-lsp.subroutine-manager.paths-local"), + CICS_TRANSLATOR("cobol-lsp.cics.translator"), + UNRECOGNIZED("unrecognized-section"); + + companion object { + operator fun invoke(section: String): Sections { + return entries.find { it.section == section } ?: UNRECOGNIZED + } + } + + override fun toString(): String { + return section + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/zowe/cobol/VSCodeSettingsAdapterService.kt b/src/main/kotlin/org/zowe/cobol/VSCodeSettingsAdapterService.kt new file mode 100644 index 0000000..00ca16a --- /dev/null +++ b/src/main/kotlin/org/zowe/cobol/VSCodeSettingsAdapterService.kt @@ -0,0 +1,70 @@ +/* + * 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.google.gson.JsonParser +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import java.io.File +import java.io.FileNotFoundException +import java.io.FileReader +import java.nio.file.Path + +/** VS Code settings adapter service. Needed to recognize configs in .vscode folder */ +@Service +class VSCodeSettingsAdapterService { + + companion object { + fun getService(): VSCodeSettingsAdapterService = service() + } + + /** + * Read the .vscode/settings.json file section + * @param workspaceFolder the workspace folder path to search for the file in + * @param readBlock the function to handle the read section + * @return the result of the read section handling or error if file is not found + */ + private fun readConfigFileSection(workspaceFolder: Path, readBlock: (FileReader) -> T?): T? { + return try { + val settingsPath = workspaceFolder.resolve(".vscode").resolve("settings.json") + val settingsFile = File(settingsPath.toUri()) + FileReader(settingsFile).use(readBlock) + } catch (e: FileNotFoundException) { + // TODO: logger +// println("settings.json file not found") + null + } + } + + /** + * Read the .vscode/settings.json file section and expect to return a list of strings as a result + * @param workspaceFolder the workspace folder path to search for the file in + * @param section the section to read + * @return the list of strings, read from the section, or empty list on failure or if there are no items + */ + fun getListOfStringsConfiguration(workspaceFolder: Path, section: Sections): List { + return readConfigFileSection(workspaceFolder) { settingsJsonReader -> + val settingsJsonElement = JsonParser.parseReader(settingsJsonReader) + if (settingsJsonElement.isJsonObject) { + val settingsJsonObject = settingsJsonElement.asJsonObject + val settingsDialectJsonArray = settingsJsonObject.get(section.toString())?.asJsonArray + settingsDialectJsonArray?.map { it.asString } ?: listOf() + } else { + listOf() + } + } ?: listOf() + } + +} diff --git a/src/main/kotlin/org/zowe/cobol/lsp/CobolConfigsRecognitionService.kt b/src/main/kotlin/org/zowe/cobol/lsp/CobolConfigsRecognitionService.kt new file mode 100644 index 0000000..5509491 --- /dev/null +++ b/src/main/kotlin/org/zowe/cobol/lsp/CobolConfigsRecognitionService.kt @@ -0,0 +1,241 @@ +/* + * 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.google.gson.* +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import org.eclipse.lsp4j.ConfigurationItem +import org.zowe.cobol.findFilesInPath +import java.io.File +import java.lang.reflect.Type +import java.net.URI +import java.nio.file.* + +private const val PROCESSOR_GROUP_FOLDER = ".cobolplugin" +private const val PROCESSOR_GROUP_PGM = "pgm_conf.json" +private const val PROCESSOR_GROUP_PROC = "proc_grps.json" +private val COBOL_EXTENSTIONS = listOf(".CBL", ".COB", ".COBOL") + +/** Service to handle COBOL-related configs */ +@Service +class CobolConfigsRecognitionService { + + companion object { + fun getService(): CobolConfigsRecognitionService = service() + } + + /** + * Preprocessor description item class + * @property name the name of the preprocessor + * @property libs the optional preprocessor libs + */ + private data class PreprocessorItem(val name: String, val libs: List = emptyList()) + + /** + * Processor group class to represent processor groups + * @property name the name of the processor group + * @property libs the optional libs to be used + * @property preprocessor the optional preprocessors list to be used with the processor group + */ + private data class ProcessorGroup( + val name: String, + val libs: List = emptyList(), + val preprocessor: List = emptyList() + ) + + /** + * Processor groups container class + * @property pgroups the processor groups list + */ + private data class PGroupContainer(val pgroups: List) + + /** Class to provide a deserialization mechanism for processor groups */ + private class ProcessorGroupDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): ProcessorGroup { + val jsonObject = json.asJsonObject + + val name = jsonObject.get("name").asString + val libs = jsonObject.getAsJsonArray("libs")?.map { it.asString } ?: emptyList() + + val preprocessor = when { + jsonObject.has("preprocessor") -> { + val preprocessorElement = jsonObject.get("preprocessor") + if (preprocessorElement.isJsonArray) { + preprocessorElement.asJsonArray.map { parsePreprocessorItem(it) } + } else { + listOf(parsePreprocessorItem(preprocessorElement)) + } + } + else -> emptyList() + } + + return ProcessorGroup(name, libs, preprocessor) + } + + /** + * Parse preprocessor item from JSON. It is either a string or an object + * @param element the [JsonElement] to parse preprocessor item from + * @return parsed and formed [PreprocessorItem] instance + */ + private fun parsePreprocessorItem(element: JsonElement): PreprocessorItem { + return if (element.isJsonPrimitive && element.asJsonPrimitive.isString) { + PreprocessorItem(name = element.asString) + } else { + val jsonObject = element.asJsonObject + val name = jsonObject.get("name").asString + val libs = jsonObject.getAsJsonArray("libs")?.map { it.asString } ?: emptyList() + PreprocessorItem(name = name, libs = libs) + } + } + } + + /** + * Program config item class to represent a single COBOL program configuration + * @param program a name or a wildcard of a file to be considered as the main program (open code) + * @param pgroup name of a processor group as defined in proc_grps.json + */ + private data class ProgramItem(val program: String, val pgroup: String) + + /** + * Programs config class to represent program configuration options for the COBOL programs + * @param pgms a list of main programs + */ + private data class ProgramContainer(val pgms: List) + + // TODO: recheck functionality correctness + /** + * Match provided processor group path from the deserialized JSON with the actual document path + * @param pgmCfg the [ProgramContainer] config item to find the proc group path that matches with the document path + * @param documentPathStr the document path as a string + * @param workspaceFolder the workspace folder to find the paths match in + * @return the matched processor group name or null + */ + private fun matchProcessorGroup(pgmCfg: ProgramContainer, documentPathStr: String, workspaceFolder: Path): String? { + val documentPath = Paths.get(documentPathStr) + val relativeDocPath = workspaceFolder.relativize(documentPath) + + val candidates = mutableListOf() + + for ((program, pgroup) in pgmCfg.pgms) { + val programPath = try { + Paths.get(program) + } catch (e: InvalidPathException) { + null + } + // exact match + if (programPath != null && programPath.isAbsolute) { + if ( + programPath == documentPath + || programPath.toString().uppercase() == documentPath.toString().uppercase() + ) { + return pgroup + } + } else { + if (relativeDocPath == programPath) { + candidates.add(pgroup) + } + } + + val relativeDocPathIgnoreCase = Paths.get(relativeDocPath.toString().uppercase()) + val matcher = FileSystems.getDefault().getPathMatcher("glob:${program.uppercase()}") + if (matcher.matches(relativeDocPathIgnoreCase)) { + candidates.add(pgroup) + } + } + + return candidates.getOrNull(0) + } + + /** + * Load processors config for the related document + * @param workspaceFolder the workspace folder to load configs in + * @param documentUri the document to find config for + * @return the [ProcessorGroup] config or null + */ + private fun loadProcessorsConfig(workspaceFolder: Path, documentUri: URI): ProcessorGroup? { + val documentPath = Paths.get(documentUri).toString() + val cfgPath = workspaceFolder.resolve(PROCESSOR_GROUP_FOLDER) + val procCfgPath = cfgPath.resolve(PROCESSOR_GROUP_PROC) + val pgmCfgPath = cfgPath.resolve(PROCESSOR_GROUP_PGM) + if (!Files.exists(pgmCfgPath) || !Files.exists(procCfgPath)) { + return null + } + val procCfgFile = File(procCfgPath.toUri()).readText() + val procCfgGson = GsonBuilder() + .registerTypeAdapter(ProcessorGroup::class.java, ProcessorGroupDeserializer()) + .create() + val procCfg = procCfgGson.fromJson(procCfgFile, PGroupContainer::class.java) + + val pgmCfgFile = File(pgmCfgPath.toUri()).readText() + val pgmCfgGson = Gson() + val pgmCfg = pgmCfgGson.fromJson(pgmCfgFile, ProgramContainer::class.java) + + val pgroup = matchProcessorGroup(pgmCfg, documentPath, workspaceFolder) + + val result = procCfg.pgroups.find { p -> pgroup == p.name } + return result + } + + // TODO: doc + fun loadProcessorGroupDialectConfig( + workspaceFolder: Path, + item: ConfigurationItem, + configObject: List + ): List { + return try { + // "SQL" is not a real dialect, we will use it only to set up sql backend for now + loadProcessorsConfig(workspaceFolder, URI(item.scopeUri)) + ?.preprocessor + ?.map { (name, _) -> name } + ?.filter { name -> name != "SQL" } + ?.ifEmpty { configObject } + ?: configObject + } catch (e: Exception) { + println(e) + configObject + } + } + + /** + * Load copybook paths from the related to the [item] processor config + * @param workspaceFolder the workspace folder to find and load config in + * @param item the configuration item to find the related processor configs for + * @param configObject the config object to return as a default configuration if no specific configs found + * @return the related copybook paths + */ + fun loadProcessorGroupCopybookPathsConfig( + workspaceFolder: Path, + item: ConfigurationItem, + configObject: List + ): List { +// val copybookPaths = loadProcessorsConfig(workspaceFolder, URI(item.scopeUri)) +// ?.libs +// ?.ifEmpty { configObject } +// ?: configObject + val copybookPaths = configObject + return findFilesInPath(copybookPaths, workspaceFolder) + } + + // TODO: doc + // TODO: implement +// fun resolveSubroutineURI(project: Project, name: String): String { +// val folders = VSCodeSettingsAdapterService +// .getService(project) +// .getListOfStringsConfiguration(Sections.SUBROUTINE_LOCAL_PATH) +// return searchCopybookInWorkspace(project, name, folders, COBOL_EXTENSTIONS) +// } + +} \ No newline at end of file diff --git a/src/main/kotlin/org/zowe/cobol/lsp/CobolCopybooksService.kt b/src/main/kotlin/org/zowe/cobol/lsp/CobolCopybooksService.kt new file mode 100644 index 0000000..d0063f0 --- /dev/null +++ b/src/main/kotlin/org/zowe/cobol/lsp/CobolCopybooksService.kt @@ -0,0 +1,132 @@ +/* + * 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.components.Service +import com.intellij.openapi.components.service +import org.zowe.cobol.searchForFileInPath +import java.nio.file.Path +import java.nio.file.Paths + +/** Service to handle the functionalities around copybooks */ +@Service +class CobolCopybooksService { + + companion object { + fun getService(): CobolCopybooksService = service() + } + + // TODO: doc + // TODO: implement + // TODO: resolve the actual functionality +// private fun getTargetFolderForCopybook( +// folderKind: string | CopybookFolderKind, +// documentUri: string, +// dialectType: string, +// ) { +// let result: string[] = []; +// const profile = SettingsService.getProfileName()!; +// switch (folderKind) { +// case CopybookFolderKind[CopybookFolderKind.local]: +// result = SettingsService.getCopybookLocalPath(documentUri, dialectType); +// break; +// case CopybookFolderKind[CopybookFolderKind["downloaded-dsn"]]: +// result = SettingsService.getDsnPath(documentUri, dialectType).map( +// (dnsPath) => CopybookURI.createDatasetPath(profile, dnsPath), +// ); +// break; +// case CopybookFolderKind[CopybookFolderKind["downloaded-uss"]]: +// result = SettingsService.getUssPath(documentUri, dialectType).map( +// (dnsPath) => CopybookURI.createDatasetPath(profile, dnsPath), +// ); +// break; +// } +// return result; +// } + + // TODO: doc + // TODO: implement + // TODO: resolve the actual functionality +// private fun searchCopybook( +// documentUri: String, +// copybookName: String, +// dialectType: String +// ) { +// let result: string | undefined; +// for (let i = 0; i < Object.values(CopybookFolderKind).length; i++) { +// const folderKind = Object.values(CopybookFolderKind)[i]; +// const targetFolder = getTargetFolderForCopybook( +// folderKind, +// documentUri, +// dialectType, +// ); +// const allowedExtensions = resolveAllowedExtensions(folderKind, documentUri); +// result = searchCopybookInWorkspace( +// copybookName, +// targetFolder, +// allowedExtensions, +// ); +// if (result) { +// return result; +// } +// } +// return result; +// } + + /** + * Search for the specified copybook in the workspace folder + * @param workspaceFolder the workspace folder path to search for the copybook in + * @param copybookName the name of the copybook to search for + * @param copybookFolders the copybook folders in the workspace to search for the copybook in + * @param extensions the potential copybook extensions to search for the exact copybook by + * @return the found copybook path string or null + */ + private fun searchCopybookInWorkspace( + workspaceFolder: Path, + copybookName: String, + copybookFolders: List, + extensions: List + ): String? { + if (copybookName.isEmpty() || extensions.isEmpty()) return null + for (copybookFolder in copybookFolders) { + for (ext in extensions) { + val foundCopybookPath = searchForFileInPath(workspaceFolder, copybookFolder, copybookName, ext) + if (foundCopybookPath != null) return Paths.get(foundCopybookPath).toUri().toString() + } + } + return null + } + + // TODO: finalize the functionality when interaction with Zowe Explorer is set up + /** + * Resolve a path of the provided copybook + * @param workspaceFolder the workspace folder path + * @param copybookFolders the folders containing the copybooks + * @param copybookName the name of the copybook to search for + * @param copybookExtensions the potential copybook extension to search for the exact copybook + * @return the copybook path in a URI string style + */ + fun resolveCopybookPath( + workspaceFolder: Path, + copybookFolders: List, + copybookName: String, + copybookExtensions: List + ): String? { +// val copybook = searchCopybook(documentUri, copybookName, dialectType); + val result = searchCopybookInWorkspace(workspaceFolder, copybookName, copybookFolders, copybookExtensions) + return result + } + +} diff --git a/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageClient.kt b/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageClient.kt index f509b14..44ff317 100644 --- a/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageClient.kt +++ b/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageClient.kt @@ -16,24 +16,13 @@ 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 org.eclipse.lsp4j.* +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest +import org.zowe.cobol.Sections +import org.zowe.cobol.VSCodeSettingsAdapterService +import java.net.URI import java.util.concurrent.CompletableFuture - -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" +import kotlin.io.path.toPath /** COBOL LSP client wrapper. Provides a comprehensive support for the COBOL LSP communications */ class CobolLanguageClient(project: Project) : LanguageClientImpl(project) { @@ -48,84 +37,109 @@ class CobolLanguageClient(project: Project) : LanguageClientImpl(project) { override fun configuration(configurationParams: ConfigurationParams?): CompletableFuture> { val result = mutableListOf() for (item in configurationParams?.items ?: emptyList()) { + val section = Sections(item.section) try { - if (item.section == DIALECT_REGISTRY_SECTION) { + if (section == Sections.DIALECT_REGISTRY) { logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 1")) result.add(emptyList()) -// val computed = DialectRegistry.getDialects() -// result.add(computed) - } else if (item.scopeUri != "") { +// val dialectInfos = DialectsService.getService().getDialects() +// logMessage(MessageParams(MessageType.Info, "Registered dialects: $dialectInfos")) +// result.add(dialectInfos) + } else if (item.scopeUri != null && item.scopeUri != "") { + val workspaceFolders = this.workspaceFolders().get().map { pathObj -> URI.create(pathObj.uri).toPath() } // val cfg = vscode.workspace.getConfiguration().get(item.section) - when (item.section) { - SETTINGS_DIALECT -> { + when (section) { + Sections.DIALECTS_SECTION -> { logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 2")) result.add(emptyList()) - // val computed = loadProcessorGroupDialectConfig(item, cfg) - // result.add(computed) +// val settingsCfg = VSCodeSettingsAdapterService.getService() +// .getListOfStringsConfiguration(workspaceFolders[0], Sections.DIALECTS_SECTION) +// val dialects = CobolConfigsRecognitionService.getService() +// .loadProcessorGroupDialectConfig(workspaceFolders[0], item, settingsCfg) +// logMessage(MessageParams(MessageType.Info, "For ${item.scopeUri} using dialects: $dialects")) +// result.add() +// result.add(dialects) } - SETTINGS_CPY_LOCAL_PATH -> { - 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) { + + Sections.CPY_LOCAL_PATH -> { + val settingsCfg = VSCodeSettingsAdapterService.getService() + .getListOfStringsConfiguration(workspaceFolders[0], Sections.CPY_LOCAL_PATH) + val cpyLocalPaths = CobolConfigsRecognitionService.getService() + .loadProcessorGroupCopybookPathsConfig(workspaceFolders[0], item, settingsCfg) + logMessage( + MessageParams(MessageType.Info, "For ${item.scopeUri} using cpy local paths: $cpyLocalPaths") + ) + result.add(cpyLocalPaths) } - DIALECT_LIBS -> { - logMessage(MessageParams(MessageType.Info, "${item.section} is not recognized yet 4")) - // val dialectLibs = SettingsService.getCopybookLocalPath(item.scopeUri, item.dialect) - // result.add(dialectLibs) + + Sections.DIALECT_LIBS -> { + logMessage(MessageParams(MessageType.Info, "$section is not recognized yet 3")) + // val dialectLibs = SettingsService.getCopybookLocalPath(item.scopeUri, item.dialect) + // result.add(dialectLibs) } - SETTINGS_CPY_EXTENSIONS -> { - 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) + + Sections.CPY_EXTENSIONS -> { + logMessage(MessageParams(MessageType.Info, "$section is not correctly recognized yet 4")) + result.add(listOf(".CPY", ".COPY", ".cpy", ".copy", "")) + // val computed = loadProcessorGroupCopybookExtensionsConfig(item, cfg as List) + // result.add(computed) } - SETTINGS_SQL_BACKEND -> { - logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 6")) + + Sections.SQL_BACKEND -> { + logMessage(MessageParams(MessageType.Info, "$section is not correctly recognized yet 5")) result.add("DB2_SERVER") - // val computed = loadProcessorGroupSqlBackendConfig(item, cfg as String) - // result.add(computed) + // val computed = loadProcessorGroupSqlBackendConfig(item, cfg as String) + // result.add(computed) } - SETTINGS_CPY_FILE_ENCODING -> { - logMessage(MessageParams(MessageType.Info, "${item.section} is not recognized yet 7")) - // val computed = loadProcessorGroupCopybookEncodingConfig(item, cfg as String) - // result.add(computed) + + Sections.CPY_FILE_ENCODING -> { + logMessage(MessageParams(MessageType.Info, "$section is not recognized yet 6")) + // val computed = loadProcessorGroupCopybookEncodingConfig(item, cfg as String) + // result.add(computed) } - SETTINGS_COMPILE_OPTIONS -> { - logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 8")) + + Sections.COMPILER_OPTIONS -> { + logMessage(MessageParams(MessageType.Info, "$section is not correctly recognized yet 7")) result.add(null) - // val computed = loadProcessorGroupCompileOptionsConfig(item, cfg as String) - // result.add(computed) + // val computed = loadProcessorGroupCompileOptionsConfig(item, cfg as String) + // result.add(computed) } - SETTINGS_CLIENT_LOGGING_LEVEL -> { - logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized 11")) + + Sections.LOGGIN_LEVEL_ROOT -> { + logMessage(MessageParams(MessageType.Info, "$section is not correctly recognized yet 8")) result.add("ERROR") } - SETTINGS_LOCALE -> { - logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized 12")) + + Sections.LOCALE -> { + logMessage(MessageParams(MessageType.Info, "$section is not correctly recognized yet 9")) result.add("en") } - SETTINGS_COBOL_PROGRAM_LAYOUT -> { - logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 12")) + + Sections.COBOL_PROGRAM_LAYOUT -> { + logMessage(MessageParams(MessageType.Info, "$section is not correctly recognized yet 10")) result.add(null) } - SETTINGS_SUBROUTINE_LOCAL_PATH -> { - logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 14")) + + Sections.SUBROUTINE_LOCAL_PATH -> { + logMessage(MessageParams(MessageType.Info, "$section is not correctly recognized yet 11")) result.add(emptyList()) + // } - SETTINGS_CICS_TRANSLATOR -> { - logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 15")) + + Sections.CICS_TRANSLATOR -> { + logMessage(MessageParams(MessageType.Info, "$section is not correctly recognized yet 12")) result.add("true") } + else -> { - // result.add(cfg) - logMessage(MessageParams(MessageType.Info, "${item.section} is not recognized yet 9")) + // result.add(cfg) + logMessage(MessageParams(MessageType.Info, "${item.section} is not recognized yet 13")) } } } else { - logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 10")) + logMessage(MessageParams(MessageType.Info, "${item.section} is not correctly recognized yet 14")) result.add(emptyList()) -// result.add(vscode.workspace.getConfiguration().get(item.section)); +// result.add(vscode.workspace.getConfiguration().get(item.section)) } } catch (error: Throwable) { logMessage(MessageParams(MessageType.Error, "${error.message}\n${error.stackTrace}")) @@ -134,4 +148,40 @@ class CobolLanguageClient(project: Project) : LanguageClientImpl(project) { return CompletableFuture.completedFuture(result) } + /** + * Handle the "copybook/resolve" request + * @param documentUri the document uri string, that triggered the request + * @param copybookName the copybook name to resolve + * @param dialectType the dialect type to resolve the copybook with + * @return the URI path string to the resolved copybook + */ + @JsonRequest("copybook/resolve") + fun resolveCopybook(documentUri: String, copybookName: String, dialectType: String): CompletableFuture { + val cpyExtenstionsConfigItem = ConfigurationItem() + cpyExtenstionsConfigItem.section = Sections.CPY_EXTENSIONS.toString() + cpyExtenstionsConfigItem.scopeUri = documentUri + + val cpyLocalPathsConfigItem = ConfigurationItem() + cpyLocalPathsConfigItem.section = Sections.CPY_LOCAL_PATH.toString() + cpyLocalPathsConfigItem.scopeUri = documentUri + + val configParams = ConfigurationParams(listOf(cpyExtenstionsConfigItem, cpyLocalPathsConfigItem)) + + return this.workspaceFolders() + .thenCombine?, String?>(this.configuration(configParams)) { workspaceFolders, cpyConfigsAny -> + val workspaceFolder = URI.create(workspaceFolders[0].uri).toPath() + + val cpyConfigs = cpyConfigsAny as List> + val cpyExtensions = cpyConfigs[0] + val cpyLocalPaths = cpyConfigs[1] + + CobolCopybooksService.getService() + .resolveCopybookPath(workspaceFolder, cpyLocalPaths, copybookName, cpyExtensions) + } + } + +// TODO: implement custom requests +// @JsonRequest("cobol/resolveSubroutine") +// @JsonRequest("copybook/download") + } diff --git a/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactory.kt b/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactory.kt index 07f3a41..bc75090 100644 --- a/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactory.kt +++ b/src/main/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactory.kt @@ -14,7 +14,6 @@ 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 @@ -28,7 +27,7 @@ import org.zowe.cobol.state.LanguageSupportStateService class CobolLanguageServerFactory : LanguageServerFactory { override fun createConnectionProvider(project: Project): StreamConnectionProvider { - val lsStateService = LanguageSupportStateService.instance + val lsStateService = LanguageSupportStateService.getService() val pluginState = lsStateService.getPluginState(project) { CobolPluginState(project) } @OptIn(InitializationOnly::class) @@ -41,7 +40,7 @@ class CobolLanguageServerFactory : LanguageServerFactory { } override fun createLanguageClient(project: Project): LanguageClientImpl { - val lsStateService = LanguageSupportStateService.instance + val lsStateService = LanguageSupportStateService.getService() val pluginState = lsStateService.getPluginState(project) { CobolPluginState(project) } @OptIn(InitializationOnly::class) diff --git a/src/main/kotlin/org/zowe/cobol/lsp/DialectInfo.kt b/src/main/kotlin/org/zowe/cobol/lsp/DialectInfo.kt new file mode 100644 index 0000000..b35fbec --- /dev/null +++ b/src/main/kotlin/org/zowe/cobol/lsp/DialectInfo.kt @@ -0,0 +1,27 @@ +/* + * 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 java.net.URI + +// TODO: doc +// TODO: clarify the usage +data class DialectInfo( + val name: String, + val uri: URI, + val description: String, + val extensionId: String, + val snippetPath: String +) diff --git a/src/main/kotlin/org/zowe/cobol/lsp/DialectsService.kt b/src/main/kotlin/org/zowe/cobol/lsp/DialectsService.kt new file mode 100644 index 0000000..037721c --- /dev/null +++ b/src/main/kotlin/org/zowe/cobol/lsp/DialectsService.kt @@ -0,0 +1,65 @@ +/* + * 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.components.Service +import com.intellij.openapi.components.service +import java.net.URI + +/** Service class that provides read/write dialect settings functionality */ +@Service +class DialectsService { + + private val dialects: MutableMap = mutableMapOf() + + companion object { + fun getService(): DialectsService = service() + } + + /** + * Gets registered [DialectInfo]s + * @return the list of [DialectInfo]s + */ + fun getDialects(): List { + return dialects.values.toList() + } + + /** Clears the registry */ + fun clear() { + dialects.clear() + } + + /** + * Registers dialect in the system + * @param extensionId the extension id + * @param name the name of the dialect + * @param uri the path to jar file + * @param description the description of the dialect + * @param snippetPath the snippet map path for the dialect + */ + fun register(extensionId: String, name: String, uri: URI, description: String, snippetPath: String) { + val dialectInfo = DialectInfo(name, uri, description, extensionId, snippetPath) + dialects[dialectInfo.name] = dialectInfo + } + + /** + * Unregisters dialect from the system + * @param name the name of the dialect + */ + fun unregister(name: String) { + dialects.remove(name) + } + +} \ 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 index 1af0a1e..e43a443 100644 --- a/src/main/kotlin/org/zowe/cobol/state/LanguageSupportStateService.kt +++ b/src/main/kotlin/org/zowe/cobol/state/LanguageSupportStateService.kt @@ -23,9 +23,7 @@ import com.intellij.openapi.project.Project class LanguageSupportStateService { companion object { - @JvmStatic - val instance - get() = service() + fun getService(): LanguageSupportStateService = service() } private val projectToPluginState = mutableMapOf() diff --git a/src/main/kotlin/org/zowe/cobol/utils.kt b/src/main/kotlin/org/zowe/cobol/utils.kt new file mode 100644 index 0000000..c96b4e1 --- /dev/null +++ b/src/main/kotlin/org/zowe/cobol/utils.kt @@ -0,0 +1,93 @@ +/* + * 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 java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.stream.Stream + +/** + * Find related files in the provided paths + * @param patterns the patterns to search for the files by + * @param paths the paths to search for the files in + * @return list of path strings + */ +fun findRelatedFilesInPaths(patterns: List, paths: Stream): List { + return paths.toList().fold(mutableListOf()) { resultFiles, nextPath -> + val nextResultFiles = patterns.filter { pattern -> + val matcher = FileSystems.getDefault().getPathMatcher("glob:**$pattern") + matcher.matches(nextPath) + } + resultFiles.addAll(nextResultFiles) + resultFiles + } +} + +/** + * Find all file paths that are provided in the config in the specified path + * @param patterns the file patterns to search for (should be as a Unix-style paths or path parts) + * @param pathToSearchBy the path to search in the patterns by + * @return list of file paths as strings + */ +fun findFilesInPath(patterns: List, pathToSearchBy: Path): List { + val matchedFiles: MutableList = mutableListOf() + Files.walk(pathToSearchBy).use { paths -> + matchedFiles.addAll(findRelatedFilesInPaths(patterns, paths)) + } + return matchedFiles +} + +/** + * Cleans the [sourceStr] from the "magic" characters (such as *?[]) + * @param sourceStr the string to clean + * @return a cleaned string + */ +fun getStringWithoutMagic(sourceStr: String): String { + val magicRegex = Regex("[*?\\[\\]]") + val magicFound = magicRegex.find(sourceStr) + val firstMagicCharIdx = magicFound?.range?.first ?: sourceStr.length + return sourceStr.substring(0, firstMagicCharIdx) +} + +/** + * Search for a specific file by the provided parameters in the [parentFolder] path + * @param parentFolder the parent folder path to search for the file in + * @param resource the folder of file in the parent folder path to search for the file in + * @param fileName the file name to search file by + * @param ext the file extension to search the exact file by + */ +fun searchForFileInPath( + parentFolder: Path, + resource: String, + fileName: String, + ext: String +): String? { + val resourcePathStr = getStringWithoutMagic(resource) + val resourcePath = Paths.get(resourcePathStr) + val absResourcePath = if (resourcePath.isAbsolute) resourcePath else parentFolder.resolve(resource) + val fileAbsPathRegex = ".*$fileName\\$ext$".toRegex() + val foundPaths = if (fileAbsPathRegex.matches(absResourcePath.toString())) { + listOf(absResourcePath.toString()) + } else { + val formedPatternPath = absResourcePath.resolve("$fileName$ext") + findFilesInPath( + listOf(formedPatternPath.toString().replace("\\", "/")), + absResourcePath + ) + } + return if (foundPaths.isNotEmpty()) foundPaths[0] else null +} diff --git a/src/test/kotlin/org/zowe/cobol/CobolProjectManagerListenerTestSpec.kt b/src/test/kotlin/org/zowe/cobol/CobolProjectManagerListenerTestSpec.kt index f13285f..08c7c9c 100644 --- a/src/test/kotlin/org/zowe/cobol/CobolProjectManagerListenerTestSpec.kt +++ b/src/test/kotlin/org/zowe/cobol/CobolProjectManagerListenerTestSpec.kt @@ -69,7 +69,7 @@ class CobolProjectManagerListenerTestSpec : FunSpec({ every { getPluginState(projectMock, any<() -> LanguageSupportState>()) } returns lsStateMock } mockkObject(LanguageSupportStateService) - every { LanguageSupportStateService.instance } returns lsStateServiceMock + every { LanguageSupportStateService.getService() } returns lsStateServiceMock cobolProjectManagerListener.projectClosing(projectMock) @@ -89,7 +89,7 @@ class CobolProjectManagerListenerTestSpec : FunSpec({ } } mockkObject(LanguageSupportStateService) - every { LanguageSupportStateService.instance } returns lsStateServiceMock + every { LanguageSupportStateService.getService() } returns lsStateServiceMock cobolProjectManagerListener.projectClosing(projectMock) @@ -109,7 +109,7 @@ class CobolProjectManagerListenerTestSpec : FunSpec({ every { getPluginState(projectMock, any<() -> LanguageSupportState>()) } returns lsStateMock } mockkObject(LanguageSupportStateService) - every { LanguageSupportStateService.instance } returns lsStateServiceMock + every { LanguageSupportStateService.getService() } returns lsStateServiceMock cobolProjectManagerListener.projectClosing(projectMock) diff --git a/src/test/kotlin/org/zowe/cobol/UtilsTestSpec.kt b/src/test/kotlin/org/zowe/cobol/UtilsTestSpec.kt new file mode 100644 index 0000000..35c65d9 --- /dev/null +++ b/src/test/kotlin/org/zowe/cobol/UtilsTestSpec.kt @@ -0,0 +1,236 @@ +/* + * 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 io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.* +import java.nio.file.* + +class UtilsTestSpec : FunSpec({ + context("utils.kt tests") { + afterTest { + unmockkAll() + clearAllMocks() + } + + context("findRelatedFilesInPaths") { + test("return a list of found files") { + mockkStatic(FileSystems::getDefault) + val testPatterns = listOf("test1", "test2", "test3") + val testPath1 = Paths.get("test1") + val testPaths = listOf(testPath1, Paths.get("test4")).stream() + + val mockedDefault = mockk { + every { getPathMatcher(any()) } answers { + if (firstArg() == "glob:**test1") { + mockk { + every { matches(any()) } answers { + firstArg() == testPath1 + } + } + } else { + mockk { + every { matches(any()) } returns false + } + } + } + } + every { FileSystems.getDefault() } returns mockedDefault + + val result = findRelatedFilesInPaths(testPatterns, testPaths) + assertSoftly { result shouldBe listOf(testPath1.toString()) } + } + } + context("searchForFileInPath") { + test("return found path in absolute resource path as folder, that contains the provided file path") { + val testWorkspaceStr = "test_workspace" + val testResourceStr = "test1" + var isPathResolvedWithParent = false + + mockkStatic(::findFilesInPath) + every { findFilesInPath(any(), any()) } answers { listOf(testResourceStr) } + + val workspacePathMock = mockk() + every { workspacePathMock.resolve(any()) } answers { + isPathResolvedWithParent = true + workspacePathMock + } + + mockkStatic(FileSystems::getDefault) + val defaultFileSystem = mockk { + every { getPath(any(), *anyVararg()) } answers { + val pathStr = firstArg() + when (pathStr) { + testWorkspaceStr -> workspacePathMock + testResourceStr -> { + mockk { + every { isAbsolute } returns true + every { resolve(any()) } returns this + } + } + else -> mockk() + } + } + } + every { FileSystems.getDefault() } returns defaultFileSystem + + val testWorkspace = Paths.get(testWorkspaceStr) + + val result = searchForFileInPath( + testWorkspace, + "$testResourceStr?with[magic]", + "test_file", + ".cpy" + ) + assertSoftly { result shouldBe testResourceStr } + assertSoftly { isPathResolvedWithParent shouldBe false } + } + test("return found path in absolute resource path as file, that corresponds to the provided file path") { + val testWorkspaceStr = "test_workspace" + val testResourceStr = "test1/test_file.cpy" + var isPathResolvedWithParent = false + var isFindFilesInPathCalled = false + + mockkStatic(::findFilesInPath) + every { + findFilesInPath(any(), any()) + } answers { + isFindFilesInPathCalled = true + listOf(testResourceStr) + } + + val workspacePathMock = mockk() + every { workspacePathMock.resolve(any()) } answers { + isPathResolvedWithParent = true + workspacePathMock + } + + mockkStatic(FileSystems::getDefault) + val defaultFileSystem = mockk { + every { getPath(any(), *anyVararg()) } answers { + val pathStr = firstArg() + when (pathStr) { + testWorkspaceStr -> workspacePathMock + testResourceStr -> { + mockk pathMockk@ { + every { isAbsolute } returns true + every { resolve(any()) } returns this + every { this@pathMockk.toString() } returns "test1/test_file.cpy" + } + } + else -> mockk() + } + } + } + every { FileSystems.getDefault() } returns defaultFileSystem + + val testWorkspace = Paths.get(testWorkspaceStr) + + val result = searchForFileInPath( + testWorkspace, + testResourceStr, + "test_file", + ".cpy" + ) + assertSoftly { result shouldBe testResourceStr } + assertSoftly { isPathResolvedWithParent shouldBe false } + assertSoftly { isFindFilesInPathCalled shouldBe false } + } + test("return found path in workspace, that contains the provided file path") { + val testWorkspaceStr = "test_workspace" + val testResourceStr = "test1" + var isPathResolvedWithParent = false + + mockkStatic(::findFilesInPath) + every { findFilesInPath(any(), any()) } answers { listOf(testWorkspaceStr) } + + val workspacePathMock = mockk() + every { workspacePathMock.resolve(any()) } answers { + isPathResolvedWithParent = true + workspacePathMock + } + + mockkStatic(FileSystems::getDefault) + val defaultFileSystem = mockk { + every { getPath(any(), *anyVararg()) } answers { + val pathStr = firstArg() + when (pathStr) { + testWorkspaceStr -> workspacePathMock + testResourceStr -> { + mockk { + every { isAbsolute } returns false + every { resolve(any()) } returns this + } + } + else -> mockk() + } + } + } + every { FileSystems.getDefault() } returns defaultFileSystem + + val testWorkspace = Paths.get(testWorkspaceStr) + + val result = searchForFileInPath( + testWorkspace, + "$testResourceStr?with[magic]", + "test_file", + ".cpy" + ) + assertSoftly { result shouldBe testWorkspaceStr } + assertSoftly { isPathResolvedWithParent shouldBe true } + } + test("return null as path is not found in a workspace") { + val testWorkspaceStr = "test_workspace" + val testResourceStr = "test1" + var isPathResolvedWithParent = false + + mockkStatic(::findFilesInPath) + every { findFilesInPath(any(), any()) } answers { listOf() } + + val workspacePathMock = mockk() + every { workspacePathMock.resolve(any()) } answers { + isPathResolvedWithParent = true + workspacePathMock + } + + mockkStatic(FileSystems::getDefault) + val defaultFileSystem = mockk { + every { getPath(any(), *anyVararg()) } answers { + val pathStr = firstArg() + when (pathStr) { + testWorkspaceStr -> workspacePathMock + testResourceStr -> { + mockk { + every { isAbsolute } returns false + every { resolve(any()) } returns this + } + } + else -> mockk() + } + } + } + every { FileSystems.getDefault() } returns defaultFileSystem + + val testWorkspace = Paths.get(testWorkspaceStr) + + val result = searchForFileInPath(testWorkspace, testResourceStr, "test_file", ".cpy") + assertSoftly { result shouldBe null } + assertSoftly { isPathResolvedWithParent shouldBe true } + } + } + } +}) diff --git a/src/test/kotlin/org/zowe/cobol/VSCodeSettingsAdapterServiceTestSpec.kt b/src/test/kotlin/org/zowe/cobol/VSCodeSettingsAdapterServiceTestSpec.kt new file mode 100644 index 0000000..bfe7287 --- /dev/null +++ b/src/test/kotlin/org/zowe/cobol/VSCodeSettingsAdapterServiceTestSpec.kt @@ -0,0 +1,135 @@ +/* + * 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.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.google.gson.stream.JsonReader +import io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.* +import java.io.FileReader +import java.io.Reader +import java.nio.file.Paths +import kotlin.reflect.KFunction + +class VSCodeSettingsAdapterServiceTestSpec : FunSpec({ + context("VSCodeSettingsAdapterService.getListOfStringsConfiguration") { + afterTest { + unmockkAll() + clearAllMocks() + } + + test("get list of strings as the result of a request to the adapter service") { + val testWorkspacePath = Paths.get("test_workspace") + + val settingsJsonReaderMock = mockk() + + val jsonArray = JsonArray() + jsonArray.add(mockk{ every { asString } returns "test_elem" }) + + val jsonObj = mockk() + every { jsonObj.get(Sections.UNRECOGNIZED.toString()) } returns mockk { + every { asJsonArray } returns jsonArray + } + + val parseReaderFun: (Reader) -> JsonElement = JsonParser::parseReader + mockkStatic(parseReaderFun as KFunction<*>) + every { parseReaderFun(settingsJsonReaderMock) } returns mockk { + every { isJsonObject } returns true + every { asJsonObject } returns jsonObj + } + + mockkObject(VSCodeSettingsAdapterService, recordPrivateCalls = true) + val vscodeAdapterService = spyk() + every { + vscodeAdapterService invoke "readConfigFileSection" withArguments listOf(testWorkspacePath, any<(FileReader) -> Any?>()) + } answers { + secondArg<(FileReader) -> List>().invoke(settingsJsonReaderMock) + } + every { VSCodeSettingsAdapterService.getService() } returns vscodeAdapterService + val result = vscodeAdapterService.getListOfStringsConfiguration(testWorkspacePath, Sections.UNRECOGNIZED) + assertSoftly { result shouldBe listOf("test_elem") } + } + test("get empty list cause there is no related section in the json file") { + val testWorkspacePath = Paths.get("test_workspace") + + val settingsJsonReaderMock = mockk() + + val jsonArray = JsonArray() + jsonArray.add(mockk{ every { asString } returns "test_elem" }) + + val jsonObj = mockk() + every { jsonObj.get(Sections.UNRECOGNIZED.toString()) } returns mockk { + every { asJsonArray } returns null + } + + val parseReaderFun: (Reader) -> JsonElement = JsonParser::parseReader + mockkStatic(parseReaderFun as KFunction<*>) + every { parseReaderFun(settingsJsonReaderMock) } returns mockk { + every { isJsonObject } returns true + every { asJsonObject } returns jsonObj + } + + mockkObject(VSCodeSettingsAdapterService, recordPrivateCalls = true) + val vscodeAdapterService = spyk() + every { + vscodeAdapterService invoke "readConfigFileSection" withArguments listOf(testWorkspacePath, any<(FileReader) -> Any?>()) + } answers { + secondArg<(FileReader) -> List>().invoke(settingsJsonReaderMock) + } + every { VSCodeSettingsAdapterService.getService() } returns vscodeAdapterService + val result = vscodeAdapterService.getListOfStringsConfiguration(testWorkspacePath, Sections.UNRECOGNIZED) + assertSoftly { result shouldBe listOf() } + } + test("get empty list cause the json file is not a json object") { + val testWorkspacePath = Paths.get("test_workspace") + + val settingsJsonReaderMock = mockk() + + val parseReaderFun: (Reader) -> JsonElement = JsonParser::parseReader + mockkStatic(parseReaderFun as KFunction<*>) + every { parseReaderFun(settingsJsonReaderMock) } returns mockk { + every { isJsonObject } returns false + } + + mockkObject(VSCodeSettingsAdapterService, recordPrivateCalls = true) + val vscodeAdapterService = spyk() + every { + vscodeAdapterService invoke "readConfigFileSection" withArguments listOf(testWorkspacePath, any<(FileReader) -> Any?>()) + } answers { + secondArg<(FileReader) -> List>().invoke(settingsJsonReaderMock) + } + every { VSCodeSettingsAdapterService.getService() } returns vscodeAdapterService + val result = vscodeAdapterService.getListOfStringsConfiguration(testWorkspacePath, Sections.UNRECOGNIZED) + assertSoftly { result shouldBe listOf() } + } + test("get empty list cause the json file is not found") { + val testWorkspacePath = Paths.get("test_workspace") + + mockkObject(VSCodeSettingsAdapterService, recordPrivateCalls = true) + val vscodeAdapterService = spyk() + every { + vscodeAdapterService invoke "readConfigFileSection" withArguments listOf(testWorkspacePath, any<(FileReader) -> Any?>()) + } returns null + every { VSCodeSettingsAdapterService.getService() } returns vscodeAdapterService + val result = vscodeAdapterService.getListOfStringsConfiguration(testWorkspacePath, Sections.UNRECOGNIZED) + assertSoftly { result shouldBe listOf() } + } + } +}) diff --git a/src/test/kotlin/org/zowe/cobol/lsp/CobolConfigsRecognitionServiceTestSpec.kt b/src/test/kotlin/org/zowe/cobol/lsp/CobolConfigsRecognitionServiceTestSpec.kt new file mode 100644 index 0000000..5ba51e3 --- /dev/null +++ b/src/test/kotlin/org/zowe/cobol/lsp/CobolConfigsRecognitionServiceTestSpec.kt @@ -0,0 +1,21 @@ +/* + * 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 io.kotest.core.spec.style.FunSpec + +class CobolConfigsRecognitionServiceTestSpec : FunSpec({ + // TODO: finish +}) \ No newline at end of file diff --git a/src/test/kotlin/org/zowe/cobol/lsp/CobolCopybooksServiceTestSpec.kt b/src/test/kotlin/org/zowe/cobol/lsp/CobolCopybooksServiceTestSpec.kt new file mode 100644 index 0000000..4bc78fc --- /dev/null +++ b/src/test/kotlin/org/zowe/cobol/lsp/CobolCopybooksServiceTestSpec.kt @@ -0,0 +1,105 @@ +/* + * 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 io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.* +import org.zowe.cobol.searchForFileInPath +import java.nio.file.Paths + +class CobolCopybooksServiceTestSpec : FunSpec({ + context("CobolCopybooksService.resolveCopybookPath") { + afterTest { + unmockkAll() + clearAllMocks() + } + + test("resolve copybook path") { + val testWorkspacePathStr = "test_workspace" + val testWorkspacePath = Paths.get(testWorkspacePathStr) + val testCopybookName = "TSTCPY" + val testCopybookFolders = listOf("test_res") + val testExtensions = listOf(".TST") + + val fullTestPath = "$testWorkspacePathStr/${testCopybookFolders[0]}/$testCopybookName${testExtensions[0]}" + + mockkStatic(::searchForFileInPath) + every { searchForFileInPath(any(), any(), any(), any()) } returns fullTestPath + + val cobolCopybooksService = spyk() + + mockkObject(CobolCopybooksService) + every { CobolCopybooksService.getService() } returns cobolCopybooksService + + val result = cobolCopybooksService + .resolveCopybookPath(testWorkspacePath, testCopybookFolders, testCopybookName, testExtensions) + val expected = Paths.get(fullTestPath).toUri().toString() + assertSoftly { result shouldBe expected } + } + test("return null as no suitable copybook path found") { + val testWorkspacePathStr = "test_workspace" + val testWorkspacePath = Paths.get(testWorkspacePathStr) + val testCopybookName = "TSTCPY" + val testCopybookFolders = listOf("test_res") + val testExtensions = listOf(".TST") + + mockkStatic(::searchForFileInPath) + every { searchForFileInPath(any(), any(), any(), any()) } returns null + + val cobolCopybooksService = spyk() + + mockkObject(CobolCopybooksService) + every { CobolCopybooksService.getService() } returns cobolCopybooksService + + val result = cobolCopybooksService + .resolveCopybookPath(testWorkspacePath, testCopybookFolders, testCopybookName, testExtensions) + assertSoftly { result shouldBe null } + } + test("return null as there are no extensions") { + val testWorkspacePathStr = "test_workspace" + val testWorkspacePath = Paths.get(testWorkspacePathStr) + val testCopybookName = "test" + val testCopybookFolders = listOf() + val testExtensions = listOf() + + val cobolCopybooksService = spyk() + + mockkObject(CobolCopybooksService) + every { CobolCopybooksService.getService() } returns cobolCopybooksService + + val result = cobolCopybooksService + .resolveCopybookPath(testWorkspacePath, testCopybookFolders, testCopybookName, testExtensions) + assertSoftly { result shouldBe null } + } + test("return null as there is no copybook name") { + val testWorkspacePathStr = "test_workspace" + val testWorkspacePath = Paths.get(testWorkspacePathStr) + val testCopybookName = "" + val testCopybookFolders = listOf() + val testExtensions = listOf() + + val cobolCopybooksService = spyk() + + mockkObject(CobolCopybooksService) + every { CobolCopybooksService.getService() } returns cobolCopybooksService + + val result = cobolCopybooksService + .resolveCopybookPath(testWorkspacePath, testCopybookFolders, testCopybookName, testExtensions) + assertSoftly { result shouldBe null } + } + } +}) diff --git a/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientTestSpec.kt b/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientConfigurationTestSpec.kt similarity index 72% rename from src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientTestSpec.kt rename to src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientConfigurationTestSpec.kt index 2cfd147..011a509 100644 --- a/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientTestSpec.kt +++ b/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientConfigurationTestSpec.kt @@ -23,8 +23,14 @@ import io.mockk.* import org.eclipse.lsp4j.ConfigurationItem import org.eclipse.lsp4j.ConfigurationParams import org.eclipse.lsp4j.MessageParams +import org.eclipse.lsp4j.WorkspaceFolder +import org.zowe.cobol.Sections +import org.zowe.cobol.VSCodeSettingsAdapterService +import java.net.URI +import java.util.concurrent.CompletableFuture +import kotlin.io.path.toPath -class CobolLanguageClientTestSpec : FunSpec({ +class CobolLanguageClientConfigurationTestSpec : FunSpec({ context("CobolLanguageClientTestSpec.configuration") { afterTest { @@ -37,14 +43,14 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { - every { section } returns DIALECT_REGISTRY_SECTION + every { section } returns Sections.DIALECT_REGISTRY.toString() } 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)) { + if (firstArg().message.contains(Sections.DIALECT_REGISTRY.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -68,6 +74,9 @@ class CobolLanguageClientTestSpec : FunSpec({ every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { if (firstArg().message.contains(someTestSection)) { isLogMessageTriggeredCorrectly = true @@ -86,14 +95,17 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns SETTINGS_DIALECT + every { section } returns Sections.DIALECTS_SECTION.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(SETTINGS_DIALECT)) { + if (firstArg().message.contains(Sections.DIALECTS_SECTION.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -107,17 +119,40 @@ class CobolLanguageClientTestSpec : FunSpec({ test("process 'workspace/configuration' for cpy-manager paths-local request with scope URI") { var isLogMessageTriggeredCorrectly = false + val fakeUriPath = "file:///c/test" + val fakeFinalPath = "final_test_path" + + val vscodeSettingsAdapterService = mockk { + every { + getListOfStringsConfiguration(URI.create(fakeUriPath).toPath(), Sections.CPY_LOCAL_PATH) + } returns listOf() + } + mockkObject(VSCodeSettingsAdapterService) + every { VSCodeSettingsAdapterService.getService() } returns vscodeSettingsAdapterService + + val cobolConfigsRecognitionService = mockk { + every { + loadProcessorGroupCopybookPathsConfig(URI.create(fakeUriPath).toPath(), any(), listOf()) + } returns listOf(fakeFinalPath) + } + + mockkObject(CobolConfigsRecognitionService) + every { CobolConfigsRecognitionService.getService() } returns cobolConfigsRecognitionService + val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns SETTINGS_CPY_LOCAL_PATH + every { section } returns Sections.CPY_LOCAL_PATH.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder(fakeUriPath))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(SETTINGS_CPY_LOCAL_PATH)) { + if (firstArg().message.contains(fakeFinalPath)) { isLogMessageTriggeredCorrectly = true } } @@ -125,7 +160,7 @@ class CobolLanguageClientTestSpec : FunSpec({ val result = cobolLanguageClient.configuration(configurationParamsMock).join() assertSoftly { isLogMessageTriggeredCorrectly shouldBe true } - assertSoftly { result shouldBeEqual listOf() } + assertSoftly { result shouldBeEqual listOf(listOf(fakeFinalPath)) } } test("process 'workspace/configuration' for dialect libs request with scope URI") { @@ -134,14 +169,17 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns DIALECT_LIBS + every { section } returns Sections.DIALECT_LIBS.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(DIALECT_LIBS)) { + if (firstArg().message.contains(Sections.DIALECT_LIBS.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -158,14 +196,17 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns SETTINGS_CPY_EXTENSIONS + every { section } returns Sections.CPY_EXTENSIONS.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(SETTINGS_CPY_EXTENSIONS)) { + if (firstArg().message.contains(Sections.CPY_EXTENSIONS.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -182,14 +223,17 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns SETTINGS_SQL_BACKEND + every { section } returns Sections.SQL_BACKEND.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(SETTINGS_SQL_BACKEND)) { + if (firstArg().message.contains(Sections.SQL_BACKEND.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -206,14 +250,17 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns SETTINGS_CPY_FILE_ENCODING + every { section } returns Sections.CPY_FILE_ENCODING.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(SETTINGS_CPY_FILE_ENCODING)) { + if (firstArg().message.contains(Sections.CPY_FILE_ENCODING.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -230,14 +277,17 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns SETTINGS_COMPILE_OPTIONS + every { section } returns Sections.COMPILER_OPTIONS.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(SETTINGS_COMPILE_OPTIONS)) { + if (firstArg().message.contains(Sections.COMPILER_OPTIONS.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -254,14 +304,17 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns SETTINGS_CLIENT_LOGGING_LEVEL + every { section } returns Sections.LOGGIN_LEVEL_ROOT.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(SETTINGS_CLIENT_LOGGING_LEVEL)) { + if (firstArg().message.contains(Sections.LOGGIN_LEVEL_ROOT.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -278,14 +331,17 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns SETTINGS_LOCALE + every { section } returns Sections.LOCALE.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(SETTINGS_LOCALE)) { + if (firstArg().message.contains(Sections.LOCALE.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -302,14 +358,17 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns SETTINGS_COBOL_PROGRAM_LAYOUT + every { section } returns Sections.COBOL_PROGRAM_LAYOUT.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(SETTINGS_COBOL_PROGRAM_LAYOUT)) { + if (firstArg().message.contains(Sections.COBOL_PROGRAM_LAYOUT.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -326,14 +385,17 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns SETTINGS_SUBROUTINE_LOCAL_PATH + every { section } returns Sections.SUBROUTINE_LOCAL_PATH.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(SETTINGS_SUBROUTINE_LOCAL_PATH)) { + if (firstArg().message.contains(Sections.SUBROUTINE_LOCAL_PATH.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -350,14 +412,17 @@ class CobolLanguageClientTestSpec : FunSpec({ val projectMock = mockk() val configurationItemMock = mockk { every { scopeUri } returns "test" - every { section } returns SETTINGS_CICS_TRANSLATOR + every { section } returns Sections.CICS_TRANSLATOR.toString() } val configurationParamsMock = mockk { every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { - if (firstArg().message.contains(SETTINGS_CICS_TRANSLATOR)) { + if (firstArg().message.contains(Sections.CICS_TRANSLATOR.toString())) { isLogMessageTriggeredCorrectly = true } } @@ -380,6 +445,9 @@ class CobolLanguageClientTestSpec : FunSpec({ every { items } returns listOf(configurationItemMock) } val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder("file:///c/test"))) every { cobolLanguageClient.logMessage(any()) } answers { if (firstArg().message.contains(someTestSection)) { isLogMessageTriggeredCorrectly = true diff --git a/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientResolveCopybookTestSpec.kt b/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientResolveCopybookTestSpec.kt new file mode 100644 index 0000000..cc9f1a3 --- /dev/null +++ b/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageClientResolveCopybookTestSpec.kt @@ -0,0 +1,70 @@ +/* + * 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 +import org.eclipse.lsp4j.WorkspaceFolder +import org.zowe.cobol.Sections +import org.zowe.cobol.VSCodeSettingsAdapterService +import java.net.URI +import java.util.concurrent.CompletableFuture +import kotlin.io.path.toPath + +class CobolLanguageClientResolveCopybookTestSpec : FunSpec({ + + context("CobolLanguageClientTestSpec.resolveCopybook") { + afterTest { + unmockkAll() + clearAllMocks() + } + + test("process 'copybook/resolve' request") { + val fakeUriPath = "file:///c/test" + val testCopybookName = "TSTCPY" + val testCpyExtensions = listOf(".testext") + val testCpyLocalPaths = listOf("test_local_path") + val fakeFinalPath = "final_test_path" + + val cobolCopybooksService = mockk { + every { + resolveCopybookPath(URI.create(fakeUriPath).toPath(), testCpyLocalPaths, testCopybookName, testCpyExtensions) + } returns fakeFinalPath + } + mockkObject(CobolCopybooksService) + every { CobolCopybooksService.getService() } returns cobolCopybooksService + + val projectMock = mockk() + val cobolLanguageClient = spyk(CobolLanguageClient(projectMock)) + every { + cobolLanguageClient.workspaceFolders() + } returns CompletableFuture.completedFuture(listOf(WorkspaceFolder(fakeUriPath))) + every { + cobolLanguageClient.configuration(any()) + } returns CompletableFuture.completedFuture(mutableListOf(testCpyExtensions, testCpyLocalPaths)) + + val result = cobolLanguageClient.resolveCopybook("test", testCopybookName, "test").join() + assertSoftly { result shouldBe fakeFinalPath } + } + } + +}) diff --git a/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactoryTestSpec.kt b/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactoryTestSpec.kt index 1cfaf0e..f29f57d 100644 --- a/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactoryTestSpec.kt +++ b/src/test/kotlin/org/zowe/cobol/lsp/CobolLanguageServerFactoryTestSpec.kt @@ -60,7 +60,7 @@ class CobolLanguageServerFactoryTestSpec : FunSpec({ every { getPluginState(projectMock, any<() -> LanguageSupportState>()) } returns lsStateMock } mockkObject(LanguageSupportStateService) - every { LanguageSupportStateService.instance } returns lsStateService + every { LanguageSupportStateService.getService() } returns lsStateService val cobolLanguageServerFactory = spyk(CobolLanguageServerFactory()) @@ -99,7 +99,7 @@ class CobolLanguageServerFactoryTestSpec : FunSpec({ } } mockkObject(LanguageSupportStateService) - every { LanguageSupportStateService.instance } returns lsStateService + every { LanguageSupportStateService.getService() } returns lsStateService val cobolLanguageServerFactory = spyk(CobolLanguageServerFactory()) @@ -144,7 +144,7 @@ class CobolLanguageServerFactoryTestSpec : FunSpec({ every { getPluginState(projectMock, any<() -> LanguageSupportState>()) } returns lsStateMock } mockkObject(LanguageSupportStateService) - every { LanguageSupportStateService.instance } returns lsStateService + every { LanguageSupportStateService.getService() } returns lsStateService val cobolLanguageServerFactory = spyk(CobolLanguageServerFactory()) @@ -183,7 +183,7 @@ class CobolLanguageServerFactoryTestSpec : FunSpec({ } } mockkObject(LanguageSupportStateService) - every { LanguageSupportStateService.instance } returns lsStateService + every { LanguageSupportStateService.getService() } returns lsStateService val cobolLanguageServerFactory = spyk(CobolLanguageServerFactory()) diff --git a/src/test/kotlin/org/zowe/cobol/state/CobolPluginStateTestSpec.kt b/src/test/kotlin/org/zowe/cobol/state/CobolPluginStateTestSpec.kt index e24f1bb..ae76db3 100644 --- a/src/test/kotlin/org/zowe/cobol/state/CobolPluginStateTestSpec.kt +++ b/src/test/kotlin/org/zowe/cobol/state/CobolPluginStateTestSpec.kt @@ -39,12 +39,12 @@ class CobolPluginStateTestSpec : FunSpec({ context("CobolPluginStateTestSpec: lateinit vars") { val cobolState = spyk(CobolPluginState(mockk())) - test("make a try to get not yet initialized lateinit var") { + test("make a try to get not yet initialized lateinit var of LSP server") { 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") { + test("make a try to get not yet initialized lateinit var of LSP client") { val exception = assertThrows { cobolState.getReadyLSPClient() } assertSoftly { exception.message shouldContain "LSP client is not ready" } } diff --git a/src/test/kotlin/org/zowe/cobol/state/LanguageSupportStateServiceTestSpec.kt b/src/test/kotlin/org/zowe/cobol/state/LanguageSupportStateServiceTestSpec.kt index 235d3fa..81b2fe6 100644 --- a/src/test/kotlin/org/zowe/cobol/state/LanguageSupportStateServiceTestSpec.kt +++ b/src/test/kotlin/org/zowe/cobol/state/LanguageSupportStateServiceTestSpec.kt @@ -40,7 +40,7 @@ class LanguageSupportStateServiceTestSpec : FunSpec({ val lsStateService = spyk(LanguageSupportStateService(), recordPrivateCalls = true) mockkObject(LanguageSupportStateService) - every { LanguageSupportStateService.instance } returns lsStateService + every { LanguageSupportStateService.getService() } returns lsStateService setPrivateFieldValue( lsStateService, @@ -49,7 +49,7 @@ class LanguageSupportStateServiceTestSpec : FunSpec({ mutableMapOf(projectMock to lsStateMock) ) - val result = LanguageSupportStateService.instance.getPluginState(projectMock) { defaultLSStateMock } + val result = LanguageSupportStateService.getService().getPluginState(projectMock) { defaultLSStateMock } assertSoftly { result shouldBe lsStateMock } } @@ -61,7 +61,7 @@ class LanguageSupportStateServiceTestSpec : FunSpec({ val lsStateService = spyk(LanguageSupportStateService(), recordPrivateCalls = true) mockkObject(LanguageSupportStateService) - every { LanguageSupportStateService.instance } returns lsStateService + every { LanguageSupportStateService.getService() } returns lsStateService setPrivateFieldValue( lsStateService, @@ -70,7 +70,7 @@ class LanguageSupportStateServiceTestSpec : FunSpec({ mutableMapOf(mockk() to lsStateMock) ) - val result = LanguageSupportStateService.instance.getPluginState(projectMock) { defaultLSStateMock } + val result = LanguageSupportStateService.getService().getPluginState(projectMock) { defaultLSStateMock } assertSoftly { result shouldBe defaultLSStateMock } }