From 281ce0ed4e90de1e7ca4ce12ffc6759460c4f83b Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Fri, 11 Apr 2025 02:30:17 +0200 Subject: [PATCH] almost feature complete - ZLS needs settings GUI --- .../zigbrains/direnv/ui/DirenvEditor.kt | 5 +- .../ZigProjectConfigurationProvider.kt | 8 +- .../project/toolchain/ZigToolchainService.kt | 3 +- .../project/toolchain/base/ZigToolchain.kt | 17 +- .../base/ZigToolchainConfigurable.kt | 19 +- .../base/ZigToolchainExtensionsProvider.kt | 10 +- .../toolchain/base/ZigToolchainProvider.kt | 11 +- .../toolchain/downloader/LocalSelector.kt | 163 ----------------- .../downloader/LocalToolchainDownloader.kt | 50 ++++++ .../downloader/LocalToolchainSelector.kt | 107 ++++++++++++ .../toolchain/downloader/ZigVersionInfo.kt | 120 ++----------- .../toolchain/local/LocalZigToolchain.kt | 18 +- .../local/LocalZigToolchainConfigurable.kt | 6 +- .../local/LocalZigToolchainProvider.kt | 6 +- .../ui/ZigToolchainComboBoxHandler.kt | 8 +- .../toolchain/ui/ZigToolchainDriver.kt | 27 ++- .../toolchain/ui/ZigToolchainEditor.kt | 12 +- .../com/falsepattern/zigbrains/shared/UUID.kt | 28 +++ .../zigbrains/shared/UUIDMapSerializable.kt | 14 +- .../downloader/DirectoryState.kt | 25 ++- .../downloader/Downloader.kt | 83 ++++----- .../shared/downloader/LocalSelector.kt | 138 +++++++++++++++ .../shared/downloader/VersionInfo.kt | 164 ++++++++++++++++++ .../zigbrains/shared/ui/UUIDMapSelector.kt | 75 ++++---- .../zigbrains/shared/ui/elements.kt | 8 +- .../resources/zigbrains/Bundle.properties | 29 ++-- .../falsepattern/zigbrains/lsp/ZLSStartup.kt | 7 + .../lsp/ZLSStreamConnectionProvider.kt | 6 +- .../zigbrains/lsp/ZigLanguageServerFactory.kt | 8 +- .../ZigEditorNotificationProvider.kt | 4 +- .../lsp/settings/ZLSSettingsConfigProvider.kt | 4 +- .../zigbrains/lsp/zls/ZLSService.kt | 48 ++--- .../zigbrains/lsp/zls/ZLSVersion.kt | 32 +++- .../lsp/zls/downloader/ZLSDownloader.kt | 92 ++++++++++ .../lsp/zls/downloader/ZLSLocalSelector.kt | 64 +++++++ .../lsp/zls/downloader/ZLSVersionInfo.kt | 85 +++++++++ .../zigbrains/lsp/zls/ui/ZLSDriver.kt | 80 +++++++-- .../zigbrains/lsp/zls/ui/ZLSEditor.kt | 48 +++-- .../zigbrains/lsp/zls/ui/ZLSListEditor.kt | 2 +- .../main/resources/META-INF/zigbrains-lsp.xml | 2 +- 40 files changed, 1131 insertions(+), 505 deletions(-) delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.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/shared/UUID.kt rename core/src/main/kotlin/com/falsepattern/zigbrains/{project/toolchain => shared}/downloader/DirectoryState.kt (61%) rename core/src/main/kotlin/com/falsepattern/zigbrains/{project/toolchain => shared}/downloader/Downloader.kt (62%) 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 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 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 index 3aa15bb8..15f9ff71 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt @@ -26,6 +26,7 @@ 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 @@ -83,8 +84,8 @@ abstract class DirenvEditor(private val sharedState: ZigProjectConfigurationP } class Provider: ZigProjectConfigurationProvider { - override fun create(project: Project?, sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { - if (project?.isDefault != false) { + override fun create(sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { + if (sharedState.getUserData(PROJECT_KEY)?.isDefault != false) { return null } DirenvService.setStateFor(sharedState, DirenvState.Auto) 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 5a2b6698..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 @@ -30,13 +30,15 @@ import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.UserDataHolderBase interface ZigProjectConfigurationProvider { - fun create(project: Project?, sharedState: IUserDataBridge): SubConfigurable? + fun create(sharedState: IUserDataBridge): SubConfigurable? val index: Int companion object { private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.projectConfigProvider") + val PROJECT_KEY: Key = Key.create("Project") fun createPanels(project: Project?): List> { val sharedState = UserDataBridge() - return EXTENSION_POINT_NAME.extensionList.sortedBy { it.index }.mapNotNull { it.create(project, sharedState) } + sharedState.putUserData(PROJECT_KEY, project) + return EXTENSION_POINT_NAME.extensionList.sortedBy { it.index }.mapNotNull { it.create(sharedState) } } } @@ -51,7 +53,7 @@ interface ZigProjectConfigurationProvider { class UserDataBridge: UserDataHolderBase(), IUserDataBridge { private val listeners = ArrayList() - override fun putUserData(key: Key, value: T?) { + override fun putUserData(key: Key, value: T?) { super.putUserData(key, value) synchronized(listeners) { listeners.forEach { listener -> 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 index 6cf457fe..9395c997 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt @@ -24,6 +24,7 @@ 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.SerializablePersistentStateComponent @@ -44,7 +45,7 @@ import java.util.UUID ) class ZigToolchainService(private val project: Project): SerializablePersistentStateComponent(State()) { var toolchainUUID: UUID? - get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { + get() = state.toolchain.ifBlank { null }?.asUUID()?.takeIf { if (it in zigToolchainList) { true } else { 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 index 2769d274..c0f2cb3b 100644 --- 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 @@ -37,12 +37,12 @@ import java.nio.file.Path interface ZigToolchain: NamedObject { val zig: ZigCompilerTool get() = ZigCompilerTool(this) - fun getUserData(key: Key): T? + val extraData: Map /** * Returned type must be the same class */ - fun withUserData(key: Key, value: T?): ZigToolchain + fun withExtraData(map: Map): ZigToolchain fun workingDirectory(project: Project? = null): Path? @@ -55,7 +55,18 @@ interface ZigToolchain: NamedObject { @Attribute val marker: String? = null, @JvmField - @MapAnnotation(surroundWithTag = false) 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 index c39841f4..46071cf5 100644 --- 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 @@ -22,23 +22,34 @@ 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.project.Project 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.UUID +import java.util.function.Supplier import javax.swing.JComponent abstract class ZigToolchainConfigurable( val uuid: UUID, - tc: T + tc: T, + val data: ZigProjectConfigurationProvider.IUserDataBridge? ): NamedConfigurable() { var toolchain: T = tc set(value) { zigToolchainList[uuid] = value field = value } + + init { + data?.putUserData(TOOLCHAIN_KEY, Supplier{ + myViews.fold(toolchain) { tc, view -> view.apply(tc) ?: tc } + }) + } private var myViews: List> = emptyList() abstract fun createPanel(): ImmutableElementPanel @@ -48,7 +59,7 @@ abstract class ZigToolchainConfigurable( if (views.isEmpty()) { views = ArrayList>() views.add(createPanel()) - views.addAll(createZigToolchainExtensionPanels()) + views.addAll(createZigToolchainExtensionPanels(data)) views.forEach { it.reset(toolchain) } myViews = views } @@ -86,4 +97,8 @@ abstract class ZigToolchainConfigurable( 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 index e0be25d6..8ef0c29f 100644 --- 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 @@ -22,18 +22,20 @@ 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 +import com.intellij.openapi.util.UserDataHolder private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainExtensionsProvider") -internal interface ZigToolchainExtensionsProvider { - fun createExtensionPanel(): ImmutableElementPanel? +interface ZigToolchainExtensionsProvider { + fun createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): ImmutableElementPanel? val index: Int } -fun createZigToolchainExtensionPanels(): List> { +fun createZigToolchainExtensionPanels(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): List> { return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull { - it.createExtensionPanel() + it.createExtensionPanel(sharedState) } } \ 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 index 58667862..c9504edc 100644 --- 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 @@ -22,6 +22,7 @@ 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 @@ -46,7 +47,7 @@ internal interface ZigToolchainProvider { fun deserialize(data: Map): ZigToolchain? fun serialize(toolchain: ZigToolchain): Map fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean - fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*> + fun createConfigurable(uuid: UUID, toolchain: ZigToolchain, data: ZigProjectConfigurationProvider.IUserDataBridge?): ZigToolchainConfigurable<*> suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) } @@ -55,17 +56,17 @@ 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) + 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)) + return ZigToolchain.Ref(provider.serialMarker, provider.serialize(this), this.extraData) } -fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<*> { +fun ZigToolchain.createNamedConfigurable(uuid: UUID, data: ZigProjectConfigurationProvider.IUserDataBridge?): ZigToolchainConfigurable<*> { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() - return provider.createConfigurable(uuid, this) + return provider.createConfigurable(uuid, this, data) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt deleted file mode 100644 index 93553933..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt +++ /dev/null @@ -1,163 +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.downloader - -import com.falsepattern.zigbrains.Icons -import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain -import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain -import com.falsepattern.zigbrains.project.toolchain.zigToolchainList -import com.falsepattern.zigbrains.shared.coroutine.asContextElement -import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT -import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext -import com.falsepattern.zigbrains.shared.withUniqueName -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.FileChooserDescriptorFactory -import com.intellij.openapi.ui.DialogBuilder -import com.intellij.openapi.util.Disposer -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.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.util.concurrent.atomic.AtomicBoolean -import javax.swing.event.DocumentEvent -import kotlin.io.path.pathString - -object LocalSelector { - suspend fun browseFromDisk(component: Component, preSelected: LocalZigToolchain? = null): ZigToolchain? { - return withEDTContext(component.asContextElement()) { - doBrowseFromDisk(component, preSelected) - } - } - - @RequiresEdt - private suspend fun doBrowseFromDisk(component: Component, preSelected: LocalZigToolchain?): ZigToolchain? { - val dialog = DialogBuilder() - val name = JBTextField().also { it.columns = 25 } - val path = textFieldWithBrowseButton( - null, - FileChooserDescriptorFactory.createSingleFolderDescriptor() - .withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title")) - ) - Disposer.register(dialog, path) - lateinit var errorMessageBox: JBLabel - fun verify(tc: LocalZigToolchain?) { - var tc = tc - if (tc == null) { - errorMessageBox.icon = AllIcons.General.Error - errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid") - dialog.setOkActionEnabled(false) - } else { - val existingToolchain = zigToolchainList - .mapNotNull { it.second as? LocalZigToolchain } - .firstOrNull { it.location == tc.location } - if (existingToolchain != null) { - errorMessageBox.icon = AllIcons.General.Warning - errorMessageBox.text = existingToolchain.name?.let { ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-named", it) } - ?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-unnamed") - dialog.setOkActionEnabled(true) - } else { - errorMessageBox.icon = AllIcons.General.Information - errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.ok") - dialog.setOkActionEnabled(true) - } - } - if (tc != null) { - tc = zigToolchainList.withUniqueName(tc) - } - val prevNameDefault = name.emptyText.text.trim() == name.text.trim() || name.text.isBlank() - name.emptyText.text = tc?.name ?: "" - if (prevNameDefault) { - name.text = name.emptyText.text - } - } - suspend fun verify(path: String) { - val tc = runCatching { withModalProgress(ModalTaskOwner.component(component), "Resolving toolchain", TaskCancellation.cancellable()) { - LocalZigToolchain.tryFromPathString(path) - } }.getOrNull() - verify(tc) - } - val active = AtomicBoolean(false) - path.addDocumentListener(object: DocumentAdapter() { - override fun textChanged(e: DocumentEvent) { - if (!active.get()) - return - zigCoroutineScope.launchWithEDT(ModalityState.current()) { - verify(path.text) - } - } - }) - val center = panel { - row(ZigBrainsBundle.message("settings.toolchain.local-selector.name.label")) { - cell(name).resizableColumn().align(AlignX.FILL) - } - row(ZigBrainsBundle.message("settings.toolchain.local-selector.path.label")) { - cell(path).resizableColumn().align(AlignX.FILL) - } - row { - errorMessageBox = JBLabel() - cell(errorMessageBox) - } - } - dialog.centerPanel(center) - dialog.setTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.title")) - dialog.addCancelAction() - dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.toolchain.local-selector.ok-action")) } - if (preSelected == null) { - val chosenFile = FileChooser.chooseFile( - FileChooserDescriptorFactory.createSingleFolderDescriptor() - .withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title")), - null, - null - ) - if (chosenFile != null) { - verify(chosenFile.path) - path.text = chosenFile.path - } - } else { - verify(preSelected) - path.text = preSelected.location.pathString - } - active.set(true) - if (!dialog.showAndGet()) { - active.set(false) - return null - } - active.set(false) - return runCatching { withModalProgress(ModalTaskOwner.component(component), "Resolving toolchain", TaskCancellation.cancellable()) { - LocalZigToolchain.tryFromPathString(path.text)?.let { it.withName(name.text.ifBlank { null } ?: it.name) } - } }.getOrNull() - } -} \ 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..c2ab44c9 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainDownloader.kt @@ -0,0 +1,50 @@ +/* + * 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 com.falsepattern.zigbrains.shared.downloader.LocalSelector +import com.intellij.openapi.util.NlsContexts +import java.awt.Component +import java.nio.file.Path + +class LocalToolchainDownloader(component: Component) : Downloader(component) { + override val windowTitle: String get() = ZigBrainsBundle.message("settings.toolchain.downloader.title") + override val versionInfoFetchTitle: @NlsContexts.ProgressTitle String get() = ZigBrainsBundle.message("settings.toolchain.downloader.progress.fetch") + override fun downloadProgressTitle(version: ZigVersionInfo): @NlsContexts.ProgressTitle String { + return ZigBrainsBundle.message("settings.toolchain.downloader.progress.install", version.version.rawVersion) + } + override fun localSelector(): LocalSelector { + return LocalToolchainSelector(component) + } + override suspend fun downloadVersionList(): List { + return ZigVersionInfo.downloadVersionList() + } + + override fun getSuggestedPath(): Path? { + return 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..a27106cc --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainSelector.kt @@ -0,0 +1,107 @@ +/* + * 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.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +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.downloader.LocalSelector +import com.falsepattern.zigbrains.shared.withUniqueName +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.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.ui.DialogBuilder +import com.intellij.openapi.util.Disposer +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.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.event.DocumentEvent +import kotlin.io.path.pathString + +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 index 8b9d54b6..40cbdfbc 100644 --- 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 @@ -24,6 +24,13 @@ package com.falsepattern.zigbrains.project.toolchain.downloader import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.shared.Unarchiver +import com.falsepattern.zigbrains.shared.downloader.VersionInfo +import com.falsepattern.zigbrains.shared.downloader.VersionInfo.Tarball +import com.falsepattern.zigbrains.shared.downloader.downloadTarball +import com.falsepattern.zigbrains.shared.downloader.flattenDownloadDir +import com.falsepattern.zigbrains.shared.downloader.getTarballIfCompatible +import com.falsepattern.zigbrains.shared.downloader.tempPluginDir +import com.falsepattern.zigbrains.shared.downloader.unpackTarball import com.intellij.openapi.application.PathManager import com.intellij.openapi.progress.EmptyProgressIndicator import com.intellij.openapi.progress.ProgressManager @@ -59,24 +66,13 @@ import kotlin.io.path.name @JvmRecord data class ZigVersionInfo( - val version: SemVer, - val date: String, + override val version: SemVer, + override val date: String, val docs: String, val notes: String, val src: Tarball?, - 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) - } - } - + override val dist: Tarball +): VersionInfo { companion object { @OptIn(ExperimentalSerializationApi::class) suspend fun downloadVersionList(): List { @@ -97,71 +93,6 @@ data class ZigVersionInfo( } } } - - @JvmRecord - @Serializable - data class Tarball(val tarball: String, val shasum: String, val size: Int) -} - -private suspend fun downloadTarball(dist: ZigVersionInfo.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() - } -} - -private 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) -private 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 - } - } } private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? { @@ -175,36 +106,9 @@ private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? 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 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) } - -private fun getTarballIfCompatible(dist: String, tb: JsonElement): ZigVersionInfo.Tarball? { - if (!dist.contains('-')) - return null - val (arch, os) = dist.split('-', limit = 2) - val theArch = when (arch) { - "x86_64" -> CpuArch.X86_64 - "i386" -> 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) -} - -private val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index 70015f5f..290d46b0 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -36,15 +36,7 @@ import com.intellij.util.keyFMap.KeyFMap import java.nio.file.Path @JvmRecord -data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null, private val userData: KeyFMap = KeyFMap.EMPTY_MAP): ZigToolchain { - override fun getUserData(key: Key): T? { - return userData.get(key) - } - - override fun withUserData(key: Key, value: T?): LocalZigToolchain { - return copy(userData = if (value == null) userData.minus(key) else userData.plus(key, value)) - } - +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() } @@ -61,6 +53,10 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override return location.resolve(exeName) } + override fun withExtraData(map: Map): ZigToolchain { + return this.copy(extraData = map) + } + override fun withName(newName: String?): LocalZigToolchain { return this.copy(name = newName) } @@ -76,10 +72,6 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override } } - suspend fun tryFromPathString(pathStr: String?): LocalZigToolchain? { - return pathStr?.ifBlank { null }?.toNioPathOrNull()?.let { tryFromPath(it) } - } - suspend fun tryFromPath(path: Path): LocalZigToolchain? { var tc = LocalZigToolchain(path) if (!tc.zig.fileValid()) { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt index bc8e5adc..b64b3e26 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt @@ -22,13 +22,15 @@ package com.falsepattern.zigbrains.project.toolchain.local +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import java.util.UUID class LocalZigToolchainConfigurable( uuid: UUID, - toolchain: LocalZigToolchain -): ZigToolchainConfigurable(uuid, toolchain) { + toolchain: LocalZigToolchain, + data: ZigProjectConfigurationProvider.IUserDataBridge? +): ZigToolchainConfigurable(uuid, toolchain, data) { override fun createPanel() = LocalZigToolchainPanel() override fun setDisplayName(name: String?) { 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 index 14a79acc..1429b1cc 100644 --- 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 @@ -24,6 +24,7 @@ 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 @@ -84,10 +85,11 @@ class LocalZigToolchainProvider: ZigToolchainProvider { override fun createConfigurable( uuid: UUID, - toolchain: ZigToolchain + toolchain: ZigToolchain, + data: ZigProjectConfigurationProvider.IUserDataBridge? ): ZigToolchainConfigurable<*> { toolchain as LocalZigToolchain - return LocalZigToolchainConfigurable(uuid, toolchain) + return LocalZigToolchainConfigurable(uuid, toolchain, data) } @OptIn(ExperimentalCoroutinesApi::class) 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 index ffc5a025..74ef2f85 100644 --- 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 @@ -23,8 +23,8 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain -import com.falsepattern.zigbrains.project.toolchain.downloader.Downloader -import com.falsepattern.zigbrains.project.toolchain.downloader.LocalSelector +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 @@ -36,7 +36,7 @@ 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 -> Downloader.downloadToolchain(context) - is ListElem.FromDisk -> LocalSelector.browseFromDisk(context) + 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 index f8188478..275a800c 100644 --- 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 @@ -23,6 +23,8 @@ 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 @@ -60,13 +62,6 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver { return ZigToolchainComboBoxHandler.onItemSelected(context, elem) } - override fun createNamedConfigurable( - uuid: UUID, - elem: ZigToolchain - ): NamedConfigurable { - return elem.createNamedConfigurable(uuid) - } - object ForList: ZigToolchainDriver { override fun constructModelList(): List> { val modelList = ArrayList>() @@ -75,9 +70,16 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver { modelList.add(suggestZigToolchains().asPending()) return modelList } + + override fun createNamedConfigurable( + uuid: UUID, + elem: ZigToolchain + ): NamedConfigurable { + return elem.createNamedConfigurable(uuid, ZigProjectConfigurationProvider.UserDataBridge()) + } } - class ForSelector(val project: Project?, val data: UserDataHolder): ZigToolchainDriver { + class ForSelector(val data: ZigProjectConfigurationProvider.IUserDataBridge): ZigToolchainDriver { override fun constructModelList(): List> { val modelList = ArrayList>() modelList.add(ListElem.None()) @@ -85,8 +87,15 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver { modelList.add(Separator("", true)) modelList.addAll(ListElem.fetchGroup()) modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) - modelList.add(suggestZigToolchains(project, data).asPending()) + modelList.add(suggestZigToolchains(data.getUserData(PROJECT_KEY), data).asPending()) return modelList } + + override fun createNamedConfigurable( + uuid: UUID, + elem: ZigToolchain + ): NamedConfigurable { + return elem.createNamedConfigurable(uuid, data) + } } } \ 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 index 12ac6894..51e405d4 100644 --- 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 @@ -24,6 +24,7 @@ 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.ZigToolchain import com.falsepattern.zigbrains.shared.SubConfigurable @@ -35,9 +36,8 @@ import com.intellij.openapi.util.Key import com.intellij.ui.dsl.builder.Panel import kotlinx.coroutines.launch -class ZigToolchainEditor(private var project: Project?, - private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): - UUIDMapSelector(ZigToolchainDriver.ForSelector(project, sharedState)), +class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): + UUIDMapSelector(ZigToolchainDriver.ForSelector(sharedState)), SubConfigurable, ZigProjectConfigurationProvider.UserDataListener { @@ -52,7 +52,7 @@ class ZigToolchainEditor(private var project: Project?, override fun attach(p: Panel): Unit = with(p) { row(ZigBrainsBundle.message( - if (project?.isDefault == true) + if (sharedState.getUserData(PROJECT_KEY)?.isDefault == true) "settings.toolchain.editor.toolchain-default.label" else "settings.toolchain.editor.toolchain.label") @@ -81,8 +81,8 @@ class ZigToolchainEditor(private var project: Project?, override val newProjectBeforeInitSelector get() = true class Provider: ZigProjectConfigurationProvider { - override fun create(project: Project?, sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { - return ZigToolchainEditor(project, sharedState).also { it.reset(project) } + override fun create(sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { + return ZigToolchainEditor(sharedState).also { it.reset(sharedState.getUserData(PROJECT_KEY)) } } override val index: Int get() = 0 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..deb3a33c --- /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.UUID + +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 index afc40ea4..f6118bc5 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt @@ -58,10 +58,10 @@ abstract class UUIDMapSerializable(init: S): SerializablePersistentSt updateState { val newMap = HashMap() newMap.putAll(getStorage(it)) - var uuidStr = uuid.toString() + var uuidStr = uuid.asString() while (newMap.containsKey(uuidStr)) { uuid = UUID.randomUUID() - uuidStr = uuid.toString() + uuidStr = uuid.asString() } newMap[uuidStr] = value updateStorage(it, newMap) @@ -71,7 +71,7 @@ abstract class UUIDMapSerializable(init: S): SerializablePersistentSt } protected fun setStateUUID(uuid: UUID, value: T) { - val str = uuid.toString() + val str = uuid.asString() updateState { val newMap = HashMap() newMap.putAll(getStorage(it)) @@ -82,15 +82,15 @@ abstract class UUIDMapSerializable(init: S): SerializablePersistentSt } protected fun getStateUUID(uuid: UUID): T? { - return getStorage(state)[uuid.toString()] + return getStorage(state)[uuid.asString()] } protected fun hasStateUUID(uuid: UUID): Boolean { - return getStorage(state).containsKey(uuid.toString()) + return getStorage(state).containsKey(uuid.asString()) } protected fun removeStateUUID(uuid: UUID) { - val str = uuid.toString() + val str = uuid.asString() updateState { updateStorage(state, getStorage(state).filter { it.key != str }) } @@ -143,7 +143,7 @@ abstract class UUIDMapSerializable(init: S): SerializablePersistentSt return getStorage(state) .asSequence() .mapNotNull { - val uuid = UUID.fromString(it.key) ?: return@mapNotNull null + val uuid = it.key.asUUID() ?: return@mapNotNull null val tc = deserialize(it.value) ?: return@mapNotNull null uuid to tc }.iterator() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/DirectoryState.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/DirectoryState.kt similarity index 61% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/DirectoryState.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/DirectoryState.kt index 5bf1a3d1..ac6110e7 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/DirectoryState.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/DirectoryState.kt @@ -1,8 +1,29 @@ -package com.falsepattern.zigbrains.project.toolchain.downloader +/* + * 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.contracts.ExperimentalContracts import kotlin.io.path.exists import kotlin.io.path.isDirectory diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/Downloader.kt similarity index 62% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/Downloader.kt index 5a7522e9..2b5a4a34 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/Downloader.kt @@ -20,20 +20,17 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain.downloader +package com.falsepattern.zigbrains.shared.downloader import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain -import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain -import com.falsepattern.zigbrains.project.toolchain.local.getSuggestedLocalToolchainPath import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.intellij.icons.AllIcons -import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory 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 @@ -45,45 +42,53 @@ 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.asSafely import com.intellij.util.concurrency.annotations.RequiresEdt import java.awt.Component import java.nio.file.Path +import java.util.Vector import javax.swing.DefaultComboBoxModel import javax.swing.JList import javax.swing.event.DocumentEvent import kotlin.io.path.pathString -object Downloader { - suspend fun downloadToolchain(component: Component): ZigToolchain? { +abstract class Downloader(val component: Component) { + suspend fun download(): T? { val info = withModalProgress( ModalTaskOwner.component(component), - ZigBrainsBundle.message("settings.toolchain.downloader.progress.fetch"), + versionInfoFetchTitle, TaskCancellation.cancellable() ) { - ZigVersionInfo.downloadVersionList() + downloadVersionList() } + val selector = localSelector() val (downloadPath, version) = runInterruptibleEDT(component.asContextElement()) { - selectToolchain(info) + selectVersion(info, selector) } ?: return null withModalProgress( ModalTaskOwner.component(component), - ZigBrainsBundle.message("settings.toolchain.downloader.progress.install", version.version.rawVersion), + downloadProgressTitle(version), TaskCancellation.cancellable() ) { version.downloadAndUnpack(downloadPath) } - return LocalZigToolchain.tryFromPath(downloadPath)?.let { LocalSelector.browseFromDisk(component, it) } + 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 selectToolchain(info: List): Pair? { + private fun selectVersion(info: List, selector: LocalSelector): Pair? { val dialog = DialogBuilder() - val theList = ComboBox(DefaultComboBoxModel(info.toTypedArray())) - theList.renderer = object: ColoredListCellRenderer() { + val theList = ComboBox(DefaultComboBoxModel(Vector(info))) + theList.renderer = object: ColoredListCellRenderer() { override fun customizeCellRenderer( - list: JList, - value: ZigVersionInfo?, + list: JList, + value: V?, index: Int, selected: Boolean, hasFocus: Boolean @@ -91,18 +96,14 @@ object Downloader { value?.let { append(it.version.rawVersion) } } } - val outputPath = textFieldWithBrowseButton( - null, - FileChooserDescriptorFactory.createSingleFolderDescriptor() - .withTitle(ZigBrainsBundle.message("settings.toolchain.downloader.chooser.title")) - ) + val outputPath = textFieldWithBrowseButton(null, 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.determine(path) + val state = DirectoryState.Companion.determine(path) if (state.isValid()) { errorMessageBox.icon = AllIcons.General.Information dialog.setOkActionEnabled(true) @@ -111,12 +112,12 @@ object Downloader { dialog.setOkActionEnabled(false) } errorMessageBox.text = ZigBrainsBundle.message(when(state) { - DirectoryState.Invalid -> "settings.toolchain.downloader.state.invalid" - DirectoryState.NotAbsolute -> "settings.toolchain.downloader.state.not-absolute" - DirectoryState.NotDirectory -> "settings.toolchain.downloader.state.not-directory" - DirectoryState.NotEmpty -> "settings.toolchain.downloader.state.not-empty" - DirectoryState.CreateNew -> "settings.toolchain.downloader.state.create-new" - DirectoryState.Ok -> "settings.toolchain.downloader.state.ok" + 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() } @@ -129,20 +130,21 @@ object Downloader { } }) var archiveSizeCell: Cell<*>? = null - fun detect(item: ZigVersionInfo) { - outputPath.text = getSuggestedLocalToolchainPath()?.resolve(item.version.rawVersion)?.pathString ?: "" + 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.toolchain.downloader.archive-size.text", "%.2fMB".format(sizeMb)) + archiveSizeCell?.comment?.text = ZigBrainsBundle.message("settings.shared.downloader.archive-size.text", "%.2fMB".format(sizeMb)) } theList.addItemListener { - detect(it.item as ZigVersionInfo) + @Suppress("UNCHECKED_CAST") + detect(it.item as V) } val center = panel { - row(ZigBrainsBundle.message("settings.toolchain.downloader.version.label")) { + row(ZigBrainsBundle.message("settings.shared.downloader.version.label")) { cell(theList).resizableColumn().align(AlignX.FILL) } - row(ZigBrainsBundle.message("settings.toolchain.downloader.location.label")) { + row(ZigBrainsBundle.message("settings.shared.downloader.location.label")) { cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } } row { @@ -152,19 +154,18 @@ object Downloader { } detect(info[0]) dialog.centerPanel(center) - dialog.setTitle(ZigBrainsBundle.message("settings.toolchain.downloader.title")) + dialog.setTitle(windowTitle) dialog.addCancelAction() - dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.toolchain.downloader.ok-action")) } + 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.determine(path).isValid()) { + if (!DirectoryState.Companion.determine(path).isValid()) { return null } - val version = theList.selectedItem?.asSafely() - ?: return null + val version = theList.item ?: return null return path to version } 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..314754ac --- /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 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) + 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..26c7d3dd --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/VersionInfo.kt @@ -0,0 +1,164 @@ +/* + * 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.project.toolchain.downloader.ZigVersionInfo +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/ui/UUIDMapSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt index b77b94b1..17a1acd8 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt @@ -28,9 +28,11 @@ 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.ui.ListElem import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.Disposable import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.observable.util.whenListChanged import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.ui.DialogWrapper @@ -57,6 +59,9 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable comboBox.addItemListener(::itemStateChanged) driver.theMap.addChangeListener(changeListener) model.whenListChanged { + zigCoroutineScope.launchWithEDT(comboBox.asContextElement()) { + tryReloadSelection() + } if (comboBox.isPopupVisible) { comboBox.isPopupVisible = false comboBox.isPopupVisible = true @@ -67,11 +72,12 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable protected var selectedUUID: UUID? get() = comboBox.selectedUUID set(value) { - comboBox.selectedUUID = value - refreshButtonState(value) + runInEdt { + applyUUIDNowOrOnReload(value) + } } - private fun refreshButtonState(item: Any?) { + private fun refreshButtonState(item: ListElem<*>) { editButton?.isEnabled = item is ListElem.One.Actual<*> editButton?.repaint() } @@ -81,6 +87,8 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable return } val item = event.item + if (item !is ListElem<*>) + return refreshButtonState(item) if (item !is ListElem.Pseudo<*>) return @@ -95,35 +103,45 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable } } + @RequiresEdt + private fun tryReloadSelection() { + val list = model.toList() + 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 { + model.selectedItem = element + return + } + } + val selected = model.selected + if (selected != null && list.contains(selected)) { + model.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 + } } + model.selectedItem = element + return + } + model.selectedItem = ListElem.None() + } + protected suspend fun listChanged() { withContext(Dispatchers.EDT + comboBox.asContextElement()) { val list = driver.constructModelList() model.updateContents(list) - val onReload = selectOnNextReload - selectOnNextReload = null - if (onReload != null) { - val element = list.firstOrNull { when(it) { - is ListElem.One.Actual<*> -> it.uuid == onReload - else -> false - } } - model.selectedItem = element - return@withContext - } - val selected = model.selected - if (selected != null && list.contains(selected)) { - model.selectedItem = selected - return@withContext - } - 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 - } } - model.selectedItem = element - return@withContext - } - model.selectedItem = ListElem.None() + tryReloadSelection() } } @@ -141,7 +159,6 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable } }.component.let { editButton = it - refreshButtonState(comboBox.selectedItem) } } 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 index cd59bd9b..f1916ed5 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt @@ -67,7 +67,7 @@ sealed interface ListElem : ListElemIn { data class Pending(val elems: Flow>): ListElem companion object { - private val fetchGroup = listOf>(Download(), FromDisk()) + private val fetchGroup: List> = listOf(Download(), FromDisk()) fun fetchGroup() = fetchGroup as List> } } @@ -79,4 +79,8 @@ fun Pair.asActual() = ListElem.One.Actual(first, second) fun T.asSuggested() = ListElem.One.Suggested(this) -fun Flow.asPending() = ListElem.Pending(map { it.asSuggested() }) \ No newline at end of file +@JvmName("listElemFlowAsPending") +fun Flow>.asPending() = ListElem.Pending(this) + +fun Flow.asPending() = map { it.asSuggested() }.asPending() + diff --git a/core/src/main/resources/zigbrains/Bundle.properties b/core/src/main/resources/zigbrains/Bundle.properties index 3de98cc2..1b05b9f3 100644 --- a/core/src/main/resources/zigbrains/Bundle.properties +++ b/core/src/main/resources/zigbrains/Bundle.properties @@ -112,6 +112,20 @@ 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 @@ -127,27 +141,14 @@ 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.version.label=Version: -settings.toolchain.downloader.location.label=Location: -settings.toolchain.downloader.ok-action=Download 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.state.invalid=Invalid path -settings.toolchain.downloader.state.not-absolute=Must be an absolute path -settings.toolchain.downloader.state.not-directory=Path is not a directory -settings.toolchain.downloader.state.not-empty=Directory is not empty -settings.toolchain.downloader.state.create-new=Directory will be created -settings.toolchain.downloader.state.ok=Directory OK -settings.toolchain.downloader.archive-size.text=Archive size: {0} 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.name.label=Name: -settings.toolchain.local-selector.path.label=Path: -settings.toolchain.local-selector.ok-action=Add -settings.toolchain.local-selector.chooser.title=Existing Zig Install Directory +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}" 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 7f4ccc86..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,6 +22,7 @@ package com.falsepattern.zigbrains.lsp +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 @@ -33,7 +34,13 @@ class ZLSStartup: ProjectActivity { override suspend fun execute(project: Project) { project.zigCoroutineScope.launch { var currentState = project.zlsRunning() + var currentZLS = project.zls while (!project.isDisposed) { + 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 5f499b0e..a55c3f39 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt @@ -23,7 +23,8 @@ package com.falsepattern.zigbrains.lsp import com.falsepattern.zigbrains.lsp.config.ZLSConfigProviderBase -import com.falsepattern.zigbrains.lsp.zls.ZLSService +import com.falsepattern.zigbrains.lsp.zls.zls +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.notification.Notification import com.intellij.notification.NotificationType @@ -52,8 +53,7 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje @OptIn(ExperimentalSerializationApi::class) suspend fun getCommand(project: Project): List? { - val svc = ZLSService.getInstance(project) - val zls = svc.zls ?: return null + val zls = project.zls ?: return null val zlsPath: Path = zls.path if (!zlsPath.toFile().exists()) { Notification( 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 783f611f..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.zls.ZLSService +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,7 +68,7 @@ class ZigLanguageServerFactory: LanguageServerFactory, LanguageServerEnablementS } features.inlayHintFeature = object: LSPInlayHintFeature() { override fun isEnabled(file: PsiFile): Boolean { - return ZLSService.getInstance(project).zls?.settings?.inlayHints == true + return project.zls?.settings?.inlayHints == true } } return features @@ -82,7 +82,7 @@ class ZigLanguageServerFactory: LanguageServerFactory, LanguageServerEnablementS } fun Project.zlsEnabled(): Boolean { - return (getUserData(ENABLED_KEY) != false) && ZLSService.getInstance(this).zls?.isValid() == true + return (getUserData(ENABLED_KEY) != false) && zls?.isValid() == true } fun Project.zlsEnabled(value: Boolean) { @@ -125,7 +125,7 @@ private suspend fun doStart(project: Project, restart: Boolean) { project.lsm.stop("ZigBrains") delay(250) } - if (ZLSService.getInstance(project).zls?.isValid() == true) { + 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 026e7c22..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,7 +23,7 @@ package com.falsepattern.zigbrains.lsp.notification import com.falsepattern.zigbrains.lsp.ZLSBundle -import com.falsepattern.zigbrains.lsp.zls.ZLSService +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 @@ -52,7 +52,7 @@ class ZigEditorNotificationProvider: EditorNotificationProvider, DumbAware { if (project.zlsRunning()) { return@async null } else { - return@async ZLSService.getInstance(project).zls?.isValid() == true + return@async project.zls?.isValid() == true } } return Function { editor -> 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 43454bf7..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,13 +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.ZLSService +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 = ZLSService.getInstance(project).zls?.settings ?: return previous + 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/zls/ZLSService.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt index 500e92f4..85a588a0 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt @@ -22,42 +22,24 @@ package com.falsepattern.zigbrains.lsp.zls -import com.falsepattern.zigbrains.project.toolchain.zigToolchainList -import com.intellij.openapi.components.* +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.util.xmlb.annotations.Attribute import java.util.UUID -@Service(Service.Level.PROJECT) -@State( - name = "ZLS", - storages = [Storage("zigbrains.xml")] -) -class ZLSService: SerializablePersistentStateComponent(MyState()) { - var zlsUUID: UUID? - get() = state.zls.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { - if (it in zigToolchainList) { - true - } else { - updateState { - it.copy(zls = "") - } - false - } - } - set(value) { - updateState { - it.copy(zls = value?.toString() ?: "") - } - } +fun T.withZLS(uuid: UUID?): T { + return withExtraData("zls_uuid", uuid?.asString()) +} - val zls: ZLSVersion? - get() = zlsUUID?.let { zlsInstallations[it] } +val ZigToolchain.zlsUUID: UUID? get() { + return extraData["zls_uuid"]?.asUUID() +} - data class MyState(@JvmField @Attribute var zls: String = "") +val ZigToolchain.zls: ZLSVersion? get() { + return zlsUUID?.let { zlsInstallations[it] } +} - companion object { - @JvmStatic - fun getInstance(project: Project): ZLSService = project.service() - } -} \ 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 index 2778c77a..c1de43e8 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt @@ -24,14 +24,19 @@ 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.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.util.text.SemVer import java.nio.file.Path import com.intellij.util.xmlb.annotations.Attribute +import kotlin.io.path.isDirectory 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?, val settings: ZLSSettings): NamedObject { +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) } @@ -48,6 +53,31 @@ data class ZLSVersion(val path: Path, override val name: String?, val settings: 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? { + if (path.isDirectory()) { + val exeName = if (SystemInfo.isWindows) "zls.exe" else "zls" + return tryFromPath(path.resolve(exeName)) + } + 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 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..97b74ca4 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSDownloader.kt @@ -0,0 +1,92 @@ +/* + * 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.zls.ZLSVersion +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable +import com.falsepattern.zigbrains.shared.downloader.Downloader +import com.falsepattern.zigbrains.shared.downloader.LocalSelector +import com.intellij.openapi.util.NlsContexts +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.util.system.OS +import java.awt.Component +import java.nio.file.Path +import kotlin.io.path.isDirectory + +class ZLSDownloader(component: Component, private val data: ZigProjectConfigurationProvider.IUserDataBridge?) : Downloader(component) { + override val windowTitle: String + get() = "Install ZLS" + override val versionInfoFetchTitle: @NlsContexts.ProgressTitle String + get() = "Fetching zls version information" + + override fun downloadProgressTitle(version: ZLSVersionInfo): @NlsContexts.ProgressTitle String { + return "Installing ZLS ${version.version.rawVersion}" + } + + override fun localSelector(): LocalSelector { + return ZLSLocalSelector(component) + } + + override suspend fun downloadVersionList(): List { + val toolchain = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY)?.get() ?: return emptyList() + val project = data.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY) + return ZLSVersionInfo.downloadVersionInfoFor(toolchain, project) + } + + override fun getSuggestedPath(): Path? { + return getSuggestedZLSPath() + } +} + +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/downloader/ZLSLocalSelector.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt new file mode 100644 index 00000000..5586bc79 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt @@ -0,0 +1,64 @@ +/* + * 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.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 java.awt.Component +import java.nio.file.Path + +class ZLSLocalSelector(component: Component) : LocalSelector(component) { + override val windowTitle: String + get() = "Select ZLS from disk" + override val descriptor: FileChooserDescriptor + get() = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor().withTitle("ZLS binary") + + 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, + "Invalid ZLS path", + ) else VerifyResult( + null, + true, + AllIcons.General.Information, + "ZLS path 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..1cc03960 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSVersionInfo.kt @@ -0,0 +1,85 @@ +/* + * 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.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo +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.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.decodeFromStream +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 zigVersion = toolchain.zig.getEnv(project).getOrNull()?.version ?: return@withContext emptyList() + val service = DownloadableFileService.getInstance() + val tempFile = FileUtil.createTempFile(tempPluginDir, "zls_version_info", ".json", false, false) + val desc = service.createFileDescription("https://releases.zigtools.org/v1/zls/select-version?zig_version=${URLEncoder.encode(zigVersion, Charsets.UTF_8)}&compatibility=only-runtime", tempFile.name) + val downloader = service.createDownloader(listOf(desc), "ZLS version information") + 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 listOfNotNull(parseVersion(info)) + } + } + } +} +private fun parseVersion(data: JsonObject): ZLSVersionInfo? { + val versionTag = data["version"]?.asSafely()?.content + + 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 index ce6d5ec8..e64d3c7a 100644 --- 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 @@ -24,26 +24,35 @@ package com.falsepattern.zigbrains.lsp.zls.ui 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 import com.falsepattern.zigbrains.shared.UUIDMapSerializable import com.falsepattern.zigbrains.shared.ui.ListElem +import com.falsepattern.zigbrains.shared.ui.ListElem.One.Actual import com.falsepattern.zigbrains.shared.ui.ListElemIn +import com.falsepattern.zigbrains.shared.ui.Separator import com.falsepattern.zigbrains.shared.ui.UUIDComboBoxDriver import com.falsepattern.zigbrains.shared.ui.ZBComboBox import com.falsepattern.zigbrains.shared.ui.ZBContext import com.falsepattern.zigbrains.shared.ui.ZBModel +import com.falsepattern.zigbrains.shared.ui.asPending +import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.util.text.SemVer +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch import java.awt.Component import java.util.UUID -object ZLSDriver: UUIDComboBoxDriver { +sealed interface ZLSDriver: UUIDComboBoxDriver { override val theMap: UUIDMapSerializable.Converting get() = zlsInstallations - override fun constructModelList(): List> { - return ListElem.fetchGroup() - } - override fun createContext(model: ZBModel): ZBContext { return ZLSContext(null, model) } @@ -52,16 +61,67 @@ object ZLSDriver: UUIDComboBoxDriver { 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? { - //TODO - return null + return when(elem) { + is ListElem.FromDisk<*> -> ZLSLocalSelector(context).browse() + else -> null + }?.let { zlsInstallations.registerNew(it) } } - override fun createNamedConfigurable(uuid: UUID, elem: ZLSVersion): NamedConfigurable { - //TODO - return ZLSConfigurable(uuid, elem) + object ForList: ZLSDriver { + override fun constructModelList(): List> { + return listOf(ListElem.None(), Separator("", true), ListElem.FromDisk()) + } } + + @JvmRecord + data class ForSelector(private val data: ZigProjectConfigurationProvider.IUserDataBridge?): ZLSDriver { + override fun constructModelList(): List> { + val res = ArrayList>() + res.add(ListElem.None()) + res.add(compatibleInstallations().asPending()) + res.add(Separator("", true)) + res.addAll(ListElem.fetchGroup()) + return res + } + + override suspend fun resolvePseudo( + context: Component, + elem: ListElem.Pseudo + ): UUID? { + return when(elem) { + is ListElem.FromDisk<*> -> ZLSLocalSelector(context).browse() + is ListElem.Download<*> -> ZLSDownloader(context, data).download() + else -> null + }?.let { zlsInstallations.registerNew(it) } + } + private fun compatibleInstallations(): Flow> = flow { + val project = data?.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY) + val toolchainVersion = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY) + ?.get() + ?.zig + ?.getEnv(project) + ?.getOrNull() + ?.version + ?.let { SemVer.parseFromText(it) } + ?: return@flow + zlsInstallations.forEach { (uuid, version) -> + val zlsVersion = version.version() ?: return@forEach + if (numericVersionEquals(toolchainVersion, zlsVersion)) { + emit(Actual(uuid, version)) + } + } + } + } +} + +private fun numericVersionEquals(a: SemVer, b: SemVer): Boolean { + return a.major == b.major && a.minor == b.minor && a.patch == b.patch } \ 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 index f93d1b00..f80d888d 100644 --- 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 @@ -22,27 +22,28 @@ package com.falsepattern.zigbrains.lsp.zls.ui -import com.falsepattern.zigbrains.lsp.zls.ZLSService +import com.falsepattern.zigbrains.lsp.ZLSStarter +import com.falsepattern.zigbrains.lsp.startLSP 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.shared.SubConfigurable -import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor +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.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 -class ZLSEditor(private var project: Project?, - private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): - UUIDMapSelector(ZLSDriver), - SubConfigurable, +class ZLSEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): + UUIDMapSelector(ZLSDriver.ForSelector(sharedState)), + ImmutableElementPanel, ZigProjectConfigurationProvider.UserDataListener { init { - sharedState.addUserDataChangeListener(this) + sharedState?.addUserDataChangeListener(this) } override fun onUserDataChanged(key: Key<*>) { @@ -55,35 +56,30 @@ class ZLSEditor(private var project: Project?, } } - override fun isModified(context: Project): Boolean { - return ZLSService.getInstance(context).zlsUUID != selectedUUID + override fun isModified(toolchain: T): Boolean { + return toolchain.zlsUUID != selectedUUID } - override fun apply(context: Project) { - ZLSService.getInstance(context).zlsUUID = selectedUUID + override fun apply(toolchain: T): T { + return toolchain.withZLS(selectedUUID) } - override fun reset(context: Project?) { - val project = context ?: ProjectManager.getInstance().defaultProject - selectedUUID = ZLSService.getInstance(project).zlsUUID + override fun reset(toolchain: T) { + selectedUUID = toolchain.zlsUUID } override fun dispose() { super.dispose() - sharedState.removeUserDataChangeListener(this) + sharedState?.removeUserDataChangeListener(this) } - override val newProjectBeforeInitSelector: Boolean get() = true - class Provider: ZigProjectConfigurationProvider { - override fun create( - project: Project?, - sharedState: ZigProjectConfigurationProvider.IUserDataBridge - ): SubConfigurable? { - return ZLSEditor(project, sharedState) + class Provider: ZigToolchainExtensionsProvider { + override fun createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): ImmutableElementPanel? { + return ZLSEditor(sharedState) } override val index: Int - get() = 50 + get() = 100 } } \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt index 3ee4e221..9469478c 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt @@ -28,7 +28,7 @@ import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor import com.intellij.openapi.ui.MasterDetailsComponent import com.intellij.openapi.util.NlsContexts -class ZLSListEditor : UUIDMapEditor(ZLSDriver) { +class ZLSListEditor : UUIDMapEditor(ZLSDriver.ForList) { override fun getEmptySelectionString(): String { return ZLSBundle.message("settings.list.empty") } diff --git a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml index 0182a606..888ca03b 100644 --- a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml +++ b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml @@ -65,7 +65,7 @@ -