From 29471b40f6486b676e8aba76a607f699e897e250 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Fri, 4 Apr 2025 14:43:08 +0200 Subject: [PATCH] backport: 25.0.0 --- CHANGELOG.md | 16 + LICENSE | 8 +- README.md | 7 +- build.gradle.kts | 3 +- cidr/build.gradle.kts | 6 +- .../execution/binary/ZigProfileStateBinary.kt | 4 +- .../runner/base/PreLaunchProcessListener.kt | 3 - .../base/ZigDebugEmitBinaryInstaller.kt | 4 +- .../runner/base/ZigDebugParametersBase.kt | 4 +- .../base/ZigDebugParametersEmitBinaryBase.kt | 4 +- .../runner/base/ZigDebugRunnerBase.kt | 8 +- .../runner/binary/ZigDebugParametersBinary.kt | 4 +- .../runner/binary/ZigDebugRunnerBinary.kt | 6 +- .../runner/build/ZigDebugParametersBuild.kt | 4 +- .../runner/build/ZigDebugRunnerBuild.kt | 6 +- .../runner/run/ZigDebugParametersRun.kt | 4 +- .../debugger/runner/run/ZigDebugRunnerRun.kt | 6 +- .../runner/test/ZigDebugParametersTest.kt | 4 +- .../runner/test/ZigDebugRunnerTest.kt | 6 +- .../toolchain/ZigDebuggerToolchainService.kt | 94 ++--- .../com/falsepattern/zigbrains/ZBStartup.kt | 19 - .../direnv/{DirenvCmd.kt => DirenvService.kt} | 96 +++-- ...DirenvProjectService.kt => DirenvState.kt} | 19 +- .../com/falsepattern/zigbrains/direnv/Env.kt | 12 +- .../zigbrains/direnv/ui/DirenvEditor.kt | 101 ++++++ .../project/execution/ZigConsoleBuilder.kt | 1 - .../project/execution/base/Configuration.kt | 3 - .../project/execution/base/ZigExecConfig.kt | 9 +- .../project/execution/base/ZigProfileState.kt | 19 +- .../project/module/ZigModuleBuilder.kt | 6 +- .../project/newproject/ZigNewProjectPanel.kt | 9 +- .../newproject/ZigProjectConfigurationData.kt | 9 +- .../newproject/ZigProjectGeneratorPeer.kt | 11 +- .../project/run/ZigProcessHandler.kt | 3 - .../zigbrains/project/run/ZigProgramRunner.kt | 8 +- .../zigbrains/project/run/ZigRegularRunner.kt | 9 +- .../project/settings/ZigConfigurable.kt | 11 +- .../settings/ZigProjectConfigurable.kt | 60 ---- .../ZigProjectConfigurationProvider.kt | 68 ++-- .../project/settings/ZigProjectSettings.kt | 55 --- .../settings/ZigProjectSettingsService.kt | 60 ---- .../stdlib/ZigLibraryRootProvider.kt | 2 +- .../stdlib/ZigSyntheticLibrary.kt | 108 ++++-- .../discovery/ZigStepDiscoveryService.kt | 4 +- .../toolchain/LocalZigToolchainProvider.kt | 66 ---- .../toolchain/ZigToolchainListService.kt | 57 +++ .../project/toolchain/ZigToolchainProvider.kt | 69 ---- .../project/toolchain/ZigToolchainService.kt | 76 ++++ .../project/toolchain/base/ZigToolchain.kt | 70 ++++ .../base/ZigToolchainConfigurable.kt | 103 ++++++ .../base/ZigToolchainExtensionsProvider.kt | 46 +++ .../toolchain/base/ZigToolchainProvider.kt | 93 +++++ .../downloader/LocalToolchainDownloader.kt | 38 ++ .../downloader/LocalToolchainSelector.kt | 88 +++++ .../toolchain/downloader/ZigVersionInfo.kt | 87 +++++ .../{ => local}/LocalZigToolchain.kt | 41 ++- .../local/LocalZigToolchainConfigurable.kt | 33 +- .../local/LocalZigToolchainPanel.kt} | 142 +++----- .../local/LocalZigToolchainProvider.kt | 147 ++++++++ .../toolchain/tools/ZigCompilerTool.kt | 5 +- .../project/toolchain/tools/ZigTool.kt | 10 +- .../ZigToolchainEnvironmentSerializable.kt | 7 +- .../toolchain/ui/ImmutableElementPanel.kt | 36 ++ .../ui/ImmutableNamedElementPanelBase.kt | 43 +++ .../ui/ZigToolchainComboBoxHandler.kt | 42 +++ .../toolchain/ui/ZigToolchainDriver.kt | 91 +++++ .../toolchain/ui/ZigToolchainEditor.kt | 141 ++++++++ .../toolchain/ui/ZigToolchainListEditor.kt | 31 ++ .../zigbrains/project/toolchain/ui/model.kt | 80 +++++ .../zigbrains/shared/MultiConfigurable.kt | 57 --- .../zigbrains/shared/NamedObject.kt | 32 ++ .../zigbrains/shared/SubConfigurable.kt | 74 +++- .../com/falsepattern/zigbrains/shared/UUID.kt | 28 ++ .../zigbrains/shared/UUIDMapSerializable.kt | 191 ++++++++++ .../zigbrains/shared/Unarchiver.kt | 73 ++++ .../zigbrains/shared/cli/CLIUtil.kt | 1 - .../shared/coroutine/CoroutinesUtil.kt | 22 +- .../shared/downloader/DirectoryState.kt | 78 ++++ .../zigbrains/shared/downloader/Downloader.kt | 172 +++++++++ .../shared/downloader/LocalSelector.kt | 138 ++++++++ .../shared/downloader/VersionInfo.kt | 163 +++++++++ .../zigbrains/shared/ipc/IPCUtil.kt | 6 +- .../zigbrains/shared/ui/UUIDComboBoxDriver.kt | 37 ++ .../zigbrains/shared/ui/UUIDMapEditor.kt | 169 +++++++++ .../zigbrains/shared/ui/UUIDMapSelector.kt | 192 ++++++++++ .../zigbrains/shared/ui/elements.kt | 86 +++++ .../falsepattern/zigbrains/shared/ui/model.kt | 288 +++++++++++++++ .../resources/META-INF/zigbrains-core.xml | 19 +- .../resources/zigbrains/Bundle.properties | 44 +++ gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 43705 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- licenses/ZLS.LICENSE | 21 ++ lsp/build.gradle.kts | 4 +- .../falsepattern/zigbrains/lsp/LSPIcons.kt | 32 ++ .../ToolchainZLSConfigProvider.kt | 26 +- .../falsepattern/zigbrains/lsp/ZLSStartup.kt | 28 +- .../lsp/ZLSStreamConnectionProvider.kt | 33 +- .../zigbrains/lsp/ZigLanguageServerFactory.kt | 26 +- .../ZigEditorNotificationProvider.kt | 8 +- .../lsp/settings/ZLSProjectSettingsService.kt | 158 --------- .../zigbrains/lsp/settings/ZLSSettings.kt | 54 ++- .../lsp/settings/ZLSSettingsConfigProvider.kt | 3 +- .../lsp/settings/ZLSSettingsConfigurable.kt | 57 --- .../lsp/settings/ZLSSettingsPanel.kt | 332 ++++++------------ .../zigbrains/lsp/zls/ZLSConfigurable.kt | 87 +++++ .../lsp/zls/ZLSInstallationsService.kt | 54 +++ .../zigbrains/lsp/zls/ZLSPanel.kt | 142 ++++++++ .../zigbrains/lsp/zls/ZLSService.kt | 32 +- .../zigbrains/lsp/zls/ZLSVersion.kt | 91 +++++ .../lsp/zls/downloader/ZLSDownloader.kt | 45 +++ .../lsp/zls/downloader/ZLSLocalSelector.kt | 74 ++++ .../lsp/zls/downloader/ZLSVersionInfo.kt | 99 ++++++ .../zigbrains/lsp/zls/ui/ZLSDriver.kt | 221 ++++++++++++ .../zigbrains/lsp/zls/ui/ZLSEditor.kt | 93 +++++ .../zigbrains/lsp/zls/ui/ZLSListEditor.kt | 24 +- .../zigbrains/lsp/zls/ui/model.kt | 83 +++++ .../main/resources/META-INF/zigbrains-lsp.xml | 13 +- lsp/src/main/resources/icons/zls.svg | 19 + .../resources/zigbrains/lsp/Bundle.properties | 27 +- settings.gradle.kts | 2 +- src/art/zls/zls.svg | 19 + src/main/resources/META-INF/plugin.xml | 9 +- 124 files changed, 4938 insertions(+), 1401 deletions(-) rename core/src/main/kotlin/com/falsepattern/zigbrains/direnv/{DirenvCmd.kt => DirenvService.kt} (59%) rename core/src/main/kotlin/com/falsepattern/zigbrains/direnv/{DirenvProjectService.kt => DirenvState.kt} (77%) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurable.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt rename core/src/main/kotlin/com/falsepattern/zigbrains/project/{toolchain => }/stdlib/ZigLibraryRootProvider.kt (95%) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/{toolchain => }/stdlib/ZigSyntheticLibrary.kt (56%) delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainDownloader.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainSelector.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{ => local}/LocalZigToolchain.kt (60%) rename lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt => core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt (51%) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/{settings/ZigProjectSettingsPanel.kt => toolchain/local/LocalZigToolchainPanel.kt} (51%) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{ => tools}/ZigToolchainEnvironmentSerializable.kt (93%) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableNamedElementPanelBase.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/MultiConfigurable.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUID.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/DirectoryState.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/Downloader.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/LocalSelector.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/VersionInfo.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt create mode 100644 licenses/ZLS.LICENSE create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/LSPIcons.kt rename lsp/src/main/kotlin/com/falsepattern/zigbrains/{project/toolchain => lsp}/ToolchainZLSConfigProvider.kt (77%) delete mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt delete mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSInstallationsService.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt rename core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigCoreProjectConfigurationProvider.kt => lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt (54%) create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSDownloader.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSVersionInfo.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt => lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt (57%) create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt create mode 100644 lsp/src/main/resources/icons/zls.svg create mode 100644 src/art/zls/zls.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b5538a..dd4bb190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,22 @@ Changelog structure reference: ## [Unreleased] +## [25.0.0] + +### Fixed + +- Project + - Zig.iml file created in every project + +### Changed + +- Project + - BREAKING MAJOR UPDATE: Fully reworked toolchain and language server management + The configuration menu is now very similar to the intellij java toolchain management, + with proper toolchain selection, detection, downloading, etc. This change will require + you to re-configure your toolchains! + - Zig external library root is now no longer shown if zig is not configured + ## [24.0.1] ### Added diff --git a/LICENSE b/LICENSE index 2d0c7b3b..22ea4295 100644 --- a/LICENSE +++ b/LICENSE @@ -25,6 +25,11 @@ which are the property of the Zig Software Foundation. (https://github.com/ziglang/logo) These art assets are licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0). -------------------------------- +The art assets inside src/art/zls, and all copies of them, are derived from the Zig Language Server, +which are the property of the zigtools organization. +(https://github.com/zigtools/zls) +These art assets are licensed under MIT license. +-------------------------------- Parts of the codebase are based on the intellij-zig plugin, developed by HTGAzureX1212 (https://github.com/HTGAzureX1212), licensed under the Apache 2.0 license. -------------------------------- @@ -37,4 +42,5 @@ All of the licenses listed here are available in the following files, bundled wi - licenses/CC_BY_SA_4.0.LICENSE - licenses/GPL3.LICENSE - licenses/INTELLIJ-RUST.LICENSE -- licenses/LGPL3.LICENSE \ No newline at end of file +- licenses/LGPL3.LICENSE +- licenses/ZLS.LICENSE \ No newline at end of file diff --git a/README.md b/README.md index 833018fb..289a7aa2 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,7 @@ excellent example on how to write debugger support that doesn't depend on CLion. Adds support for the Zig Language, utilizing the ZLS language server for advanced coding assistance. -## Quick setup guide for Zig and ZLS - -1. Download the latest version of Zig from https://ziglang.org/download -2. Download and compile the ZLS language server, available at https://github.com/zigtools/zls -3. Go to `Settings` -> `Languages & Frameworks` -> `Zig`, and point the `Toolchain Location` and `ZLS path` to the correct places +Before you can properly use the plugin, you need to select or download the Zig toolchain and language server in `Settings` -> `Languages & Frameworks` -> `Zig`. ## Debugging @@ -89,6 +85,7 @@ Debugging Zig code is supported in any native debugging capable IDE. The followi - RustRover (including the non-commercial free version too) - GoLand - PyCharm Professional +- Android Studio Additionally, in CLion, the plugin uses the C++ Toolchains for sourcing the debugger (this can be toggled off in the settings). diff --git a/build.gradle.kts b/build.gradle.kts index c19f1955..c16bf772 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask plugins { kotlin("jvm") version "1.9.24" apply false kotlin("plugin.serialization") version "1.9.24" apply false - id("org.jetbrains.intellij.platform") version "2.2.1" + id("org.jetbrains.intellij.platform") version "2.5.0" id("org.jetbrains.changelog") version "2.2.1" id("org.jetbrains.grammarkit") version "2022.3.2.2" apply false idea @@ -60,7 +60,6 @@ tasks { allprojects { idea { module { - isDownloadJavadoc = false isDownloadSources = true } } diff --git a/cidr/build.gradle.kts b/cidr/build.gradle.kts index ebeabea3..7cef7e43 100644 --- a/cidr/build.gradle.kts +++ b/cidr/build.gradle.kts @@ -14,6 +14,8 @@ sourceSets["main"].resources.srcDir(genOutputDir) tasks { register("downloadProps") { + onlyIfModified(true) + useETag(true) src("https://falsepattern.com/zigbrains/msvc.properties") dest(genOutputDir.map { it.file("msvc.properties") }) } @@ -27,7 +29,9 @@ dependencies { create(IntelliJPlatformType.CLion, clionVersion, useInstaller = useInstaller) bundledPlugins("com.intellij.clion", "com.intellij.cidr.base", "com.intellij.nativeDebug") } - implementation(project(":core")) + implementation(project(":core")) { + isTransitive = false + } implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.debug:$lsp4jVersion") { exclude("org.eclipse.lsp4j", "org.eclipse.lsp4j") exclude("org.eclipse.lsp4j", "org.eclipse.lsp4j.jsonrpc") diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/execution/binary/ZigProfileStateBinary.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/execution/binary/ZigProfileStateBinary.kt index 4f73207f..3da81d9c 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/execution/binary/ZigProfileStateBinary.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/execution/binary/ZigProfileStateBinary.kt @@ -24,14 +24,14 @@ package com.falsepattern.zigbrains.debugger.execution.binary import com.falsepattern.zigbrains.debugger.ZigDebugBundle import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.runners.ExecutionEnvironment import kotlin.io.path.pathString class ZigProfileStateBinary(environment: ExecutionEnvironment, configuration: ZigExecConfigBinary) : ZigProfileState(environment, configuration) { - override suspend fun getCommandLine(toolchain: AbstractZigToolchain, debug: Boolean): GeneralCommandLine { + override suspend fun getCommandLine(toolchain: ZigToolchain, debug: Boolean): GeneralCommandLine { val cli = GeneralCommandLine() val cfg = configuration cfg.workingDirectory.path?.let { cli.withWorkingDirectory(it) } diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/PreLaunchProcessListener.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/PreLaunchProcessListener.kt index 92b64c96..6e53c045 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/PreLaunchProcessListener.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/PreLaunchProcessListener.kt @@ -22,10 +22,7 @@ package com.falsepattern.zigbrains.debugger.runner.base -import com.falsepattern.zigbrains.project.run.ZigProcessHandler import com.falsepattern.zigbrains.shared.cli.startIPCAwareProcess -import com.falsepattern.zigbrains.shared.ipc.IPCUtil -import com.falsepattern.zigbrains.shared.ipc.ipc import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.ProcessEvent diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugEmitBinaryInstaller.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugEmitBinaryInstaller.kt index 1f34ca3c..c73c0243 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugEmitBinaryInstaller.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugEmitBinaryInstaller.kt @@ -23,7 +23,7 @@ package com.falsepattern.zigbrains.debugger.runner.base import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.PtyCommandLine @@ -34,7 +34,7 @@ import java.io.File class ZigDebugEmitBinaryInstaller>( private val profileState: ProfileState, - private val toolchain: AbstractZigToolchain, + private val toolchain: ZigToolchain, private val executableFile: File, private val exeArgs: List ): Installer { diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersBase.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersBase.kt index 038c564a..567a2700 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersBase.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersBase.kt @@ -23,7 +23,7 @@ package com.falsepattern.zigbrains.debugger.runner.base import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.util.system.CpuArch import com.jetbrains.cidr.ArchitectureType import com.jetbrains.cidr.execution.RunParameters @@ -31,7 +31,7 @@ import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration abstract class ZigDebugParametersBase>( private val driverConfiguration: DebuggerDriverConfiguration, - protected val toolchain: AbstractZigToolchain, + protected val toolchain: ZigToolchain, protected val profileState: ProfileState ): RunParameters() { override fun getDebuggerDriverConfiguration(): DebuggerDriverConfiguration { diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersEmitBinaryBase.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersEmitBinaryBase.kt index 55bca424..b1e945a8 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersEmitBinaryBase.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersEmitBinaryBase.kt @@ -24,7 +24,7 @@ package com.falsepattern.zigbrains.debugger.runner.base import com.falsepattern.zigbrains.debugger.ZigDebugBundle import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.intellij.openapi.util.io.FileUtil import com.intellij.platform.util.progress.withProgressText @@ -39,7 +39,7 @@ import kotlin.io.path.isExecutable abstract class ZigDebugParametersEmitBinaryBase>( driverConfiguration: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain, + toolchain: ZigToolchain, profileState: ProfileState, ) : ZigDebugParametersBase(driverConfiguration, toolchain, profileState), PreLaunchAware { @Volatile diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugRunnerBase.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugRunnerBase.kt index 7f176753..9e9cd863 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugRunnerBase.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugRunnerBase.kt @@ -26,7 +26,7 @@ import com.falsepattern.zigbrains.debugbridge.ZigDebuggerDriverConfigurationProv import com.falsepattern.zigbrains.debugger.ZigLocalDebugProcess import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.run.ZigProgramRunner -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.intellij.execution.DefaultExecutionResult @@ -52,7 +52,7 @@ abstract class ZigDebugRunnerBase> : ZigProgra @Throws(ExecutionException::class) override suspend fun execute( state: ProfileState, - toolchain: AbstractZigToolchain, + toolchain: ZigToolchain, environment: ExecutionEnvironment ): RunContentDescriptor? { val project = environment.project @@ -67,7 +67,7 @@ abstract class ZigDebugRunnerBase> : ZigProgra @Throws(ExecutionException::class) private suspend fun executeWithDriver( state: ProfileState, - toolchain: AbstractZigToolchain, + toolchain: ZigToolchain, environment: ExecutionEnvironment, debuggerDriver: DebuggerDriverConfiguration ): RunContentDescriptor? { @@ -113,7 +113,7 @@ abstract class ZigDebugRunnerBase> : ZigProgra protected abstract fun getDebugParameters( state: ProfileState, debuggerDriver: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain + toolchain: ZigToolchain ): ZigDebugParametersBase private class SharedConsoleBuilder(private val console: ConsoleView) : TextConsoleBuilder() { diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugParametersBinary.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugParametersBinary.kt index 95620485..887b8cf6 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugParametersBinary.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugParametersBinary.kt @@ -26,13 +26,13 @@ import com.falsepattern.zigbrains.debugger.ZigDebugBundle import com.falsepattern.zigbrains.debugger.execution.binary.ZigProfileStateBinary import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.jetbrains.cidr.execution.Installer import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration -class ZigDebugParametersBinary @Throws(ExecutionException::class) constructor(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateBinary) : +class ZigDebugParametersBinary @Throws(ExecutionException::class) constructor(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateBinary) : ZigDebugParametersBase(driverConfiguration, toolchain, profileState) { private val executableFile = profileState.configuration.exePath.path?.toFile() ?: throw ExecutionException(ZigDebugBundle.message("exception.missing-exe-path")) override fun getInstaller(): Installer { diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugRunnerBinary.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugRunnerBinary.kt index 6da574e4..34f7115a 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugRunnerBinary.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugRunnerBinary.kt @@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.execution.binary.ZigProfileStateBinar import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.execution.configurations.RunProfile import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration @@ -36,7 +36,7 @@ class ZigDebugRunnerBinary: ZigDebugRunnerBase() { override fun getDebugParameters( state: ZigProfileStateBinary, debuggerDriver: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain + toolchain: ZigToolchain ): ZigDebugParametersBase { return ZigDebugParametersBinary(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) } diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugParametersBuild.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugParametersBuild.kt index aa9e2e38..6dcda7be 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugParametersBuild.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugParametersBuild.kt @@ -28,7 +28,7 @@ import com.falsepattern.zigbrains.debugger.runner.base.PreLaunchProcessListener import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase import com.falsepattern.zigbrains.project.execution.build.ZigProfileStateBuild -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.intellij.openapi.util.SystemInfo import com.intellij.platform.util.progress.withProgressText @@ -46,7 +46,7 @@ import kotlin.io.path.isRegularFile class ZigDebugParametersBuild( driverConfiguration: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain, + toolchain: ZigToolchain, profileState: ZigProfileStateBuild ) : ZigDebugParametersBase(driverConfiguration, toolchain, profileState), PreLaunchAware { @Volatile diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugRunnerBuild.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugRunnerBuild.kt index 575ab657..97f99cff 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugRunnerBuild.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugRunnerBuild.kt @@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.build.ZigExecConfigBuild import com.falsepattern.zigbrains.project.execution.build.ZigProfileStateBuild -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.execution.configurations.RunProfile import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration @@ -36,7 +36,7 @@ class ZigDebugRunnerBuild: ZigDebugRunnerBase() { override fun getDebugParameters( state: ZigProfileStateBuild, debuggerDriver: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain + toolchain: ZigToolchain ): ZigDebugParametersBase { return ZigDebugParametersBuild(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) } diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugParametersRun.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugParametersRun.kt index 32558668..01f9809e 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugParametersRun.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugParametersRun.kt @@ -25,11 +25,11 @@ package com.falsepattern.zigbrains.debugger.runner.run import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersEmitBinaryBase import com.falsepattern.zigbrains.project.execution.run.ZigProfileStateRun -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.jetbrains.cidr.execution.Installer import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration -class ZigDebugParametersRun(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateRun) : +class ZigDebugParametersRun(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateRun) : ZigDebugParametersEmitBinaryBase(driverConfiguration, toolchain, profileState) { override fun getInstaller(): Installer { return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.argsSplit()) diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugRunnerRun.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugRunnerRun.kt index 9cc440d8..492492e0 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugRunnerRun.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugRunnerRun.kt @@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.run.ZigExecConfigRun import com.falsepattern.zigbrains.project.execution.run.ZigProfileStateRun -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.execution.configurations.RunProfile import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration @@ -36,7 +36,7 @@ class ZigDebugRunnerRun: ZigDebugRunnerBase() { override fun getDebugParameters( state: ZigProfileStateRun, debuggerDriver: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain + toolchain: ZigToolchain ): ZigDebugParametersBase { return ZigDebugParametersRun(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) } diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugParametersTest.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugParametersTest.kt index 7f45423c..9044196e 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugParametersTest.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugParametersTest.kt @@ -25,11 +25,11 @@ package com.falsepattern.zigbrains.debugger.runner.test import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersEmitBinaryBase import com.falsepattern.zigbrains.project.execution.test.ZigProfileStateTest -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.jetbrains.cidr.execution.Installer import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration -class ZigDebugParametersTest(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateTest) : +class ZigDebugParametersTest(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateTest) : ZigDebugParametersEmitBinaryBase(driverConfiguration, toolchain, profileState) { override fun getInstaller(): Installer { return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, listOf()) diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugRunnerTest.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugRunnerTest.kt index f4673d6d..f01a98c7 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugRunnerTest.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugRunnerTest.kt @@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.test.ZigExecConfigTest import com.falsepattern.zigbrains.project.execution.test.ZigProfileStateTest -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.execution.configurations.RunProfile import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration @@ -36,7 +36,7 @@ class ZigDebugRunnerTest: ZigDebugRunnerBase() { override fun getDebugParameters( state: ZigProfileStateTest, debuggerDriver: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain + toolchain: ZigToolchain ): ZigDebugParametersBase { return ZigDebugParametersTest(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) } diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt index c6f01bf7..a219eef4 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt @@ -23,6 +23,7 @@ package com.falsepattern.zigbrains.debugger.toolchain import com.falsepattern.zigbrains.debugger.ZigDebugBundle +import com.falsepattern.zigbrains.shared.Unarchiver import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.application.PathManager @@ -34,20 +35,19 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.platform.util.progress.reportSequentialProgress import com.intellij.ui.BrowserHyperlinkListener import com.intellij.ui.HyperlinkLabel import com.intellij.ui.components.JBPanel import com.intellij.util.application import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.download.DownloadableFileService -import com.intellij.util.io.Decompressor import com.intellij.util.system.CpuArch import com.intellij.util.system.OS import com.jetbrains.cidr.execution.debugger.CidrDebuggerPathManager import com.jetbrains.cidr.execution.debugger.backend.bin.UrlProvider import com.jetbrains.cidr.execution.debugger.backend.lldb.LLDBDriverConfiguration import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import java.io.IOException import java.net.URL @@ -168,7 +168,9 @@ class ZigDebuggerToolchainService { } try { - downloadAndUnArchive(baseDir, downloadableBinaries) + withContext(Dispatchers.IO) { + downloadAndUnArchive(baseDir, downloadableBinaries) + } return DownloadResult.Ok(baseDir) } catch (e: IOException) { //TODO logging @@ -207,34 +209,40 @@ class ZigDebuggerToolchainService { @Throws(IOException::class) @RequiresEdt private suspend fun downloadAndUnArchive(baseDir: Path, binariesToDownload: List) { - val service = DownloadableFileService.getInstance() + reportSequentialProgress { reporter -> + val service = DownloadableFileService.getInstance() - val downloadDir = baseDir.toFile() - downloadDir.deleteRecursively() + val downloadDir = baseDir.toFile() + downloadDir.deleteRecursively() - val descriptions = binariesToDownload.map { - service.createFileDescription(it.url, fileName(it.url)) - } - - val downloader = service.createDownloader(descriptions, "Debugger downloading") - val downloadDirectory = downloadPath().toFile() - val downloadResults = withContext(Dispatchers.IO) { - coroutineToIndicator { - downloader.download(downloadDirectory) + val descriptions = binariesToDownload.map { + service.createFileDescription(it.url, fileName(it.url)) } - } - val versions = Properties() - for (result in downloadResults) { - val downloadUrl = result.second.downloadUrl - val binaryToDownload = binariesToDownload.first { it.url == downloadUrl } - val propertyName = binaryToDownload.propertyName - val archiveFile = result.first - Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix) - archiveFile.delete() - versions[propertyName] = binaryToDownload.version - } - saveVersionsFile(baseDir, versions) + val downloader = service.createDownloader(descriptions, "Debugger downloading") + val downloadDirectory = downloadPath().toFile() + val downloadResults = reporter.sizedStep(100) { + coroutineToIndicator { + downloader.download(downloadDirectory) + } + } + val versions = Properties() + for (result in downloadResults) { + val downloadUrl = result.second.downloadUrl + val binaryToDownload = binariesToDownload.first { it.url == downloadUrl } + val propertyName = binaryToDownload.propertyName + val archiveFile = result.first + reporter.indeterminateStep { + coroutineToIndicator { + Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix) + } + } + archiveFile.delete() + versions[propertyName] = binaryToDownload.version + } + + saveVersionsFile(baseDir, versions) + } } private fun lldbUrls(): Pair? { @@ -329,38 +337,6 @@ class ZigDebuggerToolchainService { } } - private enum class Unarchiver { - ZIP { - override val extension = "zip" - override fun createDecompressor(file: Path) = Decompressor.Zip(file) - }, - TAR { - override val extension = "tar.gz" - override fun createDecompressor(file: Path) = Decompressor.Tar(file) - }, - VSIX { - override val extension = "vsix" - override fun createDecompressor(file: Path) = Decompressor.Zip(file) - }; - - protected abstract val extension: String - protected abstract fun createDecompressor(file: Path): Decompressor - - companion object { - @Throws(IOException::class) - suspend fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) { - runInterruptible { - val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } ?: error("Unexpected archive type: $archivePath") - val dec = unarchiver.createDecompressor(archivePath) - if (prefix != null) { - dec.removePrefixPath(prefix) - } - dec.extract(dst) - } - } - } - } - sealed class DownloadResult { class Ok(val baseDir: Path): DownloadResult() data object NoUrls: DownloadResult() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt index b51e5b7f..973e7b01 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt @@ -22,10 +22,6 @@ package com.falsepattern.zigbrains -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider import com.intellij.ide.BrowserUtil import com.intellij.ide.plugins.PluginManager import com.intellij.notification.Notification @@ -37,10 +33,8 @@ import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity -import com.intellij.openapi.util.UserDataHolderBase import java.lang.reflect.Constructor import java.lang.reflect.Method -import kotlin.io.path.pathString class ZBStartup: ProjectActivity { var firstInit = true @@ -73,19 +67,6 @@ class ZBStartup: ProjectActivity { notif.notify(null) } } - //Autodetection - val zigProjectState = project.zigProjectSettings.state - if (zigProjectState.toolchainPath.isNullOrBlank()) { - val data = UserDataHolderBase() - data.putUserData(LocalZigToolchain.DIRENV_KEY, - DirenvCmd.direnvInstalled() && !project.isDefault && zigProjectState.direnv - ) - val tc = ZigToolchainProvider.suggestToolchain(project, data) ?: return - if (tc is LocalZigToolchain) { - zigProjectState.toolchainPath = tc.location.pathString - project.zigProjectSettings.state = zigProjectState - } - } } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvCmd.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt similarity index 59% rename from core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvCmd.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt index 6c361bae..a56cfa2b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvCmd.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt @@ -29,23 +29,53 @@ import com.intellij.ide.impl.isTrusted import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.notification.Notifications +import com.intellij.openapi.components.* import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.UserDataHolder +import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.platform.util.progress.withProgressText import com.intellij.util.io.awaitExit +import com.intellij.util.xmlb.annotations.Attribute import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.nio.file.Path +import kotlin.io.path.isRegularFile -object DirenvCmd { - suspend fun importDirenv(project: Project): Env { - if (!direnvInstalled() || !project.isTrusted()) - return emptyEnv - val workDir = project.guessProjectDir()?.toNioPath() ?: return emptyEnv +@Service(Service.Level.PROJECT) +@State( + name = "Direnv", + storages = [Storage("zigbrains.xml")] +) +class DirenvService(val project: Project): SerializablePersistentStateComponent(State()), IDirenvService { + private val mutex = Mutex() - val runOutput = run(project, workDir, "export", "json") + override val isInstalled: Boolean by lazy { + // Using the builtin stuff here instead of Env because it should only scan for direnv on the process path + PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("direnv") != null + } + + var isEnabledRaw: DirenvState + get() = state.enabled + set(value) { + updateState { + it.copy(enabled = value) + } + } + + override val isEnabled: DirenvState + get() = isEnabledRaw + + override suspend fun import(): Env { + if (!isInstalled || !project.isTrusted() || project.isDefault) + return Env.empty + val workDir = project.guessProjectDir()?.toNioPath() ?: return Env.empty + + val runOutput = run(workDir, "export", "json") if (runOutput.error) { if (runOutput.output.contains("is blocked")) { Notifications.Bus.notify(Notification( @@ -54,7 +84,7 @@ object DirenvCmd { ZigBrainsBundle.message("notification.content.direnv-blocked"), NotificationType.ERROR )) - return emptyEnv + return Env.empty } else { Notifications.Bus.notify(Notification( GROUP_DISPLAY_ID, @@ -62,22 +92,22 @@ object DirenvCmd { ZigBrainsBundle.message("notification.content.direnv-error", runOutput.output), NotificationType.ERROR )) - return emptyEnv + return Env.empty } } return if (runOutput.output.isBlank()) { - emptyEnv + Env.empty } else { Env(Json.decodeFromString>(runOutput.output)) } } - private suspend fun run(project: Project, workDir: Path, vararg args: String): DirenvOutput { + private suspend fun run(workDir: Path, vararg args: String): DirenvOutput { val cli = GeneralCommandLine("direnv", *args).withWorkingDirectory(workDir) val (process, exitCode) = withProgressText("Running ${cli.commandLineString}") { withContext(Dispatchers.IO) { - project.direnvService.mutex.withLock { + mutex.withLock { val process = cli.createProcess() val exitCode = process.awaitExit() process to exitCode @@ -94,17 +124,39 @@ object DirenvCmd { return DirenvOutput(stdOut, false) } - private const val GROUP_DISPLAY_ID = "zigbrains-direnv" - - private val _direnvInstalled by lazy { - // Using the builtin stuff here instead of Env because it should only scan for direnv on the process path - PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("direnv") != null + fun hasDotEnv(): Boolean { + if (!isInstalled) + return false + val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return false + return envFiles.any { projectDir.resolve(it).isRegularFile() } + } + + data class State( + @JvmField + @Attribute + var enabled: DirenvState = DirenvState.Auto + ) + + companion object { + private const val GROUP_DISPLAY_ID = "zigbrains-direnv" + fun getInstance(project: Project): IDirenvService = project.service() + + private val STATE_KEY = Key.create("DIRENV_STATE") + + fun getStateFor(data: UserDataHolder?, project: Project?): DirenvState { + return data?.getUserData(STATE_KEY) ?: project?.let { getInstance(project).isEnabled } ?: DirenvState.Disabled + } + + fun setStateFor(data: UserDataHolder, state: DirenvState) { + data.putUserData(STATE_KEY, state) + } } - fun direnvInstalled() = _direnvInstalled } -suspend fun Project?.getDirenv(): Env { - if (this == null) - return emptyEnv - return DirenvCmd.importDirenv(this) -} \ No newline at end of file +sealed interface IDirenvService { + val isInstalled: Boolean + val isEnabled: DirenvState + suspend fun import(): Env +} + +private val envFiles = listOf(".envrc", ".env") \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvProjectService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvState.kt similarity index 77% rename from core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvProjectService.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvState.kt index fec31d4a..f4c96608 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvProjectService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvState.kt @@ -22,14 +22,19 @@ package com.falsepattern.zigbrains.direnv -import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.project.Project -import kotlinx.coroutines.sync.Mutex -@Service(Service.Level.PROJECT) -class DirenvProjectService { - val mutex = Mutex() -} +enum class DirenvState { + Auto, + Enabled, + Disabled; -val Project.direnvService get() = service() \ No newline at end of file + fun isEnabled(project: Project?): Boolean { + return when(this) { + Enabled -> true + Disabled -> false + Auto -> project?.service()?.hasDotEnv() == true + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt index 4d695819..e94c8194 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt @@ -25,8 +25,9 @@ package com.falsepattern.zigbrains.direnv import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.util.EnvironmentUtil -import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import org.jetbrains.annotations.NonNls import java.io.File import kotlin.io.path.absolute @@ -34,14 +35,13 @@ import kotlin.io.path.isDirectory import kotlin.io.path.isExecutable import kotlin.io.path.isRegularFile +@JvmRecord data class Env(val env: Map) { private val path get() = getVariable("PATH")?.split(File.pathSeparatorChar) private fun getVariable(name: @NonNls String) = env.getOrElse(name) { EnvironmentUtil.getValue(name) } - suspend fun findExecutableOnPATH(exe: @NonNls String) = findAllExecutablesOnPATH(exe).firstOrNull() - fun findAllExecutablesOnPATH(exe: @NonNls String) = flow { val exeName = if (SystemInfo.isWindows) "$exe.exe" else exe val paths = path ?: return@flow @@ -54,7 +54,9 @@ data class Env(val env: Map) { continue emit(exePath) } + }.flowOn(Dispatchers.IO) + + companion object { + val empty = Env(emptyMap()) } } - -val emptyEnv = Env(emptyMap()) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt new file mode 100644 index 00000000..dfe3ee1f --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt @@ -0,0 +1,101 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.direnv.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.direnv.DirenvService +import com.falsepattern.zigbrains.direnv.DirenvState +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY +import com.falsepattern.zigbrains.shared.SubConfigurable +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.dsl.builder.Panel +import java.awt.event.ItemEvent + +abstract class DirenvEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): SubConfigurable { + private var cb: ComboBox? = null + override fun attach(panel: Panel): Unit = with(panel) { + row(ZigBrainsBundle.message("settings.direnv.enable.label")) { + comboBox(DirenvState.entries).component.let { + cb = it + if (sharedState != null) { + it.addItemListener { e -> + if (e.stateChange != ItemEvent.SELECTED) + return@addItemListener + val item = e.item + if (item !is DirenvState) + return@addItemListener + DirenvService.setStateFor(sharedState, item) + } + } + } + } + } + + override fun isModified(context: T): Boolean { + return isEnabled(context) != cb?.selectedItem as DirenvState + } + + override fun apply(context: T) { + setEnabled(context, cb?.selectedItem as DirenvState) + } + + override fun reset(context: T?) { + if (context == null) { + cb?.selectedItem = DirenvState.Auto + return + } + cb?.selectedItem = isEnabled(context) + } + + override fun dispose() { + } + + abstract fun isEnabled(context: T): DirenvState + abstract fun setEnabled(context: T, value: DirenvState) + + class ForProject(sharedState: ZigProjectConfigurationProvider.IUserDataBridge) : DirenvEditor(sharedState) { + override fun isEnabled(context: Project): DirenvState { + return DirenvService.getInstance(context).isEnabled + } + + override fun setEnabled(context: Project, value: DirenvState) { + context.service().isEnabledRaw = value + } + } + + class Provider: ZigProjectConfigurationProvider { + override fun create(sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { + if (sharedState.getUserData(PROJECT_KEY)?.isDefault != false) { + return null + } + DirenvService.setStateFor(sharedState, DirenvState.Auto) + return ForProject(sharedState) + } + + override val index: Int + get() = 100 + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/ZigConsoleBuilder.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/ZigConsoleBuilder.kt index e46a415f..df3d0f31 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/ZigConsoleBuilder.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/ZigConsoleBuilder.kt @@ -26,7 +26,6 @@ import com.intellij.execution.filters.TextConsoleBuilderImpl import com.intellij.execution.ui.ConsoleView import com.intellij.openapi.project.Project import com.intellij.terminal.TerminalExecutionConsole -import java.nio.file.Path class ZigConsoleBuilder(private val project: Project, private val emulateTerminal: Boolean = false): TextConsoleBuilderImpl(project) { override fun createConsole(): ConsoleView { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/Configuration.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/Configuration.kt index 15e177a0..0baf28ff 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/Configuration.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/Configuration.kt @@ -24,15 +24,12 @@ package com.falsepattern.zigbrains.project.execution.base import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.execution.base.ZigConfigurable.ZigConfigModule -import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.shared.cli.translateCommandline import com.falsepattern.zigbrains.shared.element.* import com.intellij.openapi.Disposable import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.options.SettingsEditor -import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox -import com.intellij.openapi.ui.TextBrowseFolderListener import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.ui.components.JBCheckBox diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt index ee148595..4fc2a466 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt @@ -22,9 +22,7 @@ package com.falsepattern.zigbrains.project.execution.base -import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.direnv.DirenvService import com.intellij.execution.ExecutionException import com.intellij.execution.Executor import com.intellij.execution.configurations.ConfigurationFactory @@ -65,8 +63,9 @@ abstract class ZigExecConfig>(project: Project, factory: Con suspend fun patchCommandLine(commandLine: GeneralCommandLine): GeneralCommandLine { - if (project.zigProjectSettings.state.direnv) { - commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) + val direnv = DirenvService.getInstance(project) + if (direnv.isEnabled.isEnabled(project)) { + commandLine.withEnvironment(direnv.import().env) } return commandLine } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt index 251ca441..4e13a161 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt @@ -24,28 +24,17 @@ package com.falsepattern.zigbrains.project.execution.base import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.execution.ZigConsoleBuilder -import com.falsepattern.zigbrains.project.run.ZigProcessHandler -import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.cli.startIPCAwareProcess import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking -import com.falsepattern.zigbrains.shared.ipc.IPCUtil -import com.falsepattern.zigbrains.shared.ipc.ipc -import com.intellij.build.BuildTextConsoleView -import com.intellij.execution.DefaultExecutionResult import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.CommandLineState import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.PtyCommandLine -import com.intellij.execution.filters.TextConsoleBuilder import com.intellij.execution.process.ProcessHandler -import com.intellij.execution.process.ProcessTerminatedListener import com.intellij.execution.runners.ExecutionEnvironment -import com.intellij.openapi.project.Project import com.intellij.platform.ide.progress.ModalTaskOwner -import com.intellij.terminal.TerminalExecutionConsole -import com.intellij.util.system.OS -import kotlin.collections.contains import kotlin.io.path.pathString abstract class ZigProfileState> ( @@ -66,12 +55,12 @@ abstract class ZigProfileState> ( @Throws(ExecutionException::class) suspend fun startProcessSuspend(): ProcessHandler { - val toolchain = environment.project.zigProjectSettings.state.toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain")) + val toolchain = ZigToolchainService.getInstance(environment.project).toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain")) return getCommandLine(toolchain, false).startIPCAwareProcess(environment.project, emulateTerminal = true) } @Throws(ExecutionException::class) - open suspend fun getCommandLine(toolchain: AbstractZigToolchain, debug: Boolean): GeneralCommandLine { + open suspend fun getCommandLine(toolchain: ZigToolchain, debug: Boolean): GeneralCommandLine { val workingDir = configuration.workingDirectory val zigExePath = toolchain.zig.path() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/module/ZigModuleBuilder.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/module/ZigModuleBuilder.kt index 0cce0af7..3cd8a511 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/module/ZigModuleBuilder.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/module/ZigModuleBuilder.kt @@ -50,7 +50,7 @@ class ZigModuleBuilder: ModuleBuilder() { override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?): ModuleWizardStep? { val step = ZigModuleWizardStep(parentDisposable) - parentDisposable?.let { Disposer.register(it, step.peer) } + parentDisposable?.let { Disposer.register(it) { step.peer.dispose() } } return step } @@ -65,14 +65,14 @@ class ZigModuleBuilder: ModuleBuilder() { } inner class ZigModuleWizardStep(parent: Disposable?): ModuleWizardStep() { - internal val peer = ZigProjectGeneratorPeer(true).also { Disposer.register(parent ?: return@also, it) } + internal val peer = ZigProjectGeneratorPeer(true).also { Disposer.register(parent ?: return@also) {it.dispose()} } override fun getComponent(): JComponent { return peer.component.withBorder() } override fun disposeUIResources() { - Disposer.dispose(peer) + Disposer.dispose(peer.newProjectPanel) } override fun updateDataModel() { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt index ac1abd24..2f0cc95e 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt @@ -39,9 +39,9 @@ import com.intellij.util.ui.JBUI import javax.swing.JList import javax.swing.ListSelectionModel -class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProjectConfigurationProvider.SettingsPanelHolder { +class ZigNewProjectPanel(private var handleGit: Boolean): Disposable { private val git = JBCheckBox() - override val panels = ZigProjectConfigurationProvider.createNewProjectSettingsPanels(this).onEach { Disposer.register(this, it) } + val panels = ZigProjectConfigurationProvider.createPanels(null).onEach { Disposer.register(this, it) } private val templateList = JBList(JBList.createDefaultListModel(defaultTemplates)).apply { selectionMode = ListSelectionModel.SINGLE_SELECTION selectedIndex = 0 @@ -64,7 +64,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject fun getData(): ZigProjectConfigurationData { val selectedTemplate = templateList.selectedValue - return ZigProjectConfigurationData(handleGit && git.isSelected, panels.map { it.data }, selectedTemplate) + return ZigProjectConfigurationData(handleGit && git.isSelected, panels, selectedTemplate) } fun attach(p: Panel): Unit = with(p) { @@ -73,6 +73,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject cell(git) } } + panels.filter { it.newProjectBeforeInitSelector }.forEach { it.attach(p) } group("Zig Project Template") { row { resizableRow() @@ -81,7 +82,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject .align(AlignY.FILL) } } - panels.forEach { it.attach(p) } + panels.filter { !it.newProjectBeforeInitSelector }.forEach { it.attach(p) } } override fun dispose() { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectConfigurationData.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectConfigurationData.kt index ab425caf..1658b801 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectConfigurationData.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectConfigurationData.kt @@ -22,9 +22,10 @@ package com.falsepattern.zigbrains.project.newproject -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.template.ZigInitTemplate import com.falsepattern.zigbrains.project.template.ZigProjectTemplate +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.notification.Notification import com.intellij.notification.NotificationType @@ -42,7 +43,7 @@ import kotlinx.coroutines.launch @JvmRecord data class ZigProjectConfigurationData( val git: Boolean, - val conf: List, + val conf: List>, val selectedTemplate: ZigProjectTemplate ) { @RequiresBackgroundThread @@ -54,9 +55,7 @@ data class ZigProjectConfigurationData( if (!reporter.indeterminateStep("Initializing project") { if (template is ZigInitTemplate) { - val toolchain = conf - .mapNotNull { it as? ZigProjectConfigurationProvider.ToolchainProvider } - .firstNotNullOfOrNull { it.toolchain } ?: run { + val toolchain = ZigToolchainService.getInstance(project).toolchain ?: run { Notification( "zigbrains", "Tried to generate project with zig init, but zig toolchain is invalid", diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectGeneratorPeer.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectGeneratorPeer.kt index 41db4c5f..209d7876 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectGeneratorPeer.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectGeneratorPeer.kt @@ -23,16 +23,14 @@ package com.falsepattern.zigbrains.project.newproject import com.intellij.ide.util.projectWizard.SettingsStep -import com.intellij.openapi.Disposable import com.intellij.openapi.ui.ValidationInfo -import com.intellij.openapi.util.Disposer import com.intellij.platform.ProjectGeneratorPeer import com.intellij.ui.dsl.builder.panel import javax.swing.JComponent -class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer, Disposable { - private val newProjectPanel by lazy { - ZigNewProjectPanel(handleGit).also { Disposer.register(this, it) } +class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer { + val newProjectPanel by lazy { + ZigNewProjectPanel(handleGit) } private val myComponent: JComponent by lazy { panel { @@ -59,6 +57,7 @@ class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer>(protected val val state = castProfileState(baseState) ?: return null - val toolchain = environment.project.zigProjectSettings.state.toolchain ?: run { + val toolchain = ZigToolchainService.getInstance(environment.project).toolchain ?: run { Notification( "zigbrains", "Zig project toolchain not set, cannot execute program! Please configure it in [Settings | Languages & Frameworks | Zig]", @@ -81,5 +81,5 @@ abstract class ZigProgramRunner>(protected val protected abstract fun castProfileState(state: ZigProfileState<*>): ProfileState? @Throws(ExecutionException::class) - abstract suspend fun execute(state: ProfileState, toolchain: AbstractZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? + abstract suspend fun execute(state: ProfileState, toolchain: ZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt index 771e94d2..2601ad9b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt @@ -24,7 +24,7 @@ package com.falsepattern.zigbrains.project.run import com.falsepattern.zigbrains.project.execution.base.ZigExecConfig import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.intellij.execution.configurations.RunProfile import com.intellij.execution.executors.DefaultRunExecutor @@ -32,10 +32,13 @@ import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.runners.RunContentBuilder import com.intellij.execution.ui.RunContentDescriptor import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.progress.blockingContext class ZigRegularRunner: ZigProgramRunner>(DefaultRunExecutor.EXECUTOR_ID) { - override suspend fun execute(state: ZigProfileState<*>, toolchain: AbstractZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? { - val exec = state.execute(environment.executor, this) + override suspend fun execute(state: ZigProfileState<*>, toolchain: ZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? { + val exec = blockingContext { + state.execute(environment.executor, this) + } return withEDTContext(ModalityState.any()) { val runContentBuilder = RunContentBuilder(exec, environment) runContentBuilder.showRunContent(null) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt index e16b058f..57b53003 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt @@ -22,11 +22,14 @@ package com.falsepattern.zigbrains.project.settings -import com.falsepattern.zigbrains.shared.MultiConfigurable +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.shared.SubConfigurable import com.intellij.openapi.project.Project -class ZigConfigurable(project: Project): MultiConfigurable(ZigProjectConfigurationProvider.createConfigurables(project)) { - override fun getDisplayName(): String { - return "Zig" +class ZigConfigurable(override val context: Project) : SubConfigurable.Adapter() { + override fun instantiate(): List> { + return ZigProjectConfigurationProvider.createPanels(context) } + + override fun getDisplayName() = ZigBrainsBundle.message("settings.project.display-name") } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurable.kt deleted file mode 100644 index ff333f79..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurable.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.settings - -import com.falsepattern.zigbrains.shared.SubConfigurable -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer -import com.intellij.ui.dsl.builder.Panel - -class ZigProjectConfigurable(private val project: Project): SubConfigurable { - private var settingsPanel: ZigProjectSettingsPanel? = null - override fun createComponent(holder: ZigProjectConfigurationProvider.SettingsPanelHolder, panel: Panel): ZigProjectConfigurationProvider.SettingsPanel { - settingsPanel?.let { Disposer.dispose(it) } - val sp = ZigProjectSettingsPanel(holder, project).apply { attach(panel) }.also { Disposer.register(this, it) } - settingsPanel = sp - return sp - } - - override fun isModified(): Boolean { - return project.zigProjectSettings.isModified(settingsPanel?.data ?: return false) - } - - override fun apply() { - val service = project.zigProjectSettings - val data = settingsPanel?.data ?: return - val modified = service.isModified(data) - service.state = data - if (modified) { - ZigProjectConfigurationProvider.mainConfigChanged(project) - } - } - - override fun reset() { - settingsPanel?.data = project.zigProjectSettings.state - } - - override fun dispose() { - settingsPanel = null - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt index a609a3ee..c93d03f6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt @@ -22,42 +22,56 @@ package com.falsepattern.zigbrains.project.settings -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.shared.SubConfigurable -import com.intellij.openapi.Disposable import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.Panel +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.UserDataHolder +import com.intellij.openapi.util.UserDataHolderBase interface ZigProjectConfigurationProvider { - fun handleMainConfigChanged(project: Project) - fun createConfigurable(project: Project): SubConfigurable - fun createNewProjectSettingsPanel(holder: SettingsPanelHolder): SettingsPanel? - val priority: Int + fun create(sharedState: IUserDataBridge): SubConfigurable? + val index: Int companion object { private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.projectConfigProvider") - fun mainConfigChanged(project: Project) { - EXTENSION_POINT_NAME.extensionList.forEach { it.handleMainConfigChanged(project) } - } - fun createConfigurables(project: Project): List { - return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.map { it.createConfigurable(project) } - } - fun createNewProjectSettingsPanels(holder: SettingsPanelHolder): List { - return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.mapNotNull { it.createNewProjectSettingsPanel(holder) } + val PROJECT_KEY: Key = Key.create("Project") + fun createPanels(project: Project?): List> { + val sharedState = UserDataBridge() + sharedState.putUserData(PROJECT_KEY, project) + return EXTENSION_POINT_NAME.extensionList.sortedBy { it.index }.mapNotNull { it.create(sharedState) } } } - interface SettingsPanel: Disposable { - val data: Settings - fun attach(p: Panel) - fun direnvChanged(state: Boolean) + + interface IUserDataBridge: UserDataHolder { + fun addUserDataChangeListener(listener: UserDataListener) + fun removeUserDataChangeListener(listener: UserDataListener) } - interface SettingsPanelHolder { - val panels: List + + interface UserDataListener { + fun onUserDataChanged(key: Key<*>) } - interface Settings { - fun apply(project: Project) + + class UserDataBridge: UserDataHolderBase(), IUserDataBridge { + private val listeners = ArrayList() + override fun putUserData(key: Key, value: T?) { + super.putUserData(key, value) + synchronized(listeners) { + listeners.forEach { listener -> + listener.onUserDataChanged(key) + } + } + } + + override fun addUserDataChangeListener(listener: UserDataListener) { + synchronized(listeners) { + listeners.add(listener) + } + } + + override fun removeUserDataChangeListener(listener: UserDataListener) { + synchronized(listeners) { + listeners.remove(listener) + } + } } - interface ToolchainProvider { - val toolchain: AbstractZigToolchain? - } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt deleted file mode 100644 index facf092c..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.settings - -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.util.xmlb.annotations.Transient -import kotlin.io.path.isDirectory -import kotlin.io.path.pathString - -data class ZigProjectSettings( - var direnv: Boolean = false, - var overrideStdPath: Boolean = false, - var explicitPathToStd: String? = null, - var toolchainPath: String? = null -): ZigProjectConfigurationProvider.Settings, ZigProjectConfigurationProvider.ToolchainProvider { - override fun apply(project: Project) { - project.zigProjectSettings.loadState(this) - } - - @get:Transient - @set:Transient - override var toolchain: LocalZigToolchain? - get() { - val nioPath = toolchainPath?.toNioPathOrNull() ?: return null - if (!nioPath.isDirectory()) { - return null - } - return LocalZigToolchain(nioPath) - } - set(value) { - toolchainPath = value?.location?.pathString - } -} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt deleted file mode 100644 index 6820c488..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.settings - -import com.falsepattern.zigbrains.project.toolchain.stdlib.ZigSyntheticLibrary -import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.openapi.components.* -import com.intellij.openapi.project.Project -import kotlinx.coroutines.launch - -@Service(Service.Level.PROJECT) -@State( - name = "ZigProjectSettings", - storages = [Storage("zigbrains.xml")] -) -class ZigProjectSettingsService(val project: Project): PersistentStateComponent { - @Volatile - private var state = ZigProjectSettings() - - override fun getState(): ZigProjectSettings { - return state.copy() - } - - fun setState(value: ZigProjectSettings) { - this.state = value - zigCoroutineScope.launch { - ZigSyntheticLibrary.reload(project, value) - } - } - - override fun loadState(state: ZigProjectSettings) { - setState(state) - } - - fun isModified(otherData: ZigProjectSettings): Boolean { - return state != otherData - } -} - -val Project.zigProjectSettings get() = service() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigLibraryRootProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigLibraryRootProvider.kt similarity index 95% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigLibraryRootProvider.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigLibraryRootProvider.kt index e8ce9239..4cceeae4 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigLibraryRootProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigLibraryRootProvider.kt @@ -20,7 +20,7 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain.stdlib +package com.falsepattern.zigbrains.project.stdlib import com.intellij.openapi.project.Project import com.intellij.openapi.roots.AdditionalLibraryRootsProvider diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigSyntheticLibrary.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt similarity index 56% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigSyntheticLibrary.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt index a915a8de..52094e45 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigSyntheticLibrary.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt @@ -20,41 +20,43 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain.stdlib +package com.falsepattern.zigbrains.project.stdlib import com.falsepattern.zigbrains.Icons -import com.falsepattern.zigbrains.project.settings.ZigProjectSettings -import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.navigation.ItemPresentation import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.roots.SyntheticLibrary -import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.refreshAndFindVirtualDirectory import com.intellij.platform.backend.workspace.WorkspaceModel import com.intellij.platform.backend.workspace.toVirtualFileUrl import com.intellij.platform.workspace.jps.entities.* +import com.intellij.project.isDirectoryBased +import com.intellij.project.stateStore import com.intellij.workspaceModel.ide.impl.LegacyBridgeJpsEntitySourceFactory import kotlinx.coroutines.runBlocking import java.util.* import javax.swing.Icon class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresentation { - private var state: ZigProjectSettings = project.zigProjectSettings.state.copy() + private var toolchain: ZigToolchain? = ZigToolchainService.getInstance(project).toolchain private val roots by lazy { - runBlocking {getRoot(state, project)}?.let { setOf(it) } ?: emptySet() + runBlocking {getRoot(toolchain, project)}?.let { setOf(it) } ?: emptySet() } private val name by lazy { - getName(state, project) + getName(toolchain, project) } override fun equals(other: Any?): Boolean { if (other !is ZigSyntheticLibrary) return false - return state == other.state + return toolchain == other.toolchain } override fun hashCode(): Int { @@ -73,17 +75,49 @@ class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresen return roots } + override fun isShowInExternalLibrariesNode(): Boolean { + return !roots.isEmpty() + } + companion object { private const val ZIG_LIBRARY_ID = "Zig SDK" - private const val ZIG_MODULE_ID = "Zig" - suspend fun reload(project: Project, state: ZigProjectSettings) { - val moduleId = ModuleId(ZIG_MODULE_ID) + private const val ZIG_MODULE_ID = "ZigBrains" + private val libraryTableId = LibraryTableId.ProjectLibraryTableId + private val libraryId = LibraryId(ZIG_LIBRARY_ID, libraryTableId) + private val moduleId = ModuleId(ZIG_MODULE_ID) + suspend fun reload(project: Project, toolchain: ZigToolchain?) { + val root = getRoot(toolchain, project) + if (root != null) { + add(project, root) + } else { + remove(project) + } + } + + private suspend fun remove(project: Project) { + val workspaceModel = WorkspaceModel.getInstance(project) + workspaceModel.update("Update Zig std") { builder -> + builder.resolve(moduleId)?.let { moduleEntity -> + builder.removeEntity(moduleEntity) + } + builder.resolve(libraryId)?.let { libraryEntity -> + builder.removeEntity(libraryEntity) + } + } + } + + private suspend fun add(project: Project, root: VirtualFile) { val workspaceModel = WorkspaceModel.getInstance(project) - val root = getRoot(state, project) ?: return val libRoot = LibraryRoot(root.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()), LibraryRootTypeId.SOURCES) - val libraryTableId = LibraryTableId.ProjectLibraryTableId - val libraryId = LibraryId(ZIG_LIBRARY_ID, libraryTableId) - val baseModuleDir = project.guessProjectDir()?.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()) ?: return + + var baseModuleDirFile: VirtualFile? = null + if (project.isDirectoryBased) { + baseModuleDirFile = project.stateStore.directoryStorePath?.refreshAndFindVirtualDirectory() + } + if (baseModuleDirFile == null) { + baseModuleDirFile = project.guessProjectDir() + } + val baseModuleDir = baseModuleDirFile?.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()) ?: return workspaceModel.update("Update Zig std") { builder -> builder.resolve(moduleId)?.let { moduleEntity -> builder.removeEntity(moduleEntity) @@ -117,37 +151,39 @@ class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresen } private fun getName( - state: ZigProjectSettings, + toolchain: ZigToolchain?, project: Project ): String { - val tc = state.toolchain ?: return "Zig" - val version = runBlocking { tc.zig.getEnv(project) }.mapCatching { it.version }.getOrElse { return "Zig" } - return "Zig $version" + val tc = toolchain ?: return "Zig" + toolchain.name?.let { return it } + runBlocking { tc.zig.getEnv(project) } + .mapCatching { it.version } + .getOrNull() + ?.let { return "Zig $it" } + return "Zig" } suspend fun getRoot( - state: ZigProjectSettings, + toolchain: ZigToolchain?, project: Project ): VirtualFile? { - val toolchain = state.toolchain - if (state.overrideStdPath) run { - val ePathStr = state.explicitPathToStd ?: return@run - val ePath = ePathStr.toNioPathOrNull() ?: return@run + //TODO universal + if (toolchain !is LocalZigToolchain) { + return null + } + if (toolchain.std != null) run { + val ePath = toolchain.std if (ePath.isAbsolute) { val roots = ePath.refreshAndFindVirtualDirectory() ?: return@run return roots - } else if (toolchain != null) { - val stdPath = toolchain.location.resolve(ePath) - if (stdPath.isAbsolute) { - val roots = stdPath.refreshAndFindVirtualDirectory() ?: return@run - return roots - } + } + val stdPath = toolchain.location.resolve(ePath) + if (stdPath.isAbsolute) { + val roots = stdPath.refreshAndFindVirtualDirectory() ?: return@run + return roots } } - if (toolchain != null) { - val stdPath = toolchain.zig.getEnv(project).mapCatching { it.stdPath(toolchain, project) }.getOrNull() ?: return null - val roots = stdPath.refreshAndFindVirtualDirectory() ?: return null - return roots - } - return null + val stdPath = toolchain.zig.getEnv(project).mapCatching { it.stdPath(toolchain, project) }.getOrNull() ?: return null + val roots = stdPath.refreshAndFindVirtualDirectory() ?: return null + return roots } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt index 320118ec..1bf0fd35 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt @@ -22,8 +22,8 @@ package com.falsepattern.zigbrains.project.steps.discovery -import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.steps.discovery.ZigStepDiscoveryListener.ErrorType +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.Disposable @@ -76,7 +76,7 @@ class ZigStepDiscoveryService(private val project: Project) { private tailrec suspend fun doReload() { preReload() - val toolchain = project.zigProjectSettings.state.toolchain ?: run { + val toolchain = ZigToolchainService.getInstance(project).toolchain ?: run { errorReload(ErrorType.MissingToolchain) return } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt deleted file mode 100644 index a7701ad6..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.toolchain - -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.UserDataHolder -import com.intellij.openapi.util.io.toNioPathOrNull -import kotlinx.serialization.json.* -import kotlin.io.path.pathString - -class LocalZigToolchainProvider: ZigToolchainProvider { - override suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): LocalZigToolchain? { - val env = if (project != null && (extraData.getUserData(LocalZigToolchain.DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) { - DirenvCmd.importDirenv(project) - } else { - emptyEnv - } - val zigExePath = env.findExecutableOnPATH("zig") ?: return null - return LocalZigToolchain(zigExePath.parent) - } - - override val serialMarker: String - get() = "local" - - override fun deserialize(data: JsonElement): LocalZigToolchain? { - if (data !is JsonObject) - return null - - val loc = data["location"] as? JsonPrimitive ?: return null - val path = loc.content.toNioPathOrNull() ?: return null - return LocalZigToolchain(path) - } - - override fun canSerialize(toolchain: AbstractZigToolchain): Boolean { - return toolchain is LocalZigToolchain - } - - override fun serialize(toolchain: LocalZigToolchain): JsonElement { - return buildJsonObject { - put("location", toolchain.location.pathString) - } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt new file mode 100644 index 00000000..fc1c8733 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -0,0 +1,57 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain + +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService.MyState +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.resolve +import com.falsepattern.zigbrains.project.toolchain.base.toRef +import com.falsepattern.zigbrains.shared.UUIDMapSerializable +import com.falsepattern.zigbrains.shared.UUIDStorage +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service + +@Service(Service.Level.APP) +@State( + name = "ZigToolchainList", + storages = [Storage("zigbrains.xml")] +) +class ZigToolchainListService: UUIDMapSerializable.Converting(MyState()) { + override fun serialize(value: ZigToolchain) = value.toRef() + override fun deserialize(value: ZigToolchain.Ref) = value.resolve() + override fun getStorage(state: MyState) = state.toolchains + override fun updateStorage(state: MyState, storage: ToolchainStorage) = state.copy(toolchains = storage) + + data class MyState(@JvmField val toolchains: ToolchainStorage = emptyMap()) + + companion object { + @JvmStatic + fun getInstance(): ZigToolchainListService = service() + } +} + +inline val zigToolchainList: ZigToolchainListService get() = ZigToolchainListService.getInstance() + +private typealias ToolchainStorage = UUIDStorage diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt deleted file mode 100644 index 57aeb51b..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.toolchain - -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider.Companion.EXTENSION_POINT_NAME -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.UserDataHolder -import com.intellij.util.xmlb.Converter -import kotlinx.serialization.json.* - -sealed interface ZigToolchainProvider { - suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): AbstractZigToolchain? - - val serialMarker: String - fun deserialize(data: JsonElement): AbstractZigToolchain? - fun canSerialize(toolchain: AbstractZigToolchain): Boolean - fun serialize(toolchain: T): JsonElement - - companion object { - val EXTENSION_POINT_NAME = ExtensionPointName.create>("com.falsepattern.zigbrains.toolchainProvider") - - suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): AbstractZigToolchain? { - return EXTENSION_POINT_NAME.extensionList.firstNotNullOfOrNull { it.suggestToolchain(project, extraData) } - } - } -} - -@Suppress("UNCHECKED_CAST") -private fun ZigToolchainProvider.serialize(toolchain: AbstractZigToolchain) = serialize(toolchain as T) - -class ZigToolchainConverter: Converter() { - override fun fromString(value: String): AbstractZigToolchain? { - val json = Json.parseToJsonElement(value) as? JsonObject ?: return null - val marker = (json["marker"] as? JsonPrimitive)?.contentOrNull ?: return null - val data = json["data"] ?: return null - val provider = EXTENSION_POINT_NAME.extensionList.find { it.serialMarker == marker } ?: return null - return provider.deserialize(data) - } - - override fun toString(value: AbstractZigToolchain): String? { - val provider = EXTENSION_POINT_NAME.extensionList.find { it.canSerialize(value) } ?: return null - return buildJsonObject { - put("marker", provider.serialMarker) - put("data", provider.serialize(value)) - }.toString() - } - -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt new file mode 100644 index 00000000..3e708be5 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt @@ -0,0 +1,76 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain + +import com.falsepattern.zigbrains.project.stdlib.ZigSyntheticLibrary +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.shared.asUUID +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.application.EDT +import com.intellij.openapi.components.* +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.annotations.Attribute +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.util.* + +@Service(Service.Level.PROJECT) +@State( + name = "ZigToolchain", + storages = [Storage("zigbrains.xml")] +) +class ZigToolchainService(private val project: Project): SerializablePersistentStateComponent(State()) { + var toolchainUUID: UUID? + get() = state.toolchain.ifBlank { null }?.asUUID()?.takeIf { + if (it in zigToolchainList) { + true + } else { + updateState { + it.copy(toolchain = "") + } + false + } + } + set(value) { + updateState { + it.copy(toolchain = value?.toString() ?: "") + } + zigCoroutineScope.launch(Dispatchers.EDT) { + ZigSyntheticLibrary.reload(project, toolchain) + } + } + + val toolchain: ZigToolchain? + get() = toolchainUUID?.let { zigToolchainList[it] } + + data class State( + @JvmField + @Attribute + var toolchain: String = "" + ) + + companion object { + @JvmStatic + fun getInstance(project: Project): ZigToolchainService = project.service() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt new file mode 100644 index 00000000..74c0b44d --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt @@ -0,0 +1,70 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.base + +import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool +import com.falsepattern.zigbrains.shared.NamedObject +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.annotations.Attribute +import java.nio.file.Path + +/** + * These MUST be stateless and interchangeable! (e.g., immutable data class) + */ +interface ZigToolchain: NamedObject { + val zig: ZigCompilerTool get() = ZigCompilerTool(this) + + val extraData: Map + + /** + * Returned type must be the same class + */ + fun withExtraData(map: Map): ZigToolchain + + fun workingDirectory(project: Project? = null): Path? + + suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project? = null): GeneralCommandLine + + fun pathToExecutable(toolName: String, project: Project? = null): Path + + data class Ref( + @JvmField + @Attribute + val marker: String? = null, + @JvmField + val data: Map? = null, + @JvmField + val extraData: Map? = null, + ) +} + +fun T.withExtraData(key: String, value: String?): T { + val newMap = HashMap() + newMap.putAll(extraData.filter { (theKey, _) -> theKey != key}) + if (value != null) { + newMap[key] = value + } + @Suppress("UNCHECKED_CAST") + return withExtraData(newMap) as T +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt new file mode 100644 index 00000000..abbd34f7 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt @@ -0,0 +1,103 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.base + +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.NlsContexts +import com.intellij.ui.dsl.builder.panel +import java.util.* +import java.util.function.Supplier +import javax.swing.JComponent + +abstract class ZigToolchainConfigurable( + val uuid: UUID, + tc: T, + val data: ZigProjectConfigurationProvider.IUserDataBridge?, + val modal: Boolean +): NamedConfigurable() { + var toolchain: T = tc + set(value) { + zigToolchainList[uuid] = value + field = value + } + + init { + data?.putUserData(TOOLCHAIN_KEY, Supplier{toolchain}) + } + private var myViews: List> = emptyList() + + abstract fun createPanel(): ImmutableElementPanel + + override fun createOptionsPanel(): JComponent? { + var views = myViews + if (views.isEmpty()) { + views = ArrayList>() + views.add(createPanel()) + views.addAll(createZigToolchainExtensionPanels(data, if (modal) PanelState.ModalEditor else PanelState.ListEditor)) + myViews = views + } + val p = panel { + views.forEach { it.attach(this@panel) } + }.withMinimumWidth(20) + views.forEach { it.reset(toolchain) } + return p + } + + override fun getEditableObject(): UUID? { + return uuid + } + + override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? { + return displayName + } + + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + return toolchain.name + } + + override fun isModified(): Boolean { + return myViews.any { it.isModified(toolchain) } + } + + override fun apply() { + toolchain = myViews.fold(toolchain) { tc, view -> view.apply(tc) ?: tc } + } + + override fun reset() { + myViews.forEach { it.reset(toolchain) } + } + + override fun disposeUIResources() { + myViews.forEach { it.dispose() } + myViews = emptyList() + super.disposeUIResources() + } + + companion object { + val TOOLCHAIN_KEY: Key> = Key.create("TOOLCHAIN") + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt new file mode 100644 index 00000000..b9114d00 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt @@ -0,0 +1,46 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.base + +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel +import com.intellij.openapi.extensions.ExtensionPointName + +private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainExtensionsProvider") + +interface ZigToolchainExtensionsProvider { + fun createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?, state: PanelState): ImmutableElementPanel? + val index: Int +} + +fun createZigToolchainExtensionPanels(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?, state: PanelState): List> { + return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull { + it.createExtensionPanel(sharedState, state) + } +} + +enum class PanelState { + ProjectEditor, + ListEditor, + ModalEditor +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt new file mode 100644 index 00000000..01d66775 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt @@ -0,0 +1,93 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.base + +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.UserDataHolder +import com.intellij.ui.SimpleColoredComponent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import java.util.* + +private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainProvider") + +internal interface ZigToolchainProvider { + val serialMarker: String + fun isCompatible(toolchain: ZigToolchain): Boolean + fun deserialize(data: Map): ZigToolchain? + fun serialize(toolchain: ZigToolchain): Map + fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean + fun createConfigurable(uuid: UUID, toolchain: ZigToolchain, data: ZigProjectConfigurationProvider.IUserDataBridge?, modal: Boolean): ZigToolchainConfigurable<*> + suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow + fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) +} + +fun ZigToolchain.Ref.resolve(): ZigToolchain? { + val marker = this.marker ?: return null + val data = this.data ?: return null + val provider = EXTENSION_POINT_NAME.extensionList.find { it.serialMarker == marker } ?: return null + return provider.deserialize(data)?.let { tc -> this.extraData?.let { extraData -> tc.withExtraData(extraData) }} +} + +fun ZigToolchain.toRef(): ZigToolchain.Ref { + val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() + return ZigToolchain.Ref(provider.serialMarker, provider.serialize(this), this.extraData) +} + +fun ZigToolchain.createNamedConfigurable(uuid: UUID, data: ZigProjectConfigurationProvider.IUserDataBridge?, modal: Boolean): ZigToolchainConfigurable<*> { + val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() + return provider.createConfigurable(uuid, this, data, modal) +} + +@OptIn(ExperimentalCoroutinesApi::class) +fun suggestZigToolchains(project: Project? = null, data: UserDataHolder = emptyData): Flow { + val existing = zigToolchainList.map { (_, tc) -> tc } + return EXTENSION_POINT_NAME.extensionList.asFlow().flatMapConcat { ext -> + val compatibleExisting = existing.filter { ext.isCompatible(it) } + val suggestions = ext.suggestToolchains(project, data) + suggestions.filter { suggestion -> + compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) } + } + }.flowOn(Dispatchers.IO) +} + +fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { + val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() + return provider.render(this, component, isSuggestion, isSelected) +} + +private val emptyData = object: UserDataHolder { + override fun getUserData(key: Key): T? { + return null + } + + override fun putUserData(key: Key, value: T?) { + + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainDownloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainDownloader.kt new file mode 100644 index 00000000..3cb24106 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainDownloader.kt @@ -0,0 +1,38 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.downloader + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.getSuggestedLocalToolchainPath +import com.falsepattern.zigbrains.shared.downloader.Downloader +import java.awt.Component + +class LocalToolchainDownloader(component: Component) : Downloader(component) { + override val windowTitle get() = ZigBrainsBundle.message("settings.toolchain.downloader.title") + override val versionInfoFetchTitle get() = ZigBrainsBundle.message("settings.toolchain.downloader.progress.fetch") + override fun downloadProgressTitle(version: ZigVersionInfo) = ZigBrainsBundle.message("settings.toolchain.downloader.progress.install", version.version.rawVersion) + override fun localSelector() = LocalToolchainSelector(component) + override suspend fun downloadVersionList() = ZigVersionInfo.downloadVersionList() + override fun getSuggestedPath() = getSuggestedLocalToolchainPath() +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainSelector.kt new file mode 100644 index 00000000..22802b66 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainSelector.kt @@ -0,0 +1,88 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.downloader + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.downloader.LocalSelector +import com.falsepattern.zigbrains.shared.withUniqueName +import com.intellij.icons.AllIcons +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.TaskCancellation +import com.intellij.platform.ide.progress.withModalProgress +import java.awt.Component +import java.nio.file.Path + +class LocalToolchainSelector(component: Component): LocalSelector(component) { + override val windowTitle: String + get() = ZigBrainsBundle.message("settings.toolchain.local-selector.title") + override val descriptor: FileChooserDescriptor + get() = FileChooserDescriptorFactory.createSingleFolderDescriptor() + .withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title")) + + override suspend fun verify(path: Path): VerifyResult { + var tc = resolve(path, null) + var result: VerifyResult + if (tc == null) { + result = VerifyResult( + null, + false, + AllIcons.General.Error, + ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid"), + ) + } else { + val existingToolchain = zigToolchainList + .mapNotNull { it.second as? LocalZigToolchain } + .firstOrNull { it.location == tc!!.location } + if (existingToolchain != null) { + result = VerifyResult( + null, + true, + AllIcons.General.Warning, + existingToolchain.name?.let { ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-named", it) } + ?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-unnamed") + ) + } else { + result = VerifyResult( + null, + true, + AllIcons.General.Information, + ZigBrainsBundle.message("settings.toolchain.local-selector.state.ok") + ) + } + } + if (tc != null) { + tc = zigToolchainList.withUniqueName(tc) + } + return result.copy(name = tc?.name) + } + + override suspend fun resolve(path: Path, name: String?): LocalZigToolchain? { + return runCatching { withModalProgress(ModalTaskOwner.component(component), "Resolving toolchain", TaskCancellation.cancellable()) { + LocalZigToolchain.tryFromPath(path)?.let { it.withName(name ?: it.name) } + } }.getOrNull() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt new file mode 100644 index 00000000..e3c6aaa5 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt @@ -0,0 +1,87 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.downloader + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.shared.downloader.VersionInfo +import com.falsepattern.zigbrains.shared.downloader.VersionInfo.Tarball +import com.falsepattern.zigbrains.shared.downloader.getTarballIfCompatible +import com.falsepattern.zigbrains.shared.downloader.tempPluginDir +import com.intellij.openapi.progress.coroutineToIndicator +import com.intellij.openapi.util.io.FileUtil +import com.intellij.util.asSafely +import com.intellij.util.download.DownloadableFileService +import com.intellij.util.text.SemVer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.* + +@JvmRecord +data class ZigVersionInfo( + override val version: SemVer, + override val date: String, + val docs: String, + val notes: String, + val src: Tarball?, + override val dist: Tarball +): VersionInfo { + companion object { + @OptIn(ExperimentalSerializationApi::class) + suspend fun downloadVersionList(): List { + return withContext(Dispatchers.IO) { + val service = DownloadableFileService.getInstance() + val tempFile = FileUtil.createTempFile(tempPluginDir, "index", ".json", false, false) + val desc = service.createFileDescription("https://ziglang.org/download/index.json", tempFile.name) + val downloader = service.createDownloader(listOf(desc), ZigBrainsBundle.message("settings.toolchain.downloader.service.index")) + val downloadResults = coroutineToIndicator { + downloader.download(tempPluginDir) + } + if (downloadResults.isEmpty()) + return@withContext emptyList() + val index = downloadResults[0].first + val info = index.inputStream().use { Json.decodeFromStream(it) } + index.delete() + return@withContext info.mapNotNull { (version, data) -> parseVersion(version, data) }.toList() + } + } + } +} + +private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? { + if (data !is JsonObject) + return null + + val versionTag = data["version"]?.asSafely()?.content + + val version = SemVer.parseFromText(versionTag) ?: SemVer.parseFromText(versionKey) + ?: return null + val date = data["date"]?.asSafely()?.content ?: "" + val docs = data["docs"]?.asSafely()?.content ?: "" + val notes = data["notes"]?.asSafely()?.content ?: "" + val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } + val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } + ?: return null + + return ZigVersionInfo(version, date, docs, notes, src, dist) +} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt similarity index 60% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index b534ecb7..385235db 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -20,27 +20,27 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain +package com.falsepattern.zigbrains.project.toolchain.local -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.direnv.DirenvService +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir -import com.intellij.openapi.util.KeyWithDefaultValue import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.vfs.toNioPathOrNull import java.nio.file.Path -class LocalZigToolchain(val location: Path): AbstractZigToolchain() { +@JvmRecord +data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null, override val extraData: Map = emptyMap()): ZigToolchain { override fun workingDirectory(project: Project?): Path? { return project?.guessProjectDir()?.toNioPathOrNull() } override suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project?): GeneralCommandLine { - if (project != null && (commandLine.getUserData(DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) { - commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) + if (project != null && DirenvService.getStateFor(commandLine, project).isEnabled(project)) { + commandLine.withEnvironment(DirenvService.getInstance(project).import().env) } return commandLine } @@ -50,11 +50,17 @@ class LocalZigToolchain(val location: Path): AbstractZigToolchain() { return location.resolve(exeName) } - companion object { - val DIRENV_KEY = KeyWithDefaultValue.create("ZIG_LOCAL_DIRENV") + override fun withExtraData(map: Map): ZigToolchain { + return this.copy(extraData = map) + } + override fun withName(newName: String?): LocalZigToolchain { + return this.copy(name = newName) + } + + companion object { @Throws(ExecutionException::class) - fun ensureLocal(toolchain: AbstractZigToolchain): LocalZigToolchain { + fun ensureLocal(toolchain: ZigToolchain): LocalZigToolchain { if (toolchain is LocalZigToolchain) { return toolchain } else { @@ -62,5 +68,20 @@ class LocalZigToolchain(val location: Path): AbstractZigToolchain() { throw ExecutionException("The debugger only supports local zig toolchain") } } + + suspend fun tryFromPath(path: Path): LocalZigToolchain? { + var tc = LocalZigToolchain(path) + if (!tc.zig.fileValid()) { + return null + } + val versionStr = tc.zig + .getEnv(null) + .getOrNull() + ?.version + if (versionStr != null) { + tc = tc.copy(name = "Zig $versionStr") + } + return tc + } } } \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt similarity index 51% rename from lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt index 5a1b423a..46a161d6 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt @@ -20,28 +20,21 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.lsp +package com.falsepattern.zigbrains.project.toolchain.local -import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsConfigurable -import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsPanel import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.shared.SubConfigurable -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManager +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable +import java.util.* -class ZLSProjectConfigurationProvider: ZigProjectConfigurationProvider { - override fun handleMainConfigChanged(project: Project) { - startLSP(project, true) +class LocalZigToolchainConfigurable( + uuid: UUID, + toolchain: LocalZigToolchain, + data: ZigProjectConfigurationProvider.IUserDataBridge?, + modal: Boolean +): ZigToolchainConfigurable(uuid, toolchain, data, modal) { + override fun createPanel() = LocalZigToolchainPanel() + + override fun setDisplayName(name: String?) { + toolchain = toolchain.copy(name = name) } - - override fun createConfigurable(project: Project): SubConfigurable { - return ZLSSettingsConfigurable(project) - } - - override fun createNewProjectSettingsPanel(holder: ZigProjectConfigurationProvider.SettingsPanelHolder): ZigProjectConfigurationProvider.SettingsPanel { - return ZLSSettingsPanel(ProjectManager.getInstance().defaultProject) - } - - override val priority: Int - get() = 1000 } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt similarity index 51% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt index df1d9115..d614d361 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt @@ -20,24 +20,16 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.settings +package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider -import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableNamedElementPanelBase import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.application.ModalityState import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.UserDataHolderBase import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.platform.ide.progress.ModalTaskOwner -import com.intellij.platform.ide.progress.TaskCancellation -import com.intellij.platform.ide.progress.withModalProgress import com.intellij.ui.DocumentAdapter import com.intellij.ui.JBColor import com.intellij.ui.components.JBCheckBox @@ -52,12 +44,9 @@ import kotlinx.coroutines.launch import javax.swing.event.DocumentEvent import kotlin.io.path.pathString -class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvider.SettingsPanelHolder, private val project: Project) : ZigProjectConfigurationProvider.SettingsPanel { - private val direnv = JBCheckBox(ZigBrainsBundle.message("settings.project.label.direnv")).apply { addActionListener { - dispatchDirenvUpdate() - } } +class LocalZigToolchainPanel() : ImmutableNamedElementPanelBase() { private val pathToToolchain = textFieldWithBrowseButton( - project, + null, ZigBrainsBundle.message("dialog.title.zig-toolchain"), FileChooserDescriptorFactory.createSingleFolderDescriptor() ).also { @@ -69,7 +58,7 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide Disposer.register(this, it) } private val toolchainVersion = JBTextArea().also { it.isEditable = false } - private val stdFieldOverride = JBCheckBox(ZigBrainsBundle.message("settings.project.label.override-std")).apply { + private val stdFieldOverride = JBCheckBox().apply { addChangeListener { if (isSelected) { pathToStd.isEnabled = true @@ -80,93 +69,58 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide } } private val pathToStd = textFieldWithBrowseButton( - project, + null, ZigBrainsBundle.message("dialog.title.zig-std"), FileChooserDescriptorFactory.createSingleFolderDescriptor() ).also { Disposer.register(this, it) } private var debounce: Job? = null - private fun dispatchDirenvUpdate() { - holder.panels.forEach { - it.direnvChanged(direnv.isSelected) - } - } - - override fun direnvChanged(state: Boolean) { - dispatchAutodetect(true) - } - - private fun dispatchAutodetect(force: Boolean) { - project.zigCoroutineScope.launchWithEDT(ModalityState.defaultModalityState()) { - withModalProgress(ModalTaskOwner.component(pathToToolchain), "Detecting Zig...", TaskCancellation.cancellable()) { - autodetect(force) - } - } - } - - suspend fun autodetect(force: Boolean) { - if (!force && pathToToolchain.text.isNotBlank()) - return - val data = UserDataHolderBase() - data.putUserData(LocalZigToolchain.DIRENV_KEY, !project.isDefault && direnv.isSelected && DirenvCmd.direnvInstalled()) - val tc = ZigToolchainProvider.suggestToolchain(project, data) ?: return - if (tc !is LocalZigToolchain) { - TODO("Implement non-local zig toolchain in config") - } - if (force || pathToToolchain.text.isBlank()) { - pathToToolchain.text = tc.location.pathString - dispatchUpdateUI() - } - } - - override var data - get() = ZigProjectSettings( - direnv.isSelected, - stdFieldOverride.isSelected, - pathToStd.text.ifBlank { null }, - pathToToolchain.text.ifBlank { null } - ) - set(value) { - direnv.isSelected = value.direnv - pathToToolchain.text = value.toolchainPath ?: "" - stdFieldOverride.isSelected = value.overrideStdPath - pathToStd.text = value.explicitPathToStd ?: "" - pathToStd.isEnabled = value.overrideStdPath - dispatchUpdateUI() - } - override fun attach(p: Panel): Unit = with(p) { - data = project.zigProjectSettings.state - if (project.isDefault) { - row(ZigBrainsBundle.message("settings.project.label.toolchain")) { - cell(pathToToolchain).resizableColumn().align(AlignX.FILL) - } - row(ZigBrainsBundle.message("settings.project.label.toolchain-version")) { - cell(toolchainVersion) - } - } else { - group(ZigBrainsBundle.message("settings.project.group.title")) { - row(ZigBrainsBundle.message("settings.project.label.toolchain")) { - cell(pathToToolchain).resizableColumn().align(AlignX.FILL) - if (DirenvCmd.direnvInstalled()) { - cell(direnv) - } - } - row(ZigBrainsBundle.message("settings.project.label.toolchain-version")) { - cell(toolchainVersion) - } - row(ZigBrainsBundle.message("settings.project.label.std-location")) { - cell(pathToStd).resizableColumn().align(AlignX.FILL) - cell(stdFieldOverride) - } - } + super.attach(p) + row(ZigBrainsBundle.message("settings.toolchain.local.path.label")) { + cell(pathToToolchain).resizableColumn().align(AlignX.FILL) + } + row(ZigBrainsBundle.message("settings.toolchain.local.version.label")) { + cell(toolchainVersion) + } + row(ZigBrainsBundle.message("settings.toolchain.local.std.label")) { + cell(stdFieldOverride) + cell(pathToStd).resizableColumn().align(AlignX.FILL) + } + } + + override fun isModified(toolchain: LocalZigToolchain): Boolean { + val name = nameFieldValue ?: return false + val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false + val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null + return name != toolchain.name || toolchain.location != location || toolchain.std != std + } + + override fun apply(toolchain: LocalZigToolchain): LocalZigToolchain? { + val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return null + val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null + return toolchain.copy(location = location, std = std, name = nameFieldValue ?: "") + } + + override fun reset(toolchain: LocalZigToolchain?) { + nameFieldValue = toolchain?.name ?: "" + this.pathToToolchain.text = toolchain?.location?.pathString ?: "" + val std = toolchain?.std + if (std != null) { + stdFieldOverride.isSelected = true + pathToStd.text = std.pathString + pathToStd.isEnabled = true + } else { + stdFieldOverride.isSelected = false + pathToStd.text = "" + pathToStd.isEnabled = false + dispatchUpdateUI() } - dispatchAutodetect(false) } private fun dispatchUpdateUI() { debounce?.cancel("New debounce") - debounce = project.zigCoroutineScope.launch { + debounce = zigCoroutineScope.launch { updateUI() } } @@ -185,7 +139,7 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide } val toolchain = LocalZigToolchain(pathToToolchain) val zig = toolchain.zig - val env = zig.getEnv(project).getOrElse { throwable -> + val env = zig.getEnv(null).getOrElse { throwable -> throwable.printStackTrace() withEDTContext(ModalityState.any()) { toolchainVersion.text = "[failed to run \"zig env\"]\n${throwable.message}" @@ -196,7 +150,7 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide return } val version = env.version - val stdPath = env.stdPath(toolchain, project) + val stdPath = env.stdPath(toolchain, null) withEDTContext(ModalityState.any()) { toolchainVersion.text = version diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt new file mode 100644 index 00000000..dee209ee --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -0,0 +1,147 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.local + +import com.falsepattern.zigbrains.direnv.DirenvService +import com.falsepattern.zigbrains.direnv.Env +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider +import com.falsepattern.zigbrains.shared.ui.renderPathNameComponent +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.UserDataHolder +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.ui.SimpleColoredComponent +import com.intellij.util.system.OS +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import java.nio.file.Files +import java.nio.file.Path +import java.util.* +import kotlin.io.path.isDirectory +import kotlin.io.path.pathString + +class LocalZigToolchainProvider: ZigToolchainProvider { + override val serialMarker: String + get() = "local" + + override fun deserialize(data: Map): ZigToolchain? { + val location = data["location"]?.toNioPathOrNull() ?: return null + val std = data["std"]?.toNioPathOrNull() + val name = data["name"] + return LocalZigToolchain(location, std, name) + } + + override fun isCompatible(toolchain: ZigToolchain): Boolean { + return toolchain is LocalZigToolchain + } + + override fun serialize(toolchain: ZigToolchain): Map { + toolchain as LocalZigToolchain + val map = HashMap() + toolchain.location.pathString.let { map["location"] = it } + toolchain.std?.pathString?.let { map["std"] = it } + toolchain.name?.let { map["name"] = it } + return map + } + + override fun matchesSuggestion( + toolchain: ZigToolchain, + suggestion: ZigToolchain + ): Boolean { + toolchain as LocalZigToolchain + suggestion as LocalZigToolchain + return toolchain.location == suggestion.location + } + + override fun createConfigurable( + uuid: UUID, + toolchain: ZigToolchain, + data: ZigProjectConfigurationProvider.IUserDataBridge?, + modal: Boolean + ): ZigToolchainConfigurable<*> { + toolchain as LocalZigToolchain + return LocalZigToolchainConfigurable(uuid, toolchain, data, modal) + } + + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow { + val env = if (project != null && DirenvService.getStateFor(data, project).isEnabled(project)) { + DirenvService.getInstance(project).import() + } else { + Env.empty + } + val pathToolchains = env.findAllExecutablesOnPATH("zig").mapNotNull { it.parent } + val wellKnown = getWellKnown().asFlow().flatMapConcat { dir -> + runCatching { + Files.newDirectoryStream(dir).use { stream -> + stream.toList().filterNotNull().asFlow() + } + }.getOrElse { emptyFlow() } + } + val joined = flowOf(pathToolchains, wellKnown).flattenConcat() + return joined.mapNotNull { LocalZigToolchain.tryFromPath(it) } + } + + override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { + toolchain as LocalZigToolchain + val name = toolchain.name + val path = toolchain.location.pathString + renderPathNameComponent(path, name, "Zig", component, isSuggestion, isSelected) + } +} + +fun getSuggestedLocalToolchainPath(): Path? { + return getWellKnown().getOrNull(0) +} + +/** + * Returns the paths to the following list of folders: + * + * 1. DATA/zig + * 2. DATA/zigup + * 3. HOME/.zig + * + * Where DATA is: + * - ~/Library on macOS + * - %LOCALAPPDATA% on Windows + * - $XDG_DATA_HOME (or ~/.local/share if not set) on other OSes + * + * and HOME is the user home path + */ +private fun getWellKnown(): List { + val home = System.getProperty("user.home")?.toNioPathOrNull() ?: return emptyList() + val xdgDataHome = when(OS.CURRENT) { + OS.macOS -> home.resolve("Library") + OS.Windows -> System.getenv("LOCALAPPDATA")?.toNioPathOrNull() + else -> System.getenv("XDG_DATA_HOME")?.toNioPathOrNull() ?: home.resolve(Path.of(".local", "share")) + } + val res = ArrayList() + if (xdgDataHome != null && xdgDataHome.isDirectory()) { + res.add(xdgDataHome.resolve("zig")) + res.add(xdgDataHome.resolve("zigup")) + } + res.add(home.resolve(".zig")) + return res +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt index 85e2af65..50357a3f 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt @@ -22,14 +22,13 @@ package com.falsepattern.zigbrains.project.toolchain.tools -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainEnvironmentSerializable +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.openapi.project.Project import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import java.nio.file.Path -class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) { +class ZigCompilerTool(toolchain: ZigToolchain) : ZigTool(toolchain) { override val toolName: String get() = "zig" diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt index 27b106bf..fa9c5d49 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt @@ -22,15 +22,16 @@ package com.falsepattern.zigbrains.project.toolchain.tools -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.cli.call import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.ProcessOutput import com.intellij.openapi.project.Project import java.nio.file.Path +import kotlin.io.path.isRegularFile -abstract class ZigTool(val toolchain: AbstractZigToolchain) { +abstract class ZigTool(val toolchain: ZigToolchain) { abstract val toolName: String suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE, ipcProject: Project? = null): Result { @@ -38,6 +39,11 @@ abstract class ZigTool(val toolchain: AbstractZigToolchain) { return cli.call(timeoutMillis, ipcProject = ipcProject) } + fun fileValid(): Boolean { + val exe = toolchain.pathToExecutable(toolName) + return exe.isRegularFile() + } + private suspend fun createBaseCommandLine( workingDirectory: Path?, vararg parameters: String diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainEnvironmentSerializable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigToolchainEnvironmentSerializable.kt similarity index 93% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainEnvironmentSerializable.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigToolchainEnvironmentSerializable.kt index 8bda26d3..15e8a7ae 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainEnvironmentSerializable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigToolchainEnvironmentSerializable.kt @@ -19,15 +19,16 @@ * You should have received a copy of the GNU Lesser General Public License * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain +package com.falsepattern.zigbrains.project.toolchain.tools + +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.openapi.project.Project import com.intellij.openapi.util.io.toNioPathOrNull import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.nio.file.Path - @JvmRecord @Serializable data class ZigToolchainEnvironmentSerializable( @@ -49,4 +50,4 @@ data class ZigToolchainEnvironmentSerializable( return null } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt new file mode 100644 index 00000000..40e62f63 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt @@ -0,0 +1,36 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.intellij.openapi.Disposable +import com.intellij.ui.dsl.builder.Panel + +interface ImmutableElementPanel: Disposable { + fun attach(p: Panel) + fun isModified(elem: T): Boolean + /** + * Returned object must be the exact same class as the provided one. + */ + fun apply(elem: T): T? + fun reset(elem: T?) +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableNamedElementPanelBase.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableNamedElementPanelBase.kt new file mode 100644 index 00000000..5f3906c4 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableNamedElementPanelBase.kt @@ -0,0 +1,43 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel + +abstract class ImmutableNamedElementPanelBase: ImmutableElementPanel { + private val nameField = JBTextField(25) + + protected var nameFieldValue: String? + get() = nameField.text.ifBlank { null } + set(value) {nameField.text = value ?: ""} + + override fun attach(p: Panel): Unit = with(p) { + row(ZigBrainsBundle.message("settings.toolchain.base.name.label")) { + cell(nameField).resizableColumn().align(AlignX.FILL) + } + separator() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt new file mode 100644 index 00000000..2f323184 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt @@ -0,0 +1,42 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.downloader.LocalToolchainDownloader +import com.falsepattern.zigbrains.project.toolchain.downloader.LocalToolchainSelector +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.ui.ListElem +import com.falsepattern.zigbrains.shared.withUniqueName +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import java.awt.Component +import java.util.* + +internal object ZigToolchainComboBoxHandler { + @RequiresBackgroundThread + suspend fun onItemSelected(context: Component, elem: ListElem.Pseudo): UUID? = when(elem) { + is ListElem.One.Suggested -> zigToolchainList.withUniqueName(elem.instance) + is ListElem.Download -> LocalToolchainDownloader(context).download() + is ListElem.FromDisk -> LocalToolchainSelector(context).browse() + }?.let { zigToolchainList.registerNew(it) } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt new file mode 100644 index 00000000..6e02494f --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt @@ -0,0 +1,91 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable +import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.ui.* +import com.intellij.openapi.ui.NamedConfigurable +import java.awt.Component +import java.util.* + +sealed interface ZigToolchainDriver: UUIDComboBoxDriver { + override val theMap get() = zigToolchainList + + override fun createContext(model: ZBModel): ZBContext { + return TCContext(null, model) + } + + override fun createComboBox(model: ZBModel): ZBComboBox { + return TCComboBox(model) + } + + override suspend fun resolvePseudo( + context: Component, + elem: ListElem.Pseudo + ): UUID? { + return ZigToolchainComboBoxHandler.onItemSelected(context, elem) + } + + object ForList: ZigToolchainDriver { + override suspend fun constructModelList(): List> { + val modelList = ArrayList>() + modelList.addAll(ListElem.fetchGroup()) + modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) + modelList.add(suggestZigToolchains().asPending()) + return modelList + } + + override fun createNamedConfigurable( + uuid: UUID, + elem: ZigToolchain + ): NamedConfigurable { + return elem.createNamedConfigurable(uuid, ZigProjectConfigurationProvider.UserDataBridge(), false) + } + } + + class ForSelector(val data: ZigProjectConfigurationProvider.IUserDataBridge): ZigToolchainDriver { + override suspend fun constructModelList(): List> { + val modelList = ArrayList>() + modelList.add(ListElem.None()) + modelList.addAll(zigToolchainList.map { it.asActual() }.sortedBy { it.instance.name }) + modelList.add(Separator("", true)) + modelList.addAll(ListElem.fetchGroup()) + modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) + modelList.add(suggestZigToolchains(data.getUserData(PROJECT_KEY), data).asPending()) + return modelList + } + + override fun createNamedConfigurable( + uuid: UUID, + elem: ZigToolchain + ): NamedConfigurable { + return elem.createNamedConfigurable(uuid, data, true) + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt new file mode 100644 index 00000000..30ec0ae6 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -0,0 +1,141 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.base.PanelState +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable +import com.falsepattern.zigbrains.project.toolchain.base.createZigToolchainExtensionPanels +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.SubConfigurable +import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.util.Key +import com.intellij.ui.dsl.builder.Panel +import kotlinx.coroutines.launch +import java.util.* +import java.util.function.Supplier + +class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): + UUIDMapSelector(ZigToolchainDriver.ForSelector(sharedState)), + SubConfigurable, + ZigProjectConfigurationProvider.UserDataListener +{ + private var myViews: List> = emptyList() + init { + sharedState.putUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY, Supplier{selectedUUID?.let { zigToolchainList[it] }}) + sharedState.addUserDataChangeListener(this) + } + + override fun onUserDataChanged(key: Key<*>) { + if (key == ZigToolchainConfigurable.TOOLCHAIN_KEY) + return + zigCoroutineScope.launch { listChanged() } + } + + + override fun attach(p: Panel): Unit = with(p) { + row(ZigBrainsBundle.message( + if (sharedState.getUserData(PROJECT_KEY)?.isDefault == true) + "settings.toolchain.editor.toolchain-default.label" + else + "settings.toolchain.editor.toolchain.label") + ) { + attachComboBoxRow(this) + } + var views = myViews + if (views.isEmpty()) { + views = ArrayList>() + views.addAll(createZigToolchainExtensionPanels(sharedState, PanelState.ProjectEditor)) + myViews = views + } + views.forEach { it.attach(p) } + } + + override fun onSelection(uuid: UUID?) { + sharedState.putUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY, Supplier{selectedUUID?.let { zigToolchainList[it] }}) + refreshViews(uuid) + } + + private fun refreshViews(uuid: UUID?) { + val toolchain = uuid?.let { zigToolchainList[it] } + myViews.forEach { it.reset(toolchain) } + } + + override fun isModified(context: Project): Boolean { + if (isEmpty) + return false + val uuid = selectedUUID + if (ZigToolchainService.getInstance(context).toolchainUUID != selectedUUID) { + return true + } + if (uuid == null) + return false + val tc = zigToolchainList[uuid] + if (tc == null) + return false + return myViews.any { it.isModified(tc) } + } + + override fun apply(context: Project) { + val uuid = selectedUUID + ZigToolchainService.getInstance(context).toolchainUUID = uuid + if (uuid == null) + return + val tc = zigToolchainList[uuid] + if (tc == null) + return + val finalTc = myViews.fold(tc) { acc, view -> view.apply(acc) ?: acc } + zigToolchainList[uuid] = finalTc + } + + override fun reset(context: Project?) { + val project = context ?: ProjectManager.getInstance().defaultProject + val svc = ZigToolchainService.getInstance(project) + val uuid = svc.toolchainUUID + selectedUUID = uuid + refreshViews(uuid) + } + + override fun dispose() { + super.dispose() + sharedState.removeUserDataChangeListener(this) + myViews.forEach { it.dispose() } + myViews = emptyList() + } + + override val newProjectBeforeInitSelector get() = true + class Provider: ZigProjectConfigurationProvider { + override fun create(sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { + return ZigToolchainEditor(sharedState).also { it.reset(sharedState.getUserData(PROJECT_KEY)) } + } + + override val index: Int get() = 0 + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt new file mode 100644 index 00000000..7dd9131d --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -0,0 +1,31 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor + +class ZigToolchainListEditor : UUIDMapEditor(ZigToolchainDriver.ForList) { + override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.list.title") +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt new file mode 100644 index 00000000..f157649d --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -0,0 +1,80 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.Icons +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.render +import com.falsepattern.zigbrains.shared.ui.* +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.icons.EMPTY_ICON +import javax.swing.JList + +class TCComboBox(model: ZBModel): ZBComboBox(model, ::TCCellRenderer) + +class TCContext(project: Project?, model: ZBModel): ZBContext(project, model, ::TCCellRenderer) + +class TCCellRenderer(getModel: () -> ZBModel): ZBCellRenderer(getModel) { + override fun customizeCellRenderer( + list: JList?>, + value: ListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + icon = EMPTY_ICON + when (value) { + is ListElem.One -> { + val (icon, isSuggestion) = when(value) { + is ListElem.One.Suggested -> AllIcons.General.Information to true + is ListElem.One.Actual -> Icons.Zig to false + } + this.icon = icon + val item = value.instance + item.render(this, isSuggestion, index == -1) + } + + is ListElem.Download -> { + icon = AllIcons.Actions.Download + append(ZigBrainsBundle.message("settings.toolchain.model.download.text")) + } + + is ListElem.FromDisk -> { + icon = AllIcons.General.OpenDisk + append(ZigBrainsBundle.message("settings.toolchain.model.from-disk.text")) + } + is ListElem.Pending -> { + icon = AllIcons.Empty + append(ZigBrainsBundle.message("settings.toolchain.model.loading.text"), SimpleTextAttributes.GRAYED_ATTRIBUTES) + } + is ListElem.None, null -> { + icon = AllIcons.General.BalloonError + append(ZigBrainsBundle.message("settings.toolchain.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES) + } + } + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/MultiConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/MultiConfigurable.kt deleted file mode 100644 index 36c30c13..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/MultiConfigurable.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.shared - -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.intellij.openapi.options.Configurable -import com.intellij.openapi.util.Disposer -import com.intellij.ui.dsl.builder.panel -import javax.swing.JComponent - -abstract class MultiConfigurable(val configurables: List): Configurable, ZigProjectConfigurationProvider.SettingsPanelHolder { - final override var panels: List = emptyList() - private set - - override fun createComponent(): JComponent? { - return panel { - panels = configurables.map { it.createComponent(this@MultiConfigurable, this@panel) } - } - } - - override fun isModified(): Boolean { - return configurables.any { it.isModified() } - } - - override fun apply() { - configurables.forEach { it.apply() } - } - - override fun reset() { - configurables.forEach { it.reset() } - } - - override fun disposeUIResources() { - configurables.forEach { Disposer.dispose(it) } - panels = emptyList() - } -} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt new file mode 100644 index 00000000..3bb35b82 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt @@ -0,0 +1,32 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared + +interface NamedObject> { + val name: String? + + /** + * Returned object must be the exact same class as the called one. + */ + fun withName(newName: String?): T +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt index 5e7c52e5..eafca678 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt @@ -22,16 +22,72 @@ package com.falsepattern.zigbrains.shared -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.SettingsPanelHolder import com.intellij.openapi.Disposable -import com.intellij.openapi.options.ConfigurationException +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.util.Disposer import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent -interface SubConfigurable: Disposable { - fun createComponent(holder: SettingsPanelHolder, panel: Panel): ZigProjectConfigurationProvider.SettingsPanel - fun isModified(): Boolean - @Throws(ConfigurationException::class) - fun apply() - fun reset() +interface SubConfigurable: Disposable { + fun attach(panel: Panel) + fun isModified(context: T): Boolean + fun apply(context: T) + fun reset(context: T?) + + val newProjectBeforeInitSelector: Boolean get() = false + + abstract class Adapter: Configurable { + private val myConfigurables: MutableList> = ArrayList() + + abstract fun instantiate(): List> + protected abstract val context: T + + override fun createComponent(): JComponent? { + val configurables: List> + synchronized(myConfigurables) { + if (myConfigurables.isEmpty()) { + disposeConfigurables() + } + configurables = instantiate() + configurables.forEach { it.reset(context) } + myConfigurables.clear() + myConfigurables.addAll(configurables) + } + return panel { + configurables.forEach { it.attach(this) } + } + } + + override fun isModified(): Boolean { + synchronized(myConfigurables) { + return myConfigurables.any { it.isModified(context) } + } + } + + override fun apply() { + synchronized(myConfigurables) { + myConfigurables.forEach { it.apply(context) } + } + } + + override fun reset() { + synchronized(myConfigurables) { + myConfigurables.forEach { it.reset(context) } + } + } + + override fun disposeUIResources() { + synchronized(myConfigurables) { + disposeConfigurables() + } + super.disposeUIResources() + } + + private fun disposeConfigurables() { + val configurables = ArrayList(myConfigurables) + myConfigurables.clear() + configurables.forEach { Disposer.dispose(it) } + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUID.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUID.kt new file mode 100644 index 00000000..214fcd58 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUID.kt @@ -0,0 +1,28 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared + +import java.util.* + +fun String.asUUID(): UUID? = UUID.fromString(this) +fun UUID.asString(): String = toString() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt new file mode 100644 index 00000000..2590d2b6 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt @@ -0,0 +1,191 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared + +import com.intellij.openapi.components.SerializablePersistentStateComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.lang.ref.WeakReference +import java.util.* + +typealias UUIDStorage = Map + +abstract class UUIDMapSerializable(init: S): SerializablePersistentStateComponent(init), ChangeTrackingStorage { + private val changeListeners = ArrayList>() + + protected abstract fun getStorage(state: S): UUIDStorage + + protected abstract fun updateStorage(state: S, storage: UUIDStorage): S + + override fun addChangeListener(listener: StorageChangeListener) { + synchronized(changeListeners) { + changeListeners.add(WeakReference(listener)) + } + } + + override fun removeChangeListener(listener: StorageChangeListener) { + synchronized(changeListeners) { + changeListeners.removeIf { + val v = it.get() + v == null || v === listener + } + } + } + + protected fun registerNewUUID(value: T): UUID { + var uuid = UUID.randomUUID() + updateState { + val newMap = HashMap() + newMap.putAll(getStorage(it)) + var uuidStr = uuid.asString() + while (newMap.containsKey(uuidStr)) { + uuid = UUID.randomUUID() + uuidStr = uuid.asString() + } + newMap[uuidStr] = value + updateStorage(it, newMap) + } + notifyChanged() + return uuid + } + + protected fun setStateUUID(uuid: UUID, value: T) { + val str = uuid.asString() + updateState { + val newMap = HashMap() + newMap.putAll(getStorage(it)) + newMap[str] = value + updateStorage(it, newMap) + } + notifyChanged() + } + + protected fun getStateUUID(uuid: UUID): T? { + return getStorage(state)[uuid.asString()] + } + + protected fun hasStateUUID(uuid: UUID): Boolean { + return getStorage(state).containsKey(uuid.asString()) + } + + protected fun removeStateUUID(uuid: UUID) { + val str = uuid.asString() + updateState { + updateStorage(state, getStorage(state).filter { it.key != str }) + } + notifyChanged() + } + + private fun notifyChanged() { + synchronized(changeListeners) { + var i = 0 + while (i < changeListeners.size) { + val v = changeListeners[i].get() + if (v == null) { + changeListeners.removeAt(i) + continue + } + zigCoroutineScope.launch { + v() + } + i++ + } + } + } + + abstract class Converting(init: S): + UUIDMapSerializable(init), + AccessibleStorage, + IterableStorage + { + protected abstract fun serialize(value: R): T + protected abstract fun deserialize(value: T): R? + override fun registerNew(value: R): UUID { + val ser = serialize(value) + return registerNewUUID(ser) + } + override operator fun set(uuid: UUID, value: R) { + val ser = serialize(value) + setStateUUID(uuid, ser) + } + override operator fun get(uuid: UUID): R? { + return getStateUUID(uuid)?.let { deserialize(it) } + } + override operator fun contains(uuid: UUID): Boolean { + return hasStateUUID(uuid) + } + override fun remove(uuid: UUID) { + removeStateUUID(uuid) + } + + override fun iterator(): Iterator> { + return getStorage(state) + .asSequence() + .mapNotNull { + val uuid = it.key.asUUID() ?: return@mapNotNull null + val tc = deserialize(it.value) ?: return@mapNotNull null + uuid to tc + }.iterator() + } + } + + abstract class Direct(init: S): Converting(init) { + override fun serialize(value: T): T { + return value + } + + override fun deserialize(value: T): T? { + return value + } + } +} + +typealias StorageChangeListener = suspend CoroutineScope.() -> Unit + +interface ChangeTrackingStorage { + fun addChangeListener(listener: StorageChangeListener) + fun removeChangeListener(listener: StorageChangeListener) +} + +interface AccessibleStorage { + fun registerNew(value: R): UUID + operator fun set(uuid: UUID, value: R) + operator fun get(uuid: UUID): R? + operator fun contains(uuid: UUID): Boolean + fun remove(uuid: UUID) +} + +interface IterableStorage: Iterable> + +fun , T: R> IterableStorage.withUniqueName(value: T): T { + val baseName = value.name ?: "" + var index = 0 + var currentName = baseName + val names = this.map { (_, existing) -> existing.name } + while (names.any { it == currentName }) { + index++ + currentName = "$baseName ($index)" + } + @Suppress("UNCHECKED_CAST") + return value.withName(currentName) as T +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt new file mode 100644 index 00000000..acc89145 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt @@ -0,0 +1,73 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared + +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.util.io.Decompressor +import java.io.IOException +import java.nio.file.Path +import kotlin.io.path.name + +enum class Unarchiver { + ZIP { + override val extension = "zip" + override fun createDecompressor(file: Path) = Decompressor.Zip(file) + }, + TAR_GZ { + override val extension = "tar.gz" + override fun createDecompressor(file: Path) = Decompressor.Tar(file) + }, + TAR_XZ { + override val extension = "tar.xz" + override fun createDecompressor(file: Path) = Decompressor.Tar(file) + }, + VSIX { + override val extension = "vsix" + override fun createDecompressor(file: Path) = Decompressor.Zip(file) + }; + + protected abstract val extension: String + protected abstract fun createDecompressor(file: Path): Decompressor + + companion object { + @Throws(IOException::class) + fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) { + val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } + ?: error("Unexpected archive type: $archivePath") + val dec = unarchiver.createDecompressor(archivePath) + val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator() + indicator.isIndeterminate = true + indicator.text = "Extracting archive" + dec.filter { + indicator.text2 = it + indicator.checkCanceled() + true + } + if (prefix != null) { + dec.removePrefixPath(prefix) + } + dec.extract(dst) + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt index e1c48c16..d8042fae 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt @@ -28,7 +28,6 @@ import com.falsepattern.zigbrains.shared.ipc.IPCUtil import com.falsepattern.zigbrains.shared.ipc.ipc import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.process.ProcessHandler import com.intellij.execution.process.ProcessOutput import com.intellij.execution.process.ProcessTerminatedListener import com.intellij.openapi.options.ConfigurationException diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt index b41f0f25..8d3d47ad 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt @@ -30,6 +30,8 @@ import com.intellij.platform.ide.progress.TaskCancellation import com.intellij.platform.ide.progress.runWithModalProgressBlocking import com.intellij.util.application import kotlinx.coroutines.* +import java.awt.Component +import kotlin.coroutines.CoroutineContext inline fun runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleFactory: () -> String, cancellationFactory: () -> TaskCancellation = TaskCancellation::cancellable, noinline action: suspend CoroutineScope.() -> T): T { return if (application.isDispatchThread) { @@ -40,7 +42,11 @@ inline fun runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleF } suspend inline fun withEDTContext(state: ModalityState, noinline block: suspend CoroutineScope.() -> T): T { - return withContext(Dispatchers.EDT + state.asContextElement(), block = block) + return withEDTContext(state.asContextElement(), block = block) +} + +suspend inline fun withEDTContext(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T): T { + return withContext(Dispatchers.EDT + context, block = block) } suspend inline fun withCurrentEDTModalityContext(noinline block: suspend CoroutineScope.() -> T): T { @@ -50,9 +56,19 @@ suspend inline fun withCurrentEDTModalityContext(noinline block: suspend Cor } suspend inline fun runInterruptibleEDT(state: ModalityState, noinline targetAction: () -> T): T { - return runInterruptible(Dispatchers.EDT + state.asContextElement(), block = targetAction) + return runInterruptibleEDT(state.asContextElement(), targetAction = targetAction) +} +suspend inline fun runInterruptibleEDT(context: CoroutineContext, noinline targetAction: () -> T): T { + return runInterruptible(Dispatchers.EDT + context, block = targetAction) } fun CoroutineScope.launchWithEDT(state: ModalityState, block: suspend CoroutineScope.() -> Unit): Job { - return launch(Dispatchers.EDT + state.asContextElement(), block = block) + return launchWithEDT(state.asContextElement(), block = block) +} +fun CoroutineScope.launchWithEDT(context: CoroutineContext, block: suspend CoroutineScope.() -> Unit): Job { + return launch(Dispatchers.EDT + context, block = block) +} + +fun Component.asContextElement(): CoroutineContext { + return ModalityState.stateForComponent(this).asContextElement() } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/DirectoryState.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/DirectoryState.kt new file mode 100644 index 00000000..ac6110e7 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/DirectoryState.kt @@ -0,0 +1,78 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.downloader + +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.isDirectory + +enum class DirectoryState { + Invalid, + NotAbsolute, + NotDirectory, + NotEmpty, + CreateNew, + Ok; + + fun isValid(): Boolean { + return when(this) { + Invalid, NotAbsolute, NotDirectory, NotEmpty -> false + CreateNew, Ok -> true + } + } + + companion object { + @JvmStatic + fun determine(path: Path?): DirectoryState { + if (path == null) { + return Invalid + } + if (!path.isAbsolute) { + return NotAbsolute + } + if (!path.exists()) { + var parent: Path? = path.parent + while(parent != null) { + if (!parent.exists()) { + parent = parent.parent + continue + } + if (!parent.isDirectory()) { + return NotDirectory + } + return CreateNew + } + return Invalid + } + if (!path.isDirectory()) { + return NotDirectory + } + val isEmpty = Files.newDirectoryStream(path).use { !it.iterator().hasNext() } + if (!isEmpty) { + return NotEmpty + } + return Ok + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/Downloader.kt new file mode 100644 index 00000000..fb81e7a2 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/Downloader.kt @@ -0,0 +1,172 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.downloader + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT +import com.intellij.icons.AllIcons +import com.intellij.openapi.observable.util.whenFocusGained +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogBuilder +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.NlsContexts +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.TaskCancellation +import com.intellij.platform.ide.progress.withModalProgress +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.textFieldWithBrowseButton +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.concurrency.annotations.RequiresEdt +import java.awt.Component +import java.nio.file.Path +import java.util.* +import javax.swing.DefaultComboBoxModel +import javax.swing.JList +import javax.swing.event.DocumentEvent +import kotlin.io.path.pathString + +abstract class Downloader(val component: Component) { + suspend fun download(): T? { + val info = withModalProgress( + ModalTaskOwner.component(component), + versionInfoFetchTitle, + TaskCancellation.cancellable() + ) { + downloadVersionList() + } + val selector = localSelector() + val (downloadPath, version) = runInterruptibleEDT(component.asContextElement()) { + selectVersion(info, selector) + } ?: return null + withModalProgress( + ModalTaskOwner.component(component), + downloadProgressTitle(version), + TaskCancellation.cancellable() + ) { + version.downloadAndUnpack(downloadPath) + } + return selector.browse(downloadPath) + } + + protected abstract val windowTitle: String + protected abstract val versionInfoFetchTitle: @NlsContexts.ProgressTitle String + protected abstract fun downloadProgressTitle(version: V): @NlsContexts.ProgressTitle String + protected abstract fun localSelector(): LocalSelector + protected abstract suspend fun downloadVersionList(): List + protected abstract fun getSuggestedPath(): Path? + + @RequiresEdt + private fun selectVersion(info: List, selector: LocalSelector): Pair? { + val dialog = DialogBuilder() + val theList = ComboBox(DefaultComboBoxModel(Vector(info))) + theList.renderer = object: ColoredListCellRenderer() { + override fun customizeCellRenderer( + list: JList, + value: V?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + value?.let { append(it.version.rawVersion) } + } + } + val outputPath = textFieldWithBrowseButton(null, selector.descriptor.title, selector.descriptor) + Disposer.register(dialog, outputPath) + outputPath.textField.columns = 50 + + lateinit var errorMessageBox: JBLabel + fun onChanged() { + val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() + val state = DirectoryState.Companion.determine(path) + if (state.isValid()) { + errorMessageBox.icon = AllIcons.General.Information + dialog.setOkActionEnabled(true) + } else { + errorMessageBox.icon = AllIcons.General.Error + dialog.setOkActionEnabled(false) + } + errorMessageBox.text = ZigBrainsBundle.message(when(state) { + DirectoryState.Invalid -> "settings.shared.downloader.state.invalid" + DirectoryState.NotAbsolute -> "settings.shared.downloader.state.not-absolute" + DirectoryState.NotDirectory -> "settings.shared.downloader.state.not-directory" + DirectoryState.NotEmpty -> "settings.shared.downloader.state.not-empty" + DirectoryState.CreateNew -> "settings.shared.downloader.state.create-new" + DirectoryState.Ok -> "settings.shared.downloader.state.ok" + }) + dialog.window.repaint() + } + outputPath.whenFocusGained { + onChanged() + } + outputPath.addDocumentListener(object: DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + onChanged() + } + }) + var archiveSizeCell: Cell<*>? = null + fun detect(item: V) { + outputPath.text = getSuggestedPath()?.resolve(item.version.rawVersion)?.pathString ?: "" + val size = item.dist.size + val sizeMb = size / (1024f * 1024f) + archiveSizeCell?.comment?.text = ZigBrainsBundle.message("settings.shared.downloader.archive-size.text", "%.2fMB".format(sizeMb)) + } + theList.addItemListener { + @Suppress("UNCHECKED_CAST") + detect(it.item as V) + } + val center = panel { + row(ZigBrainsBundle.message("settings.shared.downloader.version.label")) { + cell(theList).resizableColumn().align(AlignX.FILL) + } + row(ZigBrainsBundle.message("settings.shared.downloader.location.label")) { + cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } + } + row { + errorMessageBox = JBLabel() + cell(errorMessageBox) + } + } + detect(info[0]) + dialog.centerPanel(center) + dialog.setTitle(windowTitle) + dialog.addCancelAction() + dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.shared.downloader.ok-action")) } + if (!dialog.showAndGet()) { + return null + } + val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() + ?: return null + if (!DirectoryState.Companion.determine(path).isValid()) { + return null + } + val version = theList.item ?: return null + + return path to version + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/LocalSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/LocalSelector.kt new file mode 100644 index 00000000..b35130b5 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/LocalSelector.kt @@ -0,0 +1,138 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.downloader + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.icons.AllIcons +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.ui.DialogBuilder +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextField +import com.intellij.ui.components.textFieldWithBrowseButton +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.concurrency.annotations.RequiresEdt +import java.awt.Component +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicBoolean +import javax.swing.Icon +import javax.swing.event.DocumentEvent +import kotlin.io.path.pathString + +abstract class LocalSelector(val component: Component) { + suspend open fun browse(preSelected: Path? = null): T? { + return withEDTContext(component.asContextElement()) { + doBrowseFromDisk(preSelected) + } + } + + abstract val windowTitle: String + abstract val descriptor: FileChooserDescriptor + protected abstract suspend fun verify(path: Path): VerifyResult + protected abstract suspend fun resolve(path: Path, name: String?): T? + + @RequiresEdt + private suspend fun doBrowseFromDisk(preSelected: Path?): T? { + val dialog = DialogBuilder() + val name = JBTextField().also { it.columns = 25 } + val path = textFieldWithBrowseButton(null, descriptor.title, descriptor) + Disposer.register(dialog, path) + lateinit var errorMessageBox: JBLabel + suspend fun verifyAndUpdate(path: Path?) { + val result = path?.let { verify(it) } ?: VerifyResult( + "", + false, + AllIcons.General.Error, + ZigBrainsBundle.message("settings.shared.local-selector.state.invalid") + ) + val prevNameDefault = name.emptyText.text.trim() == name.text.trim() || name.text.isBlank() + name.emptyText.text = result.name ?: "" + if (prevNameDefault) { + name.text = name.emptyText.text + } + errorMessageBox.icon = result.errorIcon + errorMessageBox.text = result.errorText + dialog.setOkActionEnabled(result.allowed) + } + val active = AtomicBoolean(false) + path.addDocumentListener(object: DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + if (!active.get()) + return + zigCoroutineScope.launchWithEDT(ModalityState.current()) { + verifyAndUpdate(path.text.ifBlank { null }?.toNioPathOrNull()) + } + } + }) + val center = panel { + row(ZigBrainsBundle.message("settings.shared.local-selector.name.label")) { + cell(name).resizableColumn().align(AlignX.FILL) + } + row(ZigBrainsBundle.message("settings.shared.local-selector.path.label")) { + cell(path).resizableColumn().align(AlignX.FILL) + } + row { + errorMessageBox = JBLabel() + cell(errorMessageBox) + } + } + dialog.centerPanel(center) + dialog.setTitle(windowTitle) + dialog.addCancelAction() + dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.shared.local-selector.ok-action")) } + if (preSelected == null) { + val chosenFile = FileChooser.chooseFile(descriptor, null, null) + if (chosenFile != null) { + verifyAndUpdate(chosenFile.toNioPath()) + path.text = chosenFile.path + } + } else { + verifyAndUpdate(preSelected) + path.text = preSelected.pathString + } + active.set(true) + if (!dialog.showAndGet()) { + active.set(false) + return null + } + active.set(false) + return path.text.ifBlank { null }?.toNioPathOrNull()?.let { resolve(it, name.text.ifBlank { null }) } + } + + @JvmRecord + data class VerifyResult( + val name: String?, + val allowed: Boolean, + val errorIcon: Icon, + val errorText: String, + ) +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/VersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/VersionInfo.kt new file mode 100644 index 00000000..68208436 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/VersionInfo.kt @@ -0,0 +1,163 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.downloader + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.shared.Unarchiver +import com.falsepattern.zigbrains.shared.downloader.VersionInfo.Tarball +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.coroutineToIndicator +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.platform.util.progress.ProgressReporter +import com.intellij.platform.util.progress.reportProgress +import com.intellij.util.download.DownloadableFileService +import com.intellij.util.io.createDirectories +import com.intellij.util.io.delete +import com.intellij.util.io.move +import com.intellij.util.system.CpuArch +import com.intellij.util.system.OS +import com.intellij.util.text.SemVer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromJsonElement +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.deleteRecursively +import kotlin.io.path.isDirectory +import kotlin.io.path.name + +interface VersionInfo { + val version: SemVer + val date: String + val dist: Tarball + + @Throws(Exception::class) + suspend fun downloadAndUnpack(into: Path) { + reportProgress { reporter -> + into.createDirectories() + val tarball = downloadTarball(dist, into, reporter) + unpackTarball(tarball, into, reporter) + tarball.delete() + flattenDownloadDir(into, reporter) + } + } + + @JvmRecord + @Serializable + data class Tarball(val tarball: String, val shasum: String, val size: Int) +} + +suspend fun downloadTarball(dist: Tarball, into: Path, reporter: ProgressReporter): Path { + return withContext(Dispatchers.IO) { + val service = DownloadableFileService.getInstance() + val fileName = dist.tarball.substringAfterLast('/') + val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false) + val desc = service.createFileDescription(dist.tarball, tempFile.name) + val downloader = service.createDownloader(listOf(desc), ZigBrainsBundle.message("settings.toolchain.downloader.service.tarball")) + val downloadResults = reporter.sizedStep(100) { + coroutineToIndicator { + downloader.download(into.toFile()) + } + } + if (downloadResults.isEmpty()) + throw IllegalStateException("No file downloaded") + return@withContext downloadResults[0].first.toPath() + } +} + +suspend fun flattenDownloadDir(dir: Path, reporter: ProgressReporter) { + withContext(Dispatchers.IO) { + val contents = Files.newDirectoryStream(dir).use { it.toList() } + if (contents.size == 1 && contents[0].isDirectory()) { + val src = contents[0] + reporter.indeterminateStep { + coroutineToIndicator { + val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator() + indicator.isIndeterminate = true + indicator.text = ZigBrainsBundle.message("settings.toolchain.downloader.progress.flatten") + Files.newDirectoryStream(src).use { stream -> + stream.forEach { + indicator.text2 = it.name + it.move(dir.resolve(src.relativize(it))) + } + } + } + } + src.delete() + } + } +} + +@OptIn(ExperimentalPathApi::class) +suspend fun unpackTarball(tarball: Path, into: Path, reporter: ProgressReporter) { + withContext(Dispatchers.IO) { + try { + reporter.indeterminateStep { + coroutineToIndicator { + Unarchiver.unarchive(tarball, into) + } + } + } catch (e: Throwable) { + tarball.delete() + val contents = Files.newDirectoryStream(into).use { it.toList() } + if (contents.size == 1 && contents[0].isDirectory()) { + contents[0].deleteRecursively() + } + throw e + } + } +} + +fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? { + if (!dist.contains('-')) + return null + val (arch, os) = dist.split('-', limit = 2) + val theArch = when (arch) { + "x86_64" -> CpuArch.X86_64 + "i386", "x86" -> CpuArch.X86 + "armv7a" -> CpuArch.ARM32 + "aarch64" -> CpuArch.ARM64 + else -> return null + } + val theOS = when (os) { + "linux" -> OS.Linux + "windows" -> OS.Windows + "macos" -> OS.macOS + "freebsd" -> OS.FreeBSD + else -> return null + } + if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) { + return null + } + return Json.decodeFromJsonElement(tb) +} + +val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt index f826bf3e..680cc1ef 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt @@ -22,7 +22,7 @@ package com.falsepattern.zigbrains.shared.ipc -import com.falsepattern.zigbrains.direnv.emptyEnv +import com.falsepattern.zigbrains.direnv.Env import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.FileUtil @@ -56,7 +56,7 @@ object IPCUtil { if (SystemInfo.isWindows) { return null } - val mkfifo = emptyEnv + val mkfifo = Env.empty .findAllExecutablesOnPATH("mkfifo") .map { it.pathString } .map(::MKFifo) @@ -67,7 +67,7 @@ object IPCUtil { true } ?: return null - val selectedBash = emptyEnv + val selectedBash = Env.empty .findAllExecutablesOnPATH("bash") .map { it.pathString } .filter { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt new file mode 100644 index 00000000..aa7fa261 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt @@ -0,0 +1,37 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.ui + +import com.falsepattern.zigbrains.shared.UUIDMapSerializable +import com.intellij.openapi.ui.NamedConfigurable +import java.awt.Component +import java.util.* + +interface UUIDComboBoxDriver { + val theMap: UUIDMapSerializable.Converting + suspend fun constructModelList(): List> + fun createContext(model: ZBModel): ZBContext + fun createComboBox(model: ZBModel): ZBComboBox + suspend fun resolvePseudo(context: Component, elem: ListElem.Pseudo): UUID? + fun createNamedConfigurable(uuid: UUID, elem: T): NamedConfigurable +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt new file mode 100644 index 00000000..12e21cc4 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt @@ -0,0 +1,169 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.shared.StorageChangeListener +import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.Presentation +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.observable.util.whenListChanged +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.ui.MasterDetailsComponent +import com.intellij.util.IconUtil +import com.intellij.util.asSafely +import com.intellij.util.concurrency.annotations.RequiresEdt +import kotlinx.coroutines.launch +import java.util.* +import javax.swing.JComponent +import javax.swing.tree.DefaultTreeModel + +abstract class UUIDMapEditor(val driver: UUIDComboBoxDriver): MasterDetailsComponent() { + private var isTreeInitialized = false + private var registered: Boolean = false + private var selectOnNextReload: UUID? = null + private var disposed: Boolean = false + private val changeListener: StorageChangeListener = { this@UUIDMapEditor.listChanged() } + + override fun createComponent(): JComponent { + if (!isTreeInitialized) { + initTree() + isTreeInitialized = true + } + if (!registered) { + driver.theMap.addChangeListener(changeListener) + registered = true + } + return super.createComponent() + } + + override fun createActions(fromPopup: Boolean): List { + val add = object : DumbAwareAction({ ZigBrainsBundle.message("settings.shared.list.add-action.name") }, Presentation.NULL_STRING, IconUtil.addIcon) { + override fun actionPerformed(e: AnActionEvent) { + zigCoroutineScope.launchWithEDT(ModalityState.current()) { + if (disposed) + return@launchWithEDT + val modelList = driver.constructModelList() + val model = ZBModel(modelList) + val context = driver.createContext(model) + val popup = ZBComboBoxPopup(context, null, ::onItemSelected) + model.whenListChanged { + popup.syncWithModelChange() + } + popup.showInBestPositionFor(e.dataContext) + } + } + } + return listOf(add, MyDeleteAction()) + } + + override fun onItemDeleted(item: Any?) { + if (item is UUID) { + driver.theMap.remove(item) + } + super.onItemDeleted(item) + } + + private fun onItemSelected(elem: ListElem) { + if (elem !is ListElem.Pseudo) + return + zigCoroutineScope.launch(myWholePanel.asContextElement()) { + if (disposed) + return@launch + val uuid = driver.resolvePseudo(myWholePanel, elem) + if (uuid != null) { + withEDTContext(myWholePanel.asContextElement()) { + applyUUIDNowOrOnReload(uuid) + } + } + } + } + + override fun reset() { + reloadTree() + super.reset() + } + + override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.shared.list.empty") + + override fun disposeUIResources() { + disposed = true + super.disposeUIResources() + if (registered) { + driver.theMap.removeChangeListener(changeListener) + } + } + + private fun addElem(uuid: UUID, elem: T) { + val node = MyNode(driver.createNamedConfigurable(uuid, elem)) + addNode(node, myRoot) + } + + private fun reloadTree() { + if (disposed) + return + val currentSelection = selectedObject?.asSafely() + selectedNode = null + myRoot.removeAllChildren() + (myTree.model as DefaultTreeModel).reload() + val onReload = selectOnNextReload + selectOnNextReload = null + var hasOnReload = false + driver.theMap.forEach { (uuid, elem) -> + addElem(uuid, elem) + if (uuid == onReload) { + hasOnReload = true + } + } + (myTree.model as DefaultTreeModel).reload() + if (hasOnReload) { + selectedNode = findNodeByObject(myRoot, onReload) + return + } + selectedNode = currentSelection?.let { findNodeByObject(myRoot, it) } + } + + @RequiresEdt + private fun applyUUIDNowOrOnReload(uuid: UUID?) { + selectNodeInTree(uuid) + val currentSelection = selectedObject?.asSafely() + if (uuid != null && uuid != currentSelection) { + selectOnNextReload = uuid + } else { + selectOnNextReload = null + } + } + + private suspend fun listChanged() { + if (disposed) + return + withEDTContext(myWholePanel.asContextElement()) { + reloadTree() + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt new file mode 100644 index 00000000..92f13728 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt @@ -0,0 +1,192 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.StorageChangeListener +import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.observable.util.whenListChanged +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Row +import com.intellij.util.concurrency.annotations.RequiresEdt +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.awt.event.ItemEvent +import java.util.* +import javax.swing.JButton + +abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable { + private val comboBox: ZBComboBox + private var selectOnNextReload: UUID? = null + private val model: ZBModel + private var editButton: JButton? = null + private val changeListener: StorageChangeListener = { this@UUIDMapSelector.listChanged() } + init { + model = ZBModel(emptyList()) + comboBox = driver.createComboBox(model) + comboBox.addItemListener(::itemStateChanged) + driver.theMap.addChangeListener(changeListener) + model.whenListChanged { + zigCoroutineScope.launchWithEDT(comboBox.asContextElement()) { + tryReloadSelection() + } + if (comboBox.isPopupVisible) { + comboBox.isPopupVisible = false + comboBox.isPopupVisible = true + } + } + zigCoroutineScope.launchWithEDT(ModalityState.any()) { + model.updateContents(driver.constructModelList()) + } + } + + protected var selectedUUID: UUID? + get() = comboBox.selectedUUID + set(value) { + zigCoroutineScope.launchWithEDT(ModalityState.any()) { + applyUUIDNowOrOnReload(value) + } + } + + protected val isEmpty: Boolean get() = model.isEmpty + + protected open fun onSelection(uuid: UUID?) {} + + private fun refreshButtonState(item: ListElem<*>) { + val actual = item is ListElem.One.Actual<*> + editButton?.isEnabled = actual + editButton?.repaint() + onSelection(if (actual) (item as ListElem.One.Actual<*>).uuid else null) + } + + private fun itemStateChanged(event: ItemEvent) { + if (event.stateChange != ItemEvent.SELECTED) { + return + } + val item = event.item + if (item !is ListElem<*>) + return + refreshButtonState(item) + if (item !is ListElem.Pseudo<*>) + return + @Suppress("UNCHECKED_CAST") + item as ListElem.Pseudo + zigCoroutineScope.launch(comboBox.asContextElement()) { + val uuid = runCatching { driver.resolvePseudo(comboBox, item) }.getOrNull() + delay(100) + withEDTContext(comboBox.asContextElement()) { + applyUUIDNowOrOnReload(uuid) + } + } + } + + @RequiresEdt + private fun tryReloadSelection() { + val list = model.toList() + if (list.size == 1) { + comboBox.selectedItem = list[0] + comboBox.isEnabled = false + return + } + comboBox.isEnabled = true + val onReload = selectOnNextReload + selectOnNextReload = null + if (onReload != null) { + val element = list.firstOrNull { when(it) { + is ListElem.One.Actual<*> -> it.uuid == onReload + else -> false + } } + if (element == null) { + selectOnNextReload = onReload + } else { + comboBox.selectedItem = element + return + } + } + val selected = model.selected + if (selected != null && list.contains(selected)) { + comboBox.selectedItem = selected + return + } + if (selected is ListElem.One.Actual<*>) { + val uuid = selected.uuid + val element = list.firstOrNull { when(it) { + is ListElem.One.Actual -> it.uuid == uuid + else -> false + } } + comboBox.selectedItem = element + return + } + comboBox.selectedItem = ListElem.None() + } + + protected suspend fun listChanged() { + withContext(Dispatchers.EDT + comboBox.asContextElement()) { + val list = driver.constructModelList() + model.updateContents(list) + tryReloadSelection() + } + } + + protected fun attachComboBoxRow(row: Row): Unit = with(row) { + cell(comboBox).resizableColumn().align(AlignX.FILL) + button(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.edit-button.name")) { e -> + zigCoroutineScope.launchWithEDT(comboBox.asContextElement()) { + var selectedUUID = comboBox.selectedUUID ?: return@launchWithEDT + val elem = driver.theMap[selectedUUID] ?: return@launchWithEDT + val config = driver.createNamedConfigurable(selectedUUID, elem) + val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(comboBox)?.contentPane, config) + if (apply) { + applyUUIDNowOrOnReload(selectedUUID) + } + } + }.component.let { + editButton = it + } + } + + @RequiresEdt + private fun applyUUIDNowOrOnReload(uuid: UUID?) { + comboBox.selectedUUID = uuid + if (uuid != null && comboBox.selectedUUID == null) { + selectOnNextReload = uuid + } else { + selectOnNextReload = null + } + } + + override fun dispose() { + zigToolchainList.removeChangeListener(changeListener) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt new file mode 100644 index 00000000..3148d936 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt @@ -0,0 +1,86 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.ui + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.util.* + + +sealed interface ListElemIn +@Suppress("UNCHECKED_CAST") +sealed interface ListElem : ListElemIn { + sealed interface Pseudo: ListElem + sealed interface One : ListElem { + val instance: T + + @JvmRecord + data class Suggested(override val instance: T): One, Pseudo + + @JvmRecord + data class Actual(val uuid: UUID, override val instance: T): One + } + class None private constructor(): ListElem { + companion object { + private val INSTANCE = None() + operator fun invoke(): None { + return INSTANCE as None + } + } + } + class Download private constructor(): ListElem, Pseudo { + companion object { + private val INSTANCE = Download() + operator fun invoke(): Download { + return INSTANCE as Download + } + } + } + class FromDisk private constructor(): ListElem, Pseudo { + companion object { + private val INSTANCE = FromDisk() + operator fun invoke(): FromDisk { + return INSTANCE as FromDisk + } + } + } + data class Pending(val elems: Flow>): ListElem + + companion object { + private val fetchGroup: List> = listOf(Download(), FromDisk()) + fun fetchGroup() = fetchGroup as List> + } +} + +@JvmRecord +data class Separator(val text: String, val line: Boolean) : ListElemIn + +fun Pair.asActual() = ListElem.One.Actual(first, second) + +fun T.asSuggested() = ListElem.One.Suggested(this) + +@JvmName("listElemFlowAsPending") +fun Flow>.asPending() = ListElem.Pending(this) + +fun Flow.asPending() = map { it.asSuggested() }.asPending() + diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt new file mode 100644 index 00000000..717a16d1 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt @@ -0,0 +1,288 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.ui + +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.text.StringUtil +import com.intellij.ui.* +import com.intellij.ui.components.panels.OpaquePanel +import com.intellij.ui.popup.list.ComboBoxPopup +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.EmptyIcon +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.awt.BorderLayout +import java.awt.Component +import java.util.* +import java.util.function.Consumer +import javax.accessibility.AccessibleContext +import javax.swing.JList +import javax.swing.border.Border + +class ZBComboBoxPopup( + context: ZBContext, + selected: ListElem?, + onItemSelected: Consumer>, +) : ComboBoxPopup>(context, selected, onItemSelected) + +open class ZBComboBox(model: ZBModel, renderer: (() -> ZBModel)-> ZBCellRenderer): ComboBox>(model) { + init { + setRenderer(renderer { model }) + } + + var selectedUUID: UUID? + set(value) { + if (value == null) { + selectedItem = ListElem.None() + return + } + for (i in 0..() + } + get() { + val item = selectedItem + return when(item) { + is ListElem.One.Actual<*> -> item.uuid + else -> null + } + } +} + +class ZBModel private constructor(elements: List>, private var separators: MutableMap, Separator>) : CollectionComboBoxModel>(elements) { + private var counter: Int = 0 + companion object { + operator fun invoke(input: List>): ZBModel { + val (elements, separators) = convert(input) + val model = ZBModel(elements, separators) + model.launchPendingResolve() + return model + } + + private fun convert(input: List>): Pair>, MutableMap, Separator>> { + val separators = IdentityHashMap, Separator>() + var lastSeparator: Separator? = null + val elements = ArrayList>() + input.forEach { + when (it) { + is ListElem -> { + if (lastSeparator != null) { + separators[it] = lastSeparator + lastSeparator = null + } + elements.add(it) + } + + is Separator -> lastSeparator = it + } + } + return elements to separators + } + } + + fun separatorAbove(elem: ListElem) = separators[elem] + + private fun launchPendingResolve() { + runInEdt(ModalityState.any()) { + val counter = this.counter + val size = this.size + for (i in 0.. + insertBefore(elem, newElem, counter) + } + remove(elem, counter) + } + } + } + } + + @RequiresEdt + private fun remove(old: ListElem, oldCounter: Int) { + val newCounter = this@ZBModel.counter + if (oldCounter != newCounter) { + return + } + val index = this@ZBModel.getElementIndex(old) + this@ZBModel.remove(index) + val sep = separators.remove(old) + if (sep != null && this@ZBModel.size > index) { + this@ZBModel.getElementAt(index)?.let { separators[it] = sep } + } + } + + @RequiresEdt + private fun insertBefore(old: ListElem, new: ListElem?, oldCounter: Int) { + val newCounter = this@ZBModel.counter + if (oldCounter != newCounter) { + return + } + if (new == null) { + return + } + val currentIndex = this@ZBModel.getElementIndex(old) + separators.remove(old)?.let { + separators.put(new, it) + } + this@ZBModel.add(currentIndex, new) + } + + @RequiresEdt + fun updateContents(input: List>) { + counter++ + val (elements, separators) = convert(input) + this.separators = separators + replaceAll(elements) + launchPendingResolve() + } +} + +open class ZBContext(private val project: Project?, private val model: ZBModel, private val getRenderer: (() -> ZBModel) -> ZBCellRenderer) : ComboBoxPopup.Context> { + override fun getProject(): Project? { + return project + } + + override fun getModel(): ZBModel { + return model + } + + override fun getRenderer(): ZBCellRenderer { + return getRenderer(::getModel) + } +} + +abstract class ZBCellRenderer(val getModel: () -> ZBModel) : ColoredListCellRenderer>() { + final override fun getListCellRendererComponent( + list: JList?>?, + value: ListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ): Component? { + val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent + val panel = object : CellRendererPanel(BorderLayout()) { + val myContext = component.accessibleContext + + override fun getAccessibleContext(): AccessibleContext? { + return myContext + } + + override fun setBorder(border: Border?) { + component.border = border + } + } + panel.add(component, BorderLayout.CENTER) + + component.isOpaque = true + list?.let { background = if (selected) it.selectionBackground else it.background } + + val model = getModel() + + if (index == -1) { + component.isOpaque = false + panel.isOpaque = false + return panel + } + + val separator = value?.let { model.separatorAbove(it) } + + if (separator != null) { + val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3 + val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0)) + separatorComponent.isHideLine = !separator.line + separatorComponent.caption = separator.text.ifBlank { null } + val wrapper = OpaquePanel(BorderLayout()) + wrapper.add(separatorComponent, BorderLayout.CENTER) + list?.let { wrapper.background = it.background } + panel.add(wrapper, BorderLayout.NORTH) + } + + return panel + } + + abstract override fun customizeCellRenderer( + list: JList?>, + value: ListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) +} + +fun renderPathNameComponent(path: String, name: String?, nameFallback: String, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { + val path = presentDetectedPath(path) + val primary: String + var secondary: String? + val tooltip: String? + if (isSuggestion) { + primary = path + secondary = name + } else { + primary = name ?: nameFallback + secondary = path + } + if (isSelected) { + tooltip = secondary + secondary = null + } else { + tooltip = null + } + component.append(primary) + if (secondary != null) { + component.append(" ") + component.append(secondary, SimpleTextAttributes.GRAYED_ATTRIBUTES) + } + component.toolTipText = tooltip +} + +fun presentDetectedPath(home: String, maxLength: Int = 50, suffixLength: Int = 30): String { + //for macOS, let's try removing Bundle internals + var home = home + home = StringUtil.trimEnd(home, "/Contents/Home") //NON-NLS + home = StringUtil.trimEnd(home, "/Contents/MacOS") //NON-NLS + home = FileUtil.getLocationRelativeToUserHome(home, false) + home = StringUtil.shortenTextWithEllipsis(home, maxLength, suffixLength) + return home +} + +private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file diff --git a/core/src/main/resources/META-INF/zigbrains-core.xml b/core/src/main/resources/META-INF/zigbrains-core.xml index 45220600..b3eb4bc2 100644 --- a/core/src/main/resources/META-INF/zigbrains-core.xml +++ b/core/src/main/resources/META-INF/zigbrains-core.xml @@ -142,7 +142,15 @@ parentId="language" instance="com.falsepattern.zigbrains.project.settings.ZigConfigurable" id="ZigConfigurable" - displayName="Zig" + bundle="zigbrains.Bundle" + key="settings.project.display-name" + /> + @@ -177,10 +185,13 @@ + diff --git a/core/src/main/resources/zigbrains/Bundle.properties b/core/src/main/resources/zigbrains/Bundle.properties index 48b56bf6..1b05b9f3 100644 --- a/core/src/main/resources/zigbrains/Bundle.properties +++ b/core/src/main/resources/zigbrains/Bundle.properties @@ -110,3 +110,47 @@ build.tool.window.status.error.general=Error while running zig build -l build.tool.window.status.no-builds=No builds currently in progress build.tool.window.status.timeout=zig build -l timed out after {0} seconds. zig=Zig +settings.shared.list.add-action.name=Add New +settings.shared.list.empty=Select an entry to view or edit its details here +settings.shared.downloader.version.label=Version: +settings.shared.downloader.location.label=Location: +settings.shared.downloader.ok-action=Download +settings.shared.downloader.state.invalid=Invalid path +settings.shared.downloader.state.not-absolute=Must be an absolute path +settings.shared.downloader.state.not-directory=Path is not a directory +settings.shared.downloader.state.not-empty=Directory is not empty +settings.shared.downloader.state.create-new=Directory will be created +settings.shared.downloader.state.ok=Directory OK +settings.shared.downloader.archive-size.text=Archive size: {0} +settings.shared.local-selector.name.label=Name: +settings.shared.local-selector.path.label=Path: +settings.shared.local-selector.ok-action=Add +settings.shared.local-selector.state.invalid=Invalid path +settings.project.display-name=Zig +settings.toolchain.base.name.label=Name +settings.toolchain.local.path.label=Toolchain location +settings.toolchain.local.version.label=Detected zig version +settings.toolchain.local.std.label=Override standard library +settings.toolchain.editor.toolchain.label=Toolchain +settings.toolchain.editor.toolchain-default.label=Default toolchain +settings.toolchain.editor.toolchain.edit-button.name=Edit +settings.toolchain.model.detected.separator=Detected toolchains +settings.toolchain.model.none.text= +settings.toolchain.model.loading.text=Loading\u2026 +settings.toolchain.model.from-disk.text=Add Zig from disk\u2026 +settings.toolchain.model.download.text=Download Zig\u2026 +settings.toolchain.list.title=Toolchains +settings.toolchain.downloader.title=Install Zig +settings.toolchain.downloader.progress.fetch=Fetching zig version information +settings.toolchain.downloader.progress.install=Installing Zig {0} +settings.toolchain.downloader.progress.flatten=Flattening unpacked archive +settings.toolchain.downloader.chooser.title=Zig Install Directory +settings.toolchain.downloader.service.index=Zig version information +settings.toolchain.downloader.service.tarball=Zig archive +settings.toolchain.local-selector.title=Select Zig From Disk +settings.toolchain.local-selector.chooser.title=Zig Installation Directory +settings.toolchain.local-selector.state.invalid=Invalid toolchain path +settings.toolchain.local-selector.state.already-exists-unnamed=Toolchain already exists +settings.toolchain.local-selector.state.already-exists-named=Toolchain already exists as "{0}" +settings.toolchain.local-selector.state.ok=Toolchain path OK +settings.direnv.enable.label=Direnv diff --git a/gradle.properties b/gradle.properties index e6e7efbd..90952bdf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ pluginName=ZigBrains pluginRepositoryUrl=https://github.com/FalsePattern/ZigBrains -pluginVersion=24.0.1 +pluginVersion=25.0.0 pluginSinceBuild=242 pluginUntilBuild=242.* @@ -14,7 +14,7 @@ javaVersion=21 runIdeTarget=clion lsp4jVersion=0.21.1 -lsp4ijVersion=0.11.0 +lsp4ijVersion=0.12.0 lsp4ijNightly=false kotlin.stdlib.default.dependency=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9530d66f5e68d973ea569d8e19de379189..9bbc975c742b298b441bfb90dbc124400a3751b9 100644 GIT binary patch delta 34744 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 delta 34727 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�{ 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 a%EQ2*fnt2>. + */ + +package com.falsepattern.zigbrains.lsp + +import com.intellij.openapi.util.IconLoader +import org.jetbrains.annotations.NonNls + +@NonNls +object LSPIcons { + @JvmField + val ZLS = IconLoader.getIcon("/icons/zls.svg", LSPIcons::class.java) +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ToolchainZLSConfigProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ToolchainZLSConfigProvider.kt similarity index 77% rename from lsp/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ToolchainZLSConfigProvider.kt rename to lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ToolchainZLSConfigProvider.kt index 0b24a3d0..300dd6cd 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ToolchainZLSConfigProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ToolchainZLSConfigProvider.kt @@ -20,24 +20,22 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain +package com.falsepattern.zigbrains.lsp import com.falsepattern.zigbrains.lsp.config.SuspendingZLSConfigProvider import com.falsepattern.zigbrains.lsp.config.ZLSConfig -import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.project.Project -import com.intellij.openapi.util.UserDataHolderBase import com.intellij.openapi.util.io.toNioPathOrNull import kotlin.io.path.pathString - class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider { override suspend fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig { - val svc = project.zigProjectSettings - var state = svc.state - val toolchain = state.toolchain ?: ZigToolchainProvider.suggestToolchain(project, UserDataHolderBase()) ?: return previous + val svc = ZigToolchainService.getInstance(project) + val toolchain = svc.toolchain ?: return previous val env = toolchain.zig.getEnv(project).getOrElse { throwable -> throwable.printStackTrace() @@ -65,16 +63,10 @@ class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider { ).notify(project) return previous } - var lib = if (state.overrideStdPath && state.explicitPathToStd != null) { - state.explicitPathToStd?.toNioPathOrNull() ?: run { - Notification( - "zigbrains-lsp", - "Invalid zig standard library path override: ${state.explicitPathToStd}", - NotificationType.ERROR - ).notify(project) - null - } - } else null + var lib = if (toolchain is LocalZigToolchain) + toolchain.std + else + null if (lib == null) { lib = env.libDirectory.toNioPathOrNull() ?: run { diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt index d197dd30..05c6aec3 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt @@ -22,36 +22,26 @@ package com.falsepattern.zigbrains.lsp -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv -import com.falsepattern.zigbrains.lsp.settings.zlsSettings -import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.lsp.zls.zls import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity import com.intellij.ui.EditorNotifications import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlin.io.path.pathString class ZLSStartup: ProjectActivity { override suspend fun execute(project: Project) { - val zlsState = project.zlsSettings.state - if (zlsState.zlsPath.isBlank()) { - val env = if (DirenvCmd.direnvInstalled() && !project.isDefault && project.zigProjectSettings.state.direnv) - project.getDirenv() - else - emptyEnv - env.findExecutableOnPATH("zls")?.let { - zlsState.zlsPath = it.pathString - project.zlsSettings.state = zlsState - } - } project.zigCoroutineScope.launch { - var currentState = project.zlsRunningAsync() + var currentState = project.zlsRunning() + var currentZLS = project.zls while (!project.isDisposed) { - val running = project.zlsRunningAsync() + val zls = project.zls + if (currentZLS != zls) { + startLSP(project, true) + } + currentZLS = zls + val running = project.zlsRunning() if (currentState != running) { EditorNotifications.getInstance(project).updateAllNotifications() } diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt index 6470d863..326102ed 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt @@ -22,11 +22,8 @@ package com.falsepattern.zigbrains.lsp -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv import com.falsepattern.zigbrains.lsp.config.ZLSConfigProviderBase -import com.falsepattern.zigbrains.lsp.settings.zlsSettings -import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.lsp.zls.zls import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.notification.Notification import com.intellij.notification.NotificationType @@ -55,30 +52,8 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje @OptIn(ExperimentalSerializationApi::class) suspend fun getCommand(project: Project): List? { - val svc = project.zlsSettings - val state = svc.state - val zlsPath: Path = state.zlsPath.let { zlsPath -> - if (zlsPath.isEmpty()) { - val env = if (project.zigProjectSettings.state.direnv) project.getDirenv() else emptyEnv - env.findExecutableOnPATH("zls") ?: run { - Notification( - "zigbrains-lsp", - ZLSBundle.message("notification.message.could-not-detect.content"), - NotificationType.ERROR - ).notify(project) - return null - } - } else { - zlsPath.toNioPathOrNull() ?: run { - Notification( - "zigbrains-lsp", - ZLSBundle.message("notification.message.zls-exe-path-invalid.content", zlsPath), - NotificationType.ERROR - ).notify(project) - return null - } - } - } + val zls = project.zls ?: return null + val zlsPath: Path = zls.path if (!zlsPath.toFile().exists()) { Notification( "zigbrains-lsp", @@ -95,7 +70,7 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje ).notify(project) return null } - val configPath: Path? = state.zlsConfigPath.let { configPath -> + val configPath: Path? = "".let { configPath -> if (configPath.isNotBlank()) { configPath.toNioPathOrNull()?.let { nioPath -> if (!nioPath.toFile().exists()) { diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt index c46e9ac7..8cc3b328 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt @@ -22,7 +22,7 @@ package com.falsepattern.zigbrains.lsp -import com.falsepattern.zigbrains.lsp.settings.zlsSettings +import com.falsepattern.zigbrains.lsp.zls.zls import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -68,39 +68,29 @@ class ZigLanguageServerFactory: LanguageServerFactory, LanguageServerEnablementS } features.inlayHintFeature = object: LSPInlayHintFeature() { override fun isEnabled(file: PsiFile): Boolean { - return features.project.zlsSettings.state.inlayHints + return project.zls?.settings?.inlayHints == true } } return features } - override fun isEnabled(project: Project) = project.zlsEnabledSync() + override fun isEnabled(project: Project) = project.zlsEnabled() override fun setEnabled(enabled: Boolean, project: Project) { project.zlsEnabled(enabled) } } -suspend fun Project.zlsEnabledAsync(): Boolean { - return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateAsync() -} - -fun Project.zlsEnabledSync(): Boolean { - return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateSync() +fun Project.zlsEnabled(): Boolean { + return (getUserData(ENABLED_KEY) != false) && zls?.isValid() == true } fun Project.zlsEnabled(value: Boolean) { putUserData(ENABLED_KEY, value) } -suspend fun Project.zlsRunningAsync(): Boolean { - if (!zlsEnabledAsync()) - return false - return lsm.isRunning -} - -fun Project.zlsRunningSync(): Boolean { - if (!zlsEnabledSync()) +fun Project.zlsRunning(): Boolean { + if (!zlsEnabled()) return false return lsm.isRunning } @@ -135,7 +125,7 @@ private suspend fun doStart(project: Project, restart: Boolean) { project.lsm.stop("ZigBrains") delay(250) } - if (project.zlsSettings.validateAsync()) { + if (project.zls?.isValid() == true) { delay(250) project.lsm.start("ZigBrains") } diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt index a812b8b8..599ecacb 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt @@ -23,8 +23,8 @@ package com.falsepattern.zigbrains.lsp.notification import com.falsepattern.zigbrains.lsp.ZLSBundle -import com.falsepattern.zigbrains.lsp.settings.zlsSettings -import com.falsepattern.zigbrains.lsp.zlsRunningAsync +import com.falsepattern.zigbrains.lsp.zls.zls +import com.falsepattern.zigbrains.lsp.zlsRunning import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.zig.ZigFileType import com.falsepattern.zigbrains.zon.ZonFileType @@ -49,10 +49,10 @@ class ZigEditorNotificationProvider: EditorNotificationProvider, DumbAware { else -> return null } val task = project.zigCoroutineScope.async { - if (project.zlsRunningAsync()) { + if (project.zlsRunning()) { return@async null } else { - return@async project.zlsSettings.validateAsync() + return@async project.zls?.isValid() == true } } return Function { editor -> diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt deleted file mode 100644 index 819a0bc2..00000000 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.lsp.settings - -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv -import com.falsepattern.zigbrains.lsp.ZLSBundle -import com.falsepattern.zigbrains.lsp.startLSP -import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.intellij.ide.IdeEventQueue -import com.intellij.openapi.components.* -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.platform.ide.progress.ModalTaskOwner -import com.intellij.platform.ide.progress.runWithModalProgressBlocking -import com.intellij.util.application -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import java.nio.file.Path -import kotlin.io.path.isExecutable -import kotlin.io.path.isRegularFile - -@Service(Service.Level.PROJECT) -@State( - name = "ZLSSettings", - storages = [Storage(value = "zigbrains.xml")] -) -class ZLSProjectSettingsService(val project: Project): PersistentStateComponent { - @Volatile - private var state = ZLSSettings() - @Volatile - private var dirty = true - @Volatile - private var valid = false - - private val mutex = Mutex() - override fun getState(): ZLSSettings { - return state.copy() - } - - fun setState(value: ZLSSettings) { - runBlocking { - mutex.withLock { - this@ZLSProjectSettingsService.state = value - dirty = true - } - } - startLSP(project, true) - } - - override fun loadState(state: ZLSSettings) { - setState(state) - } - - suspend fun validateAsync(): Boolean { - mutex.withLock { - if (dirty) { - val state = this.state - valid = doValidate(project, state) - dirty = false - } - return valid - } - } - - fun validateSync(): Boolean { - val isValid: Boolean? = runBlocking { - mutex.withLock { - if (dirty) - null - else - valid - } - } - if (isValid != null) { - return isValid - } - return if (useModalProgress()) { - runWithModalProgressBlocking(ModalTaskOwner.project(project), ZLSBundle.message("progress.title.validate")) { - validateAsync() - } - } else { - runBlocking { - validateAsync() - } - } - } -} - -private val prohibitClass: Class<*>? = runCatching { - Class.forName("com_intellij_ide_ProhibitAWTEvents".replace('_', '.')) -}.getOrNull() - -private val postProcessors: List<*>? = runCatching { - if (prohibitClass == null) - return@runCatching null - val postProcessorsField = IdeEventQueue::class.java.getDeclaredField("postProcessors") - postProcessorsField.isAccessible = true - postProcessorsField.get(IdeEventQueue.getInstance()) as? List<*> -}.getOrNull() - -private fun useModalProgress(): Boolean { - if (!application.isDispatchThread) - return false - - if (application.isWriteAccessAllowed) - return false - - if (postProcessors == null) - return true - - return postProcessors.none { prohibitClass!!.isInstance(it) } -} - -private suspend fun doValidate(project: Project, state: ZLSSettings): Boolean { - val zlsPath: Path = state.zlsPath.let { zlsPath -> - if (zlsPath.isEmpty()) { - val env = if (project.zigProjectSettings.state.direnv) project.getDirenv() else emptyEnv - env.findExecutableOnPATH("zls") ?: run { - return false - } - } else { - zlsPath.toNioPathOrNull() ?: run { - return false - } - } - } - if (!zlsPath.toFile().exists()) { - return false - } - if (!zlsPath.isRegularFile() || !zlsPath.isExecutable()) { - return false - } - return true -} - -val Project.zlsSettings get() = service() \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt index d7f2f26e..1c462864 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt @@ -23,37 +23,31 @@ package com.falsepattern.zigbrains.lsp.settings import com.falsepattern.zigbrains.lsp.config.SemanticTokens -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.annotations.Attribute import org.jetbrains.annotations.NonNls @Suppress("PropertyName") data class ZLSSettings( - var zlsPath: @NonNls String = "", - var zlsConfigPath: @NonNls String = "", - val inlayHints: Boolean = true, - val enable_snippets: Boolean = true, - val enable_argument_placeholders: Boolean = true, - val completion_label_details: Boolean = true, - val enable_build_on_save: Boolean = false, - val build_on_save_args: String = "", - val semantic_tokens: SemanticTokens = SemanticTokens.full, - val inlay_hints_show_variable_type_hints: Boolean = true, - val inlay_hints_show_struct_literal_field_type: Boolean = true, - val inlay_hints_show_parameter_name: Boolean = true, - val inlay_hints_show_builtin: Boolean = true, - val inlay_hints_exclude_single_argument: Boolean = true, - val inlay_hints_hide_redundant_param_names: Boolean = false, - val inlay_hints_hide_redundant_param_names_last_token: Boolean = false, - val warn_style: Boolean = false, - val highlight_global_var_declarations: Boolean = false, - val skip_std_references: Boolean = false, - val prefer_ast_check_as_child_process: Boolean = true, - val builtin_path: String? = null, - val build_runner_path: @NonNls String? = null, - val global_cache_path: @NonNls String? = null, -): ZigProjectConfigurationProvider.Settings { - override fun apply(project: Project) { - project.zlsSettings.loadState(this) - } -} + @JvmField @Attribute val zlsConfigPath: @NonNls String = "", + @JvmField @Attribute val inlayHints: Boolean = true, + @JvmField @Attribute val enable_snippets: Boolean = true, + @JvmField @Attribute val enable_argument_placeholders: Boolean = true, + @JvmField @Attribute val completion_label_details: Boolean = true, + @JvmField @Attribute val enable_build_on_save: Boolean = false, + @JvmField @Attribute val build_on_save_args: String = "", + @JvmField @Attribute val semantic_tokens: SemanticTokens = SemanticTokens.full, + @JvmField @Attribute val inlay_hints_show_variable_type_hints: Boolean = true, + @JvmField @Attribute val inlay_hints_show_struct_literal_field_type: Boolean = true, + @JvmField @Attribute val inlay_hints_show_parameter_name: Boolean = true, + @JvmField @Attribute val inlay_hints_show_builtin: Boolean = true, + @JvmField @Attribute val inlay_hints_exclude_single_argument: Boolean = true, + @JvmField @Attribute val inlay_hints_hide_redundant_param_names: Boolean = false, + @JvmField @Attribute val inlay_hints_hide_redundant_param_names_last_token: Boolean = false, + @JvmField @Attribute val warn_style: Boolean = false, + @JvmField @Attribute val highlight_global_var_declarations: Boolean = false, + @JvmField @Attribute val skip_std_references: Boolean = false, + @JvmField @Attribute val prefer_ast_check_as_child_process: Boolean = true, + @JvmField @Attribute val builtin_path: String? = null, + @JvmField @Attribute val build_runner_path: @NonNls String? = null, + @JvmField @Attribute val global_cache_path: @NonNls String? = null, +) \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt index b11e3ab4..aa1aaabb 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt @@ -24,12 +24,13 @@ package com.falsepattern.zigbrains.lsp.settings import com.falsepattern.zigbrains.lsp.config.ZLSConfig import com.falsepattern.zigbrains.lsp.config.ZLSConfigProvider +import com.falsepattern.zigbrains.lsp.zls.zls import com.falsepattern.zigbrains.shared.cli.translateCommandline import com.intellij.openapi.project.Project class ZLSSettingsConfigProvider: ZLSConfigProvider { override fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig { - val state = project.zlsSettings.state + val state = project.zls?.settings ?: return previous return previous.copy( enable_snippets = state.enable_snippets, enable_argument_placeholders = state.enable_argument_placeholders, diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt deleted file mode 100644 index fad06b27..00000000 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.lsp.settings - -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.shared.SubConfigurable -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer -import com.intellij.ui.dsl.builder.Panel - -class ZLSSettingsConfigurable(private val project: Project): SubConfigurable { - private var appSettingsComponent: ZLSSettingsPanel? = null - override fun createComponent(holder: ZigProjectConfigurationProvider.SettingsPanelHolder, panel: Panel): ZigProjectConfigurationProvider.SettingsPanel { - val settingsPanel = ZLSSettingsPanel(project).apply { attach(panel) }.also { Disposer.register(this, it) } - appSettingsComponent = settingsPanel - return settingsPanel - } - - override fun isModified(): Boolean { - val data = appSettingsComponent?.data ?: return false - return project.zlsSettings.state != data - } - - override fun apply() { - val data = appSettingsComponent?.data ?: return - val settings = project.zlsSettings - settings.state = data - } - - override fun reset() { - appSettingsComponent?.data = project.zlsSettings.state - } - - override fun dispose() { - appSettingsComponent = null - } -} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt index 2806228c..08daa015 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt @@ -22,74 +22,27 @@ package com.falsepattern.zigbrains.lsp.settings -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.direnv.Env -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv import com.falsepattern.zigbrains.lsp.ZLSBundle import com.falsepattern.zigbrains.lsp.config.SemanticTokens -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.falsepattern.zigbrains.shared.cli.call -import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe -import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext -import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.execution.processTools.mapFlat -import com.intellij.openapi.application.ModalityState +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.openapi.vfs.toNioPathOrNull -import com.intellij.platform.ide.progress.ModalTaskOwner -import com.intellij.platform.ide.progress.TaskCancellation -import com.intellij.platform.ide.progress.withModalProgress -import com.intellij.ui.DocumentAdapter -import com.intellij.ui.JBColor import com.intellij.ui.components.JBCheckBox -import com.intellij.ui.components.JBTextArea import com.intellij.ui.components.fields.ExtendableTextField import com.intellij.ui.components.textFieldWithBrowseButton import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.Row -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import org.jetbrains.annotations.PropertyKey -import javax.swing.event.DocumentEvent -import kotlin.io.path.pathString @Suppress("PrivatePropertyName") -class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationProvider.SettingsPanel { - private val zlsPath = textFieldWithBrowseButton( - project, - ZLSBundle.message("settings.zls-path.browse.title"), - FileChooserDescriptorFactory.createSingleFileDescriptor(), - ).also { - it.textField.document.addDocumentListener(object: DocumentAdapter() { - override fun textChanged(p0: DocumentEvent) { - dispatchUpdateUI() - } - }) - Disposer.register(this, it) - } +class ZLSSettingsPanel() : ImmutableElementPanel { private val zlsConfigPath = textFieldWithBrowseButton( - project, + null, ZLSBundle.message("settings.zls-config-path.browse.title"), - FileChooserDescriptorFactory.createSingleFileDescriptor() + FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() ).also { Disposer.register(this, it) } - - private val zlsVersion = JBTextArea().also { it.isEditable = false } - - private var debounce: Job? = null - - private var direnv: Boolean = project.zigProjectSettings.state.direnv - private val inlayHints = JBCheckBox() private val enable_snippets = JBCheckBox() private val enable_argument_placeholders = JBCheckBox() @@ -112,121 +65,113 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr private val build_runner_path = ExtendableTextField() private val global_cache_path = ExtendableTextField() - override fun attach(p: Panel) = with(p) { - if (!project.isDefault) { - group(ZLSBundle.message("settings.group.title")) { - fancyRow( - "settings.zls-path.label", - "settings.zls-path.tooltip" - ) { - cell(zlsPath).resizableColumn().align(AlignX.FILL) - } - row(ZLSBundle.message("settings.zls-version.label")) { - cell(zlsVersion) - } - fancyRow( - "settings.zls-config-path.label", - "settings.zls-config-path.tooltip" - ) { cell(zlsConfigPath).align(AlignX.FILL) } - fancyRow( - "settings.enable_snippets.label", - "settings.enable_snippets.tooltip" - ) { cell(enable_snippets) } - fancyRow( - "settings.enable_argument_placeholders.label", - "settings.enable_argument_placeholders.tooltip" - ) { cell(enable_argument_placeholders) } - fancyRow( - "settings.completion_label_details.label", - "settings.completion_label_details.tooltip" - ) { cell(completion_label_details) } - fancyRow( - "settings.enable_build_on_save.label", - "settings.enable_build_on_save.tooltip" - ) { cell(enable_build_on_save) } - fancyRow( - "settings.build_on_save_args.label", - "settings.build_on_save_args.tooltip" - ) { cell(build_on_save_args).resizableColumn().align(AlignX.FILL) } - fancyRow( - "settings.semantic_tokens.label", - "settings.semantic_tokens.tooltip" - ) { cell(semantic_tokens) } - group(ZLSBundle.message("settings.inlay-hints-group.label")) { - fancyRow( - "settings.inlay-hints-enable.label", - "settings.inlay-hints-enable.tooltip" - ) { cell(inlayHints) } - fancyRow( - "settings.inlay_hints_show_variable_type_hints.label", - "settings.inlay_hints_show_variable_type_hints.tooltip" - ) { cell(inlay_hints_show_variable_type_hints) } - fancyRow( - "settings.inlay_hints_show_struct_literal_field_type.label", - "settings.inlay_hints_show_struct_literal_field_type.tooltip" - ) { cell(inlay_hints_show_struct_literal_field_type) } - fancyRow( - "settings.inlay_hints_show_parameter_name.label", - "settings.inlay_hints_show_parameter_name.tooltip" - ) { cell(inlay_hints_show_parameter_name) } - fancyRow( - "settings.inlay_hints_show_builtin.label", - "settings.inlay_hints_show_builtin.tooltip" - ) { cell(inlay_hints_show_builtin) } - fancyRow( - "settings.inlay_hints_exclude_single_argument.label", - "settings.inlay_hints_exclude_single_argument.tooltip" - ) { cell(inlay_hints_exclude_single_argument) } - fancyRow( - "settings.inlay_hints_hide_redundant_param_names.label", - "settings.inlay_hints_hide_redundant_param_names.tooltip" - ) { cell(inlay_hints_hide_redundant_param_names) } - fancyRow( - "settings.inlay_hints_hide_redundant_param_names_last_token.label", - "settings.inlay_hints_hide_redundant_param_names_last_token.tooltip" - ) { cell(inlay_hints_hide_redundant_param_names_last_token) } - } - fancyRow( - "settings.warn_style.label", - "settings.warn_style.tooltip" - ) { cell(warn_style) } - fancyRow( - "settings.highlight_global_var_declarations.label", - "settings.highlight_global_var_declarations.tooltip" - ) { cell(highlight_global_var_declarations) } - fancyRow( - "settings.skip_std_references.label", - "settings.skip_std_references.tooltip" - ) { cell(skip_std_references) } - fancyRow( - "settings.prefer_ast_check_as_child_process.label", - "settings.prefer_ast_check_as_child_process.tooltip" - ) { cell(prefer_ast_check_as_child_process) } - fancyRow( - "settings.builtin_path.label", - "settings.builtin_path.tooltip" - ) { cell(builtin_path).resizableColumn().align(AlignX.FILL) } - fancyRow( - "settings.build_runner_path.label", - "settings.build_runner_path.tooltip" - ) { cell(build_runner_path).resizableColumn().align(AlignX.FILL) } - fancyRow( - "settings.global_cache_path.label", - "settings.global_cache_path.tooltip" - ) { cell(global_cache_path).resizableColumn().align(AlignX.FILL) } - } + override fun attach(p: Panel): Unit = with(p) { + fancyRow( + "settings.zls-config-path.label", + "settings.zls-config-path.tooltip" + ) { cell(zlsConfigPath).align(AlignX.FILL) } + fancyRow( + "settings.enable_snippets.label", + "settings.enable_snippets.tooltip" + ) { cell(enable_snippets) } + fancyRow( + "settings.enable_argument_placeholders.label", + "settings.enable_argument_placeholders.tooltip" + ) { cell(enable_argument_placeholders) } + fancyRow( + "settings.completion_label_details.label", + "settings.completion_label_details.tooltip" + ) { cell(completion_label_details) } + fancyRow( + "settings.enable_build_on_save.label", + "settings.enable_build_on_save.tooltip" + ) { cell(enable_build_on_save) } + fancyRow( + "settings.build_on_save_args.label", + "settings.build_on_save_args.tooltip" + ) { cell(build_on_save_args).resizableColumn().align(AlignX.FILL) } + fancyRow( + "settings.semantic_tokens.label", + "settings.semantic_tokens.tooltip" + ) { cell(semantic_tokens) } + collapsibleGroup(ZLSBundle.message("settings.inlay-hints-group.label"), indent = false) { + fancyRow( + "settings.inlay-hints-enable.label", + "settings.inlay-hints-enable.tooltip" + ) { cell(inlayHints) } + fancyRow( + "settings.inlay_hints_show_variable_type_hints.label", + "settings.inlay_hints_show_variable_type_hints.tooltip" + ) { cell(inlay_hints_show_variable_type_hints) } + fancyRow( + "settings.inlay_hints_show_struct_literal_field_type.label", + "settings.inlay_hints_show_struct_literal_field_type.tooltip" + ) { cell(inlay_hints_show_struct_literal_field_type) } + fancyRow( + "settings.inlay_hints_show_parameter_name.label", + "settings.inlay_hints_show_parameter_name.tooltip" + ) { cell(inlay_hints_show_parameter_name) } + fancyRow( + "settings.inlay_hints_show_builtin.label", + "settings.inlay_hints_show_builtin.tooltip" + ) { cell(inlay_hints_show_builtin) } + fancyRow( + "settings.inlay_hints_exclude_single_argument.label", + "settings.inlay_hints_exclude_single_argument.tooltip" + ) { cell(inlay_hints_exclude_single_argument) } + fancyRow( + "settings.inlay_hints_hide_redundant_param_names.label", + "settings.inlay_hints_hide_redundant_param_names.tooltip" + ) { cell(inlay_hints_hide_redundant_param_names) } + fancyRow( + "settings.inlay_hints_hide_redundant_param_names_last_token.label", + "settings.inlay_hints_hide_redundant_param_names_last_token.tooltip" + ) { cell(inlay_hints_hide_redundant_param_names_last_token) } } - dispatchAutodetect(false) + fancyRow( + "settings.warn_style.label", + "settings.warn_style.tooltip" + ) { cell(warn_style) } + fancyRow( + "settings.highlight_global_var_declarations.label", + "settings.highlight_global_var_declarations.tooltip" + ) { cell(highlight_global_var_declarations) } + fancyRow( + "settings.skip_std_references.label", + "settings.skip_std_references.tooltip" + ) { cell(skip_std_references) } + fancyRow( + "settings.prefer_ast_check_as_child_process.label", + "settings.prefer_ast_check_as_child_process.tooltip" + ) { cell(prefer_ast_check_as_child_process) } + fancyRow( + "settings.builtin_path.label", + "settings.builtin_path.tooltip" + ) { cell(builtin_path).resizableColumn().align(AlignX.FILL) } + fancyRow( + "settings.build_runner_path.label", + "settings.build_runner_path.tooltip" + ) { cell(build_runner_path).resizableColumn().align(AlignX.FILL) } + fancyRow( + "settings.global_cache_path.label", + "settings.global_cache_path.tooltip" + ) { cell(global_cache_path).resizableColumn().align(AlignX.FILL) } } - override fun direnvChanged(state: Boolean) { - direnv = state - dispatchAutodetect(true) + override fun isModified(elem: ZLSSettings): Boolean { + return elem != data } - override var data - get() = if (project.isDefault) ZLSSettings() else ZLSSettings( - zlsPath.text, + override fun apply(elem: ZLSSettings): ZLSSettings? { + return data + } + + override fun reset(elem: ZLSSettings?) { + data = elem ?: ZLSSettings() + } + + private var data + get() = ZLSSettings( zlsConfigPath.text, inlayHints.isSelected, enable_snippets.isSelected, @@ -251,7 +196,6 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr global_cache_path.text?.ifBlank { null }, ) set(value) { - zlsPath.text = value.zlsPath zlsConfigPath.text = value.zlsConfigPath inlayHints.isSelected = value.inlayHints enable_snippets.isSelected = value.enable_snippets @@ -275,72 +219,10 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr builtin_path.text = value.builtin_path ?: "" build_runner_path.text = value.build_runner_path ?: "" global_cache_path.text = value.global_cache_path ?: "" - dispatchUpdateUI() } - private fun dispatchAutodetect(force: Boolean) { - project.zigCoroutineScope.launchWithEDT(ModalityState.defaultModalityState()) { - withModalProgress(ModalTaskOwner.component(zlsPath), "Detecting ZLS...", TaskCancellation.cancellable()) { - autodetect(force) - } - } - } - - suspend fun autodetect(force: Boolean) { - if (force || zlsPath.text.isBlank()) { - getDirenv().findExecutableOnPATH("zls")?.let { - if (force || zlsPath.text.isBlank()) { - zlsPath.text = it.pathString - dispatchUpdateUI() - } - } - } - } - override fun dispose() { - debounce?.cancel("Disposed") - } - - private suspend fun getDirenv(): Env { - if (!project.isDefault && DirenvCmd.direnvInstalled() && direnv) - return project.getDirenv() - return emptyEnv - } - - private fun dispatchUpdateUI() { - debounce?.cancel("New debounce") - debounce = project.zigCoroutineScope.launch { - updateUI() - } - } - - private suspend fun updateUI() { - if (project.isDefault) - return - delay(200) - val zlsPath = this.zlsPath.text.ifBlank { null }?.toNioPathOrNull() - if (zlsPath == null) { - withEDTContext(ModalityState.any()) { - zlsVersion.text = "[zls path empty or invalid]" - } - return - } - val workingDir = project.guessProjectDir()?.toNioPathOrNull() - val result = createCommandLineSafe(workingDir, zlsPath, "version") - .map { it.withEnvironment(getDirenv().env) } - .mapFlat { it.call() } - .getOrElse { throwable -> - throwable.printStackTrace() - withEDTContext(ModalityState.any()) { - zlsVersion.text = "[failed to run \"zls version\"]\n${throwable.message}" - } - return - } - val version = result.stdout.trim() - withEDTContext(ModalityState.any()) { - zlsVersion.text = version - zlsVersion.foreground = JBColor.foreground() - } + zlsConfigPath.dispose() } } diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt new file mode 100644 index 00000000..87c5fb76 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt @@ -0,0 +1,87 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls + +import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.NlsContexts +import com.intellij.ui.dsl.builder.panel +import java.awt.Dimension +import java.util.* +import javax.swing.JComponent + +class ZLSConfigurable(val uuid: UUID, zls: ZLSVersion): NamedConfigurable() { + var zls: ZLSVersion = zls + set(value) { + zlsInstallations[uuid] = value + field = value + } + private var myView: ZLSPanel? = null + + override fun setDisplayName(name: String?) { + zls = zls.copy(name = name) + } + + override fun getEditableObject(): UUID? { + return uuid + } + + override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? { + return displayName + } + + override fun createOptionsPanel(): JComponent? { + var view = myView + if (view == null) { + view = ZLSPanel() + view.reset(zls) + myView = view + } + val p = panel { + view.attach(this@panel) + } + p.preferredSize = Dimension(640, 480) + return p + } + + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + return zls.name + } + + override fun isModified(): Boolean { + return myView?.isModified(zls) == true + } + + override fun apply() { + myView?.apply(zls)?.let { zls = it } + } + + override fun reset() { + myView?.reset(zls) + } + + override fun disposeUIResources() { + myView?.dispose() + myView = null + super.disposeUIResources() + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSInstallationsService.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSInstallationsService.kt new file mode 100644 index 00000000..52e1b3f8 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSInstallationsService.kt @@ -0,0 +1,54 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls + +import com.falsepattern.zigbrains.lsp.zls.ZLSInstallationsService.MyState +import com.falsepattern.zigbrains.shared.UUIDMapSerializable +import com.falsepattern.zigbrains.shared.UUIDStorage +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service + +@Service(Service.Level.APP) +@State( + name = "ZLSInstallations", + storages = [Storage("zigbrains.xml")] +) +class ZLSInstallationsService: UUIDMapSerializable.Converting(MyState()) { + override fun serialize(value: ZLSVersion) = value.toRef() + override fun deserialize(value: ZLSVersion.Ref) = value.resolve() + override fun getStorage(state: MyState) = state.zlsInstallations + override fun updateStorage(state: MyState, storage: ZLSStorage) = state.copy(zlsInstallations = storage) + + data class MyState(@JvmField val zlsInstallations: ZLSStorage = emptyMap()) + + companion object { + @JvmStatic + fun getInstance(): ZLSInstallationsService = service() + } +} + +inline val zlsInstallations: ZLSInstallationsService get() = ZLSInstallationsService.getInstance() + +private typealias ZLSStorage = UUIDStorage \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt new file mode 100644 index 00000000..ba52933f --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt @@ -0,0 +1,142 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls + +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsPanel +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableNamedElementPanelBase +import com.falsepattern.zigbrains.shared.cli.call +import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBTextArea +import com.intellij.ui.components.textFieldWithBrowseButton +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.swing.event.DocumentEvent +import kotlin.io.path.pathString + +class ZLSPanel() : ImmutableNamedElementPanelBase() { + private val pathToZLS = textFieldWithBrowseButton( + null, + ZLSBundle.message("dialog.title.zls"), + FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() + ).also { + it.textField.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + dispatchUpdateUI() + } + }) + Disposer.register(this, it) + } + private val zlsVersion = JBTextArea().also { it.isEditable = false } + private var settingsPanel: ZLSSettingsPanel? = null + private var debounce: Job? = null + + override fun attach(p: Panel): Unit = with(p) { + super.attach(p) + row(ZLSBundle.message("settings.panel.path.label")) { + cell(pathToZLS).resizableColumn().align(AlignX.FILL) + } + row(ZLSBundle.message("settings.panel.version.label")) { + cell(zlsVersion) + } + val sp = ZLSSettingsPanel() + p.collapsibleGroup(ZLSBundle.message("settings.panel.settings.group.label"), indent = false) { + sp.attach(this@collapsibleGroup) + } + settingsPanel = sp + } + + override fun isModified(version: ZLSVersion): Boolean { + val name = nameFieldValue ?: return false + val path = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() ?: return false + return name != version.name || version.path != path || settingsPanel?.isModified(version.settings) == true + } + + override fun apply(version: ZLSVersion): ZLSVersion? { + val path = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() ?: return null + return version.copy(path = path, name = nameFieldValue ?: "", settings = settingsPanel?.apply(version.settings) ?: version.settings) + } + + override fun reset(version: ZLSVersion?) { + nameFieldValue = version?.name ?: "" + this.pathToZLS.text = version?.path?.pathString ?: "" + settingsPanel?.reset(version?.settings) + dispatchUpdateUI() + } + + private fun dispatchUpdateUI() { + debounce?.cancel("New debounce") + debounce = zigCoroutineScope.launch { + updateUI() + } + } + + private suspend fun updateUI() { + delay(200) + val pathToZLS = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() + if (pathToZLS == null) { + withEDTContext(ModalityState.any()) { + zlsVersion.text = "[zls path empty or invalid]" + } + return + } + val versionCommand = createCommandLineSafe(null, pathToZLS, "--version").getOrElse { + it.printStackTrace() + withEDTContext(ModalityState.any()) { + zlsVersion.text = "[could not create \"zls --version\" command]\n${it.message}" + } + return + } + val result = versionCommand.call().getOrElse { + it.printStackTrace() + withEDTContext(ModalityState.any()) { + zlsVersion.text = "[failed to run \"zls --version\"]\n${it.message}" + } + return + } + val version = result.stdout.trim() + + withEDTContext(ModalityState.any()) { + zlsVersion.text = version + zlsVersion.foreground = JBColor.foreground() + } + } + + override fun dispose() { + debounce?.cancel("Disposed") + settingsPanel?.dispose() + settingsPanel = null + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigCoreProjectConfigurationProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt similarity index 54% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigCoreProjectConfigurationProvider.kt rename to lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt index 207da863..cdadebf7 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigCoreProjectConfigurationProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt @@ -20,24 +20,26 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.settings +package com.falsepattern.zigbrains.lsp.zls -import com.falsepattern.zigbrains.shared.SubConfigurable +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.withExtraData +import com.falsepattern.zigbrains.shared.asString +import com.falsepattern.zigbrains.shared.asUUID import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManager +import java.util.* -class ZigCoreProjectConfigurationProvider: ZigProjectConfigurationProvider { - override fun handleMainConfigChanged(project: Project) { - } +fun T.withZLS(uuid: UUID?): T { + return withExtraData("zls_uuid", uuid?.asString()) +} - override fun createConfigurable(project: Project): SubConfigurable { - return ZigProjectConfigurable(project) - } +val ZigToolchain.zlsUUID: UUID? get() { + return extraData["zls_uuid"]?.asUUID() +} - override fun createNewProjectSettingsPanel(holder: ZigProjectConfigurationProvider.SettingsPanelHolder): ZigProjectConfigurationProvider.SettingsPanel { - return ZigProjectSettingsPanel(holder, ProjectManager.getInstance().defaultProject) - } +val ZigToolchain.zls: ZLSVersion? get() { + return zlsUUID?.let { zlsInstallations[it] } +} - override val priority: Int - get() = 0 -} \ No newline at end of file +val Project.zls: ZLSVersion? get() = ZigToolchainService.getInstance(this).toolchain?.zls diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt new file mode 100644 index 00000000..8386baea --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt @@ -0,0 +1,91 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls + +import com.falsepattern.zigbrains.lsp.settings.ZLSSettings +import com.falsepattern.zigbrains.shared.NamedObject +import com.falsepattern.zigbrains.shared.cli.call +import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.util.text.SemVer +import com.intellij.util.xmlb.annotations.Attribute +import com.intellij.util.xmlb.annotations.Tag +import java.nio.file.Path +import kotlin.io.path.isExecutable +import kotlin.io.path.isRegularFile +import kotlin.io.path.pathString + +data class ZLSVersion(val path: Path, override val name: String? = null, val settings: ZLSSettings = ZLSSettings()): NamedObject { + override fun withName(newName: String?): ZLSVersion { + return copy(name = newName) + } + + fun toRef(): Ref { + return Ref(path.pathString, name, settings) + } + + fun isValid(): Boolean { + if (!path.toFile().exists()) + return false + if (!path.isRegularFile() || !path.isExecutable()) + return false + return true + } + + suspend fun version(): SemVer? { + if (!isValid()) + return null + val cli = createCommandLineSafe(null, path, "--version").getOrElse { return null } + val info = cli.call(5000).getOrElse { return null } + return SemVer.parseFromText(info.stdout.trim()) + } + + companion object { + suspend fun tryFromPath(path: Path): ZLSVersion? { + var zls = ZLSVersion(path) + if (!zls.isValid()) + return null + val version = zls.version()?.rawVersion + if (version != null) { + zls = zls.copy(name = "ZLS $version") + } + return zls + } + } + + data class Ref( + @JvmField + @Attribute + val path: String? = "", + @JvmField + @Attribute + val name: String? = "", + @JvmField + @Tag + val settings: ZLSSettings = ZLSSettings() + ) { + fun resolve(): ZLSVersion? { + return path?.ifBlank { null }?.toNioPathOrNull()?.let { ZLSVersion(it, name, settings) } + } + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSDownloader.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSDownloader.kt new file mode 100644 index 00000000..b03fed30 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSDownloader.kt @@ -0,0 +1,45 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.downloader + +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.lsp.zls.ui.getSuggestedZLSPath +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.IUserDataBridge +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable +import com.falsepattern.zigbrains.shared.downloader.Downloader +import java.awt.Component + +class ZLSDownloader(component: Component, private val data: IUserDataBridge?) : Downloader(component) { + override val windowTitle get() = ZLSBundle.message("settings.downloader.title") + override val versionInfoFetchTitle get() = ZLSBundle.message("settings.downloader.progress.fetch") + override fun downloadProgressTitle(version: ZLSVersionInfo) = ZLSBundle.message("settings.downloader.progress.install", version.version.rawVersion) + override fun localSelector() = ZLSLocalSelector(component) + override suspend fun downloadVersionList(): List { + val toolchain = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY)?.get() + val project = data?.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY) + return ZLSVersionInfo.downloadVersionInfoFor(toolchain, project) + } + override fun getSuggestedPath() = getSuggestedZLSPath() +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt new file mode 100644 index 00000000..a3126e21 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt @@ -0,0 +1,74 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.downloader + +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.lsp.zls.zlsInstallations +import com.falsepattern.zigbrains.shared.downloader.LocalSelector +import com.falsepattern.zigbrains.shared.withUniqueName +import com.intellij.icons.AllIcons +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.util.SystemInfo +import java.awt.Component +import java.nio.file.Path +import kotlin.io.path.isDirectory + +class ZLSLocalSelector(component: Component) : LocalSelector(component) { + override val windowTitle: String + get() = ZLSBundle.message("settings.local-selector.title") + override val descriptor: FileChooserDescriptor + get() = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor().withTitle(ZLSBundle.message("settings.local-selector.chooser.title")) + + override suspend fun browse(preSelected: Path?): ZLSVersion? { + if (preSelected?.isDirectory() == true) { + return super.browse(preSelected.resolve(if (SystemInfo.isWindows) "zls.exe" else "zls")) + } + return super.browse(preSelected) + } + + override suspend fun verify(path: Path): VerifyResult { + var zls = resolve(path, null) + var result: VerifyResult + result = if (zls == null) VerifyResult( + null, + false, + AllIcons.General.Error, + ZLSBundle.message("settings.local-selector.state.invalid"), + ) else VerifyResult( + null, + true, + AllIcons.General.Information, + ZLSBundle.message("settings.local-selector.state.ok") + ) + if (zls != null) { + zls = zlsInstallations.withUniqueName(zls) + } + return result.copy(name = zls?.name) + } + + override suspend fun resolve(path: Path, name: String?): ZLSVersion? { + return ZLSVersion.tryFromPath(path)?.let { zls -> name?.let { zls.copy(name = it) } ?: zls } + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSVersionInfo.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSVersionInfo.kt new file mode 100644 index 00000000..d6c7f0a0 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSVersionInfo.kt @@ -0,0 +1,99 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.downloader + +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.shared.downloader.VersionInfo +import com.falsepattern.zigbrains.shared.downloader.VersionInfo.Tarball +import com.falsepattern.zigbrains.shared.downloader.getTarballIfCompatible +import com.falsepattern.zigbrains.shared.downloader.tempPluginDir +import com.intellij.openapi.progress.coroutineToIndicator +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.io.FileUtil +import com.intellij.util.asSafely +import com.intellij.util.download.DownloadableFileService +import com.intellij.util.text.SemVer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.* +import java.net.URLEncoder + +@JvmRecord +data class ZLSVersionInfo( + override val version: SemVer, + override val date: String, + override val dist: Tarball +): VersionInfo { + companion object { + @OptIn(ExperimentalSerializationApi::class) + suspend fun downloadVersionInfoFor(toolchain: ZigToolchain?, project: Project?): List { + return withContext(Dispatchers.IO) { + val single = toolchain != null + val url = if (single) { + getToolchainURL(toolchain!!, project) ?: return@withContext emptyList() + } else { + multiURL + } + val service = DownloadableFileService.getInstance() + val tempFile = FileUtil.createTempFile(tempPluginDir, "zls_version_info", ".json", false, false) + val desc = service.createFileDescription(url, tempFile.name) + val downloader = service.createDownloader(listOf(desc), ZLSBundle.message("settings.downloader.service.index")) + val downloadResults = coroutineToIndicator { + downloader.download(tempPluginDir) + } + if (downloadResults.isEmpty()) + return@withContext emptyList() + val index = downloadResults[0].first + val info = index.inputStream().use { Json.decodeFromStream(it) } + index.delete() + return@withContext if (single) { + listOfNotNull(parseVersion(null, info)) + } else { + info.mapNotNull { (key, value) -> parseVersion(key, value) } + } + } + } + } +} + +private suspend fun getToolchainURL(toolchain: ZigToolchain, project: Project?): String? { + val zigVersion = toolchain.zig.getEnv(project).getOrNull()?.version ?: return null + return "https://releases.zigtools.org/v1/zls/select-version?zig_version=${URLEncoder.encode(zigVersion, Charsets.UTF_8)}&compatibility=only-runtime" +} +private const val multiURL: String = "https://builds.zigtools.org/index.json" +private fun parseVersion(versionKey: String?, data: JsonElement): ZLSVersionInfo? { + if (data !is JsonObject) { + return null + } + + val versionTag = data["version"]?.asSafely()?.content ?: versionKey + + val version = SemVer.parseFromText(versionTag) ?: return null + val date = data["date"]?.asSafely()?.content ?: "" + val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } + ?: return null + + return ZLSVersionInfo(version, date, dist) +} diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt new file mode 100644 index 00000000..01f76706 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt @@ -0,0 +1,221 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.ui + +import com.falsepattern.zigbrains.direnv.DirenvService +import com.falsepattern.zigbrains.direnv.Env +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.lsp.zls.ZLSConfigurable +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.lsp.zls.downloader.ZLSDownloader +import com.falsepattern.zigbrains.lsp.zls.downloader.ZLSLocalSelector +import com.falsepattern.zigbrains.lsp.zls.zlsInstallations +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable.Companion.TOOLCHAIN_KEY +import com.falsepattern.zigbrains.shared.UUIDMapSerializable +import com.falsepattern.zigbrains.shared.ui.* +import com.falsepattern.zigbrains.shared.ui.ListElem.One.Actual +import com.falsepattern.zigbrains.shared.withUniqueName +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.SystemInfo +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.util.system.OS +import com.intellij.util.text.SemVer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import java.awt.Component +import java.nio.file.Files +import java.nio.file.Path +import java.util.* +import kotlin.io.path.isDirectory +import kotlin.io.path.isExecutable +import kotlin.io.path.isRegularFile + +sealed interface ZLSDriver: UUIDComboBoxDriver { + override val theMap: UUIDMapSerializable.Converting + get() = zlsInstallations + + override fun createContext(model: ZBModel): ZBContext { + return ZLSContext(null, model) + } + + override fun createComboBox(model: ZBModel): ZBComboBox { + return ZLSComboBox(model) + } + + override fun createNamedConfigurable(uuid: UUID, elem: ZLSVersion): NamedConfigurable { + return ZLSConfigurable(uuid, elem) + } + + override suspend fun resolvePseudo( + context: Component, + elem: ListElem.Pseudo + ): UUID? { + return when(elem) { + is ListElem.One.Suggested -> zlsInstallations.withUniqueName(elem.instance) + is ListElem.FromDisk -> ZLSLocalSelector(context).browse() + is ListElem.Download -> ZLSDownloader(context, data).download() + }?.let { zlsInstallations.registerNew(it) } + } + + val data: ZigProjectConfigurationProvider.IUserDataBridge? + + object ForList: ZLSDriver { + override suspend fun constructModelList(): List> { + val res = ArrayList>() + res.addAll(ListElem.fetchGroup()) + res.add(Separator(ZLSBundle.message("settings.model.detected.separator"), true)) + res.add(suggestZLSVersions().asPending()) + return res + } + + override val data: ZigProjectConfigurationProvider.IUserDataBridge? + get() = null + } + + @JvmRecord + data class ForSelector(override val data: ZigProjectConfigurationProvider.IUserDataBridge?): ZLSDriver { + override suspend fun constructModelList(): List> { + val (project, toolchainVersion) = unpack(data) + if (toolchainVersion == null) { + return listOf(ListElem.None()) + } + val res = ArrayList>() + res.add(ListElem.None()) + res.addAll(compatibleInstallations(toolchainVersion)) + res.add(Separator("", true)) + res.addAll(ListElem.fetchGroup()) + res.add(Separator(ZLSBundle.message("settings.model.detected.separator"), true)) + res.add(suggestZLSVersions(project, data, toolchainVersion).asPending()) + return res + } + } +} + +private suspend fun unpack(data: ZigProjectConfigurationProvider.IUserDataBridge?): Pair { + val toolchain = data?.getUserData(TOOLCHAIN_KEY)?.get() + val project = data?.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY) + val toolchainVersion = toolchain + ?.zig + ?.getEnv(project) + ?.getOrNull() + ?.version + ?.let { SemVer.parseFromText(it) } + return project to toolchainVersion +} + +private fun suggestZLSVersions(project: Project? = null, data: ZigProjectConfigurationProvider.IUserDataBridge? = null, toolchainVersion: SemVer? = null): Flow = flow { + val env = if (project != null && DirenvService.getStateFor(data, project).isEnabled(project)) { + DirenvService.getInstance(project).import() + } else { + Env.empty + } + val existing = zlsInstallations.map { (_, zls) -> zls } + env.findAllExecutablesOnPATH("zls").collect { path -> + if (existing.any { it.path == path }) { + return@collect + } + emitIfCompatible(path, toolchainVersion) + } + val exe = if (SystemInfo.isWindows) "zls.exe" else "zls" + getWellKnownZLS().forEach { wellKnown -> + runCatching { + Files.newDirectoryStream(wellKnown).use { stream -> + stream.asSequence().filterNotNull().forEach { dir -> + val path = dir.resolve(exe) + if (!path.isRegularFile() || !path.isExecutable()) { + return@forEach + } + if (existing.any { it.path == path }) { + return@forEach + } + emitIfCompatible(path, toolchainVersion) + } + } + } + } +}.flowOn(Dispatchers.IO) + +private suspend fun FlowCollector.emitIfCompatible(path: Path, toolchainVersion: SemVer?) { + val ver = ZLSVersion.tryFromPath(path) ?: return + if (isCompatible(ver, toolchainVersion)) { + emit(ver) + } +} + +private suspend fun compatibleInstallations(toolchainVersion: SemVer): List> { + return zlsInstallations.mapNotNull { (uuid, version) -> + if (!isCompatible(version, toolchainVersion)) { + return@mapNotNull null + } + Actual(uuid, version) + } +} + +private suspend fun isCompatible(version: ZLSVersion, toolchainVersion: SemVer?): Boolean { + if (toolchainVersion == null) + return true + val zlsVersion = version.version() ?: return false + return numericVersionEquals(zlsVersion, toolchainVersion) +} + +private fun numericVersionEquals(a: SemVer, b: SemVer): Boolean { + return a.major == b.major && a.minor == b.minor && a.patch == b.patch +} + + +fun getSuggestedZLSPath(): Path? { + return getWellKnownZLS().getOrNull(0) +} + +/** + * Returns the paths to the following list of folders: + * + * 1. DATA/zls + * 2. HOME/.zig + * + * Where DATA is: + * - ~/Library on macOS + * - %LOCALAPPDATA% on Windows + * - $XDG_DATA_HOME (or ~/.local/share if not set) on other OSes + * + * and HOME is the user home path + */ +private fun getWellKnownZLS(): List { + val home = System.getProperty("user.home")?.toNioPathOrNull() ?: return emptyList() + val xdgDataHome = when(OS.CURRENT) { + OS.macOS -> home.resolve("Library") + OS.Windows -> System.getenv("LOCALAPPDATA")?.toNioPathOrNull() + else -> System.getenv("XDG_DATA_HOME")?.toNioPathOrNull() ?: home.resolve(Path.of(".local", "share")) + } + val res = ArrayList() + if (xdgDataHome != null && xdgDataHome.isDirectory()) { + res.add(xdgDataHome.resolve("zls")) + } + res.add(home.resolve(".zls")) + return res +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt new file mode 100644 index 00000000..e5355722 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt @@ -0,0 +1,93 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.ui + +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.lsp.zls.withZLS +import com.falsepattern.zigbrains.lsp.zls.zlsUUID +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.toolchain.base.PanelState +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainExtensionsProvider +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel +import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.util.Key +import com.intellij.ui.dsl.builder.Panel +import kotlinx.coroutines.launch + +class ZLSEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): + UUIDMapSelector(ZLSDriver.ForSelector(sharedState)), + ImmutableElementPanel, + ZigProjectConfigurationProvider.UserDataListener +{ + init { + sharedState?.addUserDataChangeListener(this) + } + + override fun onUserDataChanged(key: Key<*>) { + zigCoroutineScope.launch { listChanged() } + } + + override fun attach(panel: Panel): Unit = with(panel) { + row("Language Server") { + attachComboBoxRow(this) + } + } + + override fun isModified(toolchain: T): Boolean { + if (isEmpty) + return false + return toolchain.zlsUUID != selectedUUID + } + + override fun apply(toolchain: T): T { + return toolchain.withZLS(selectedUUID) + } + + override fun reset(toolchain: T?) { + selectedUUID = toolchain?.zlsUUID + zigCoroutineScope.launch { + listChanged() + selectedUUID = toolchain?.zlsUUID + } + } + + override fun dispose() { + super.dispose() + sharedState?.removeUserDataChangeListener(this) + } + + class Provider: ZigToolchainExtensionsProvider { + override fun createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?, state: PanelState): ImmutableElementPanel? { + if (state == PanelState.ModalEditor) { + return null + } + return ZLSEditor(sharedState) + } + + override val index: Int + get() = 100 + + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt similarity index 57% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt rename to lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt index ec3e3b94..8b67cff0 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt @@ -20,19 +20,19 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain +package com.falsepattern.zigbrains.lsp.zls.ui -import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool -import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.openapi.project.Project -import java.nio.file.Path +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor +import com.intellij.openapi.util.NlsContexts -abstract class AbstractZigToolchain { - val zig: ZigCompilerTool by lazy { ZigCompilerTool(this) } +class ZLSListEditor : UUIDMapEditor(ZLSDriver.ForList) { + override fun getEmptySelectionString(): String { + return ZLSBundle.message("settings.list.empty") + } - abstract fun workingDirectory(project: Project? = null): Path? - - abstract suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project? = null): GeneralCommandLine - - abstract fun pathToExecutable(toolName: String, project: Project? = null): Path + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + return ZLSBundle.message("settings.list.title") + } } \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt new file mode 100644 index 00000000..bdfd879e --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt @@ -0,0 +1,83 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.ui + +import com.falsepattern.zigbrains.lsp.LSPIcons +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.shared.ui.* +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.icons.EMPTY_ICON +import javax.swing.JList +import kotlin.io.path.pathString + + +class ZLSComboBox(model: ZBModel): ZBComboBox(model, ::ZLSCellRenderer) + +class ZLSContext(project: Project?, model: ZBModel): ZBContext(project, model, ::ZLSCellRenderer) + +class ZLSCellRenderer(getModel: () -> ZBModel): ZBCellRenderer(getModel) { + override fun customizeCellRenderer( + list: JList?>, + value: ListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + icon = EMPTY_ICON + when (value) { + is ListElem.One -> { + val (icon, isSuggestion) = when(value) { + is ListElem.One.Suggested -> AllIcons.General.Information to true + is ListElem.One.Actual -> LSPIcons.ZLS to false + } + this.icon = icon + val item = value.instance + val name = item.name + val path = item.path.pathString + renderPathNameComponent(path, name, "ZLS", this, isSuggestion, index == -1) + } + + is ListElem.Download -> { + icon = AllIcons.Actions.Download + append(ZLSBundle.message("settings.model.download.text")) + } + + is ListElem.FromDisk -> { + icon = AllIcons.General.OpenDisk + append(ZLSBundle.message("settings.model.from-disk.text")) + } + is ListElem.Pending -> { + icon = AllIcons.Empty + append(ZLSBundle.message("settings.model.loading.text"), SimpleTextAttributes.GRAYED_ATTRIBUTES) + } + is ListElem.None, null -> { + icon = AllIcons.General.BalloonError + append(ZLSBundle.message("settings.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES) + } + } + } + +} \ No newline at end of file diff --git a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml index d07aaa59..888ca03b 100644 --- a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml +++ b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml @@ -49,6 +49,13 @@ + @@ -56,10 +63,10 @@ implementation="com.falsepattern.zigbrains.lsp.settings.ZLSSettingsConfigProvider" /> - diff --git a/lsp/src/main/resources/icons/zls.svg b/lsp/src/main/resources/icons/zls.svg new file mode 100644 index 00000000..5fbf01fb --- /dev/null +++ b/lsp/src/main/resources/icons/zls.svg @@ -0,0 +1,19 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties index 3b76263f..8a680305 100644 --- a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties +++ b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties @@ -1,8 +1,3 @@ -settings.group.title=ZLS Settings -settings.zls-path.label=Executable path -settings.zls-path.tooltip=Path to the ZLS Binary -settings.zls-path.browse.title=Path to the ZLS Binary -settings.zls-version.label=Detected ZLS version settings.zls-config-path.label=Config path settings.zls-config-path.tooltip=Leave empty to use built-in config generated from the settings below settings.zls-config-path.browse.title=Path to the Custom ZLS Config File (Optional) @@ -50,8 +45,6 @@ settings.build_runner_path.tooltip=Specify a custom build runner to resolve buil settings.global_cache_path.label=Global cache path settings.global_cache_path.tooltip=Path to a directory that will be used as zig's cache. Will default to `${KnownFolders.Cache}/zls`. notification.group.zigbrains-lsp=ZigBrains LSP Integration -notification.message.could-not-detect.content=Could not detect ZLS binary, please configure it -notification.message.zls-exe-path-invalid.content=ZLS executable path could not be parsed: {0} notification.message.zls-exe-not-exists.content=ZLS executable does not exist: {0} notification.message.zls-exe-not-executable.content=ZLS executable is not an executable file: {0} notification.message.zls-config-not-exists.content=ZLS config file does not exist: {0} @@ -61,8 +54,26 @@ notification.message.zls-config-autogen-failed.content=Failed to autogenerate ZL notification.banner.zls-not-running=Zig Language Server is not running. Check the [Language Servers] tool menu! notification.banner.zls-bad-config=Zig Language Server is misconfigured. Check [Settings | Languages \\& Frameworks | Zig]! progress.title.create-connection-provider=Creating ZLS connection provider -progress.title.validate=Validating ZLS # suppress inspection "UnusedProperty" lsp.zls.name=Zig Language Server # suppress inspection "UnusedProperty" lsp.zls.description=The Zig Language Server, via ZigBrains +settings.list.title=ZLS Instances +settings.list.empty=Select a ZLS version to view or edit its details here +settings.model.detected.separator=Detected ZLS versions +settings.model.none.text= +settings.model.loading.text=Loading\u2026 +settings.model.from-disk.text=Add ZLS from disk\u2026 +settings.model.download.text=Download ZLS\u2026 +settings.downloader.title=Install ZLS +settings.downloader.progress.fetch=Fetching ZLS version information +settings.downloader.progress.install=Installing ZLS {} +settings.downloader.service.index=ZLS version information +settings.local-selector.title=Select ZLS from disk +settings.local-selector.chooser.title=ZLS Binary +settings.local-selector.state.invalid=Invalid ZLS path +settings.local-selector.state.ok=ZLS path OK +dialog.title.zls=Path to the ZLS Executable +settings.panel.path.label=Path: +settings.panel.version.label=Version: +settings.panel.settings.group.label=Settings: \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0c52557a..7eb587d0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" } rootProject.name = "ZigBrains" diff --git a/src/art/zls/zls.svg b/src/art/zls/zls.svg new file mode 100644 index 00000000..893dff1f --- /dev/null +++ b/src/art/zls/zls.svg @@ -0,0 +1,19 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index b25173d7..1656a8e0 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -4,7 +4,7 @@ FalsePattern com.intellij.modules.platform - com.redhat.devtools.lsp4ij + com.redhat.devtools.lsp4ij com.intellij.modules.cidr.debugger com.intellij.cidr.base com.intellij.clion @@ -17,13 +17,18 @@ dynamic="true" name="zlsConfigProvider" /> +