almost feature complete - ZLS needs settings GUI
This commit is contained in:
parent
dcede7eb43
commit
281ce0ed4e
40 changed files with 1131 additions and 505 deletions
|
@ -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<T>(private val sharedState: ZigProjectConfigurationP
|
|||
}
|
||||
|
||||
class Provider: ZigProjectConfigurationProvider {
|
||||
override fun create(project: Project?, sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>? {
|
||||
if (project?.isDefault != false) {
|
||||
override fun create(sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>? {
|
||||
if (sharedState.getUserData(PROJECT_KEY)?.isDefault != false) {
|
||||
return null
|
||||
}
|
||||
DirenvService.setStateFor(sharedState, DirenvState.Auto)
|
||||
|
|
|
@ -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<Project>?
|
||||
fun create(sharedState: IUserDataBridge): SubConfigurable<Project>?
|
||||
val index: Int
|
||||
companion object {
|
||||
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigProjectConfigurationProvider>("com.falsepattern.zigbrains.projectConfigProvider")
|
||||
val PROJECT_KEY: Key<Project> = Key.create("Project")
|
||||
fun createPanels(project: Project?): List<SubConfigurable<Project>> {
|
||||
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<UserDataListener>()
|
||||
override fun <T : Any?> putUserData(key: Key<T?>, value: T?) {
|
||||
override fun <T : Any?> putUserData(key: Key<T>, value: T?) {
|
||||
super.putUserData(key, value)
|
||||
synchronized(listeners) {
|
||||
listeners.forEach { listener ->
|
||||
|
|
|
@ -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<ZigToolchainService.State>(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 {
|
||||
|
|
|
@ -37,12 +37,12 @@ import java.nio.file.Path
|
|||
interface ZigToolchain: NamedObject<ZigToolchain> {
|
||||
val zig: ZigCompilerTool get() = ZigCompilerTool(this)
|
||||
|
||||
fun <T> getUserData(key: Key<T>): T?
|
||||
val extraData: Map<String, String>
|
||||
|
||||
/**
|
||||
* Returned type must be the same class
|
||||
*/
|
||||
fun <T> withUserData(key: Key<T>, value: T?): ZigToolchain
|
||||
fun withExtraData(map: Map<String, String>): ZigToolchain
|
||||
|
||||
fun workingDirectory(project: Project? = null): Path?
|
||||
|
||||
|
@ -55,7 +55,18 @@ interface ZigToolchain: NamedObject<ZigToolchain> {
|
|||
@Attribute
|
||||
val marker: String? = null,
|
||||
@JvmField
|
||||
@MapAnnotation(surroundWithTag = false)
|
||||
val data: Map<String, String>? = null,
|
||||
@JvmField
|
||||
val extraData: Map<String, String>? = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun <T: ZigToolchain> T.withExtraData(key: String, value: String?): T {
|
||||
val newMap = HashMap<String, String>()
|
||||
newMap.putAll(extraData.filter { (theKey, _) -> theKey != key})
|
||||
if (value != null) {
|
||||
newMap[key] = value
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return withExtraData(newMap) as T
|
||||
}
|
|
@ -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<T: ZigToolchain>(
|
||||
val uuid: UUID,
|
||||
tc: T
|
||||
tc: T,
|
||||
val data: ZigProjectConfigurationProvider.IUserDataBridge?
|
||||
): NamedConfigurable<UUID>() {
|
||||
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<ImmutableElementPanel<T>> = emptyList()
|
||||
|
||||
abstract fun createPanel(): ImmutableElementPanel<T>
|
||||
|
@ -48,7 +59,7 @@ abstract class ZigToolchainConfigurable<T: ZigToolchain>(
|
|||
if (views.isEmpty()) {
|
||||
views = ArrayList<ImmutableElementPanel<T>>()
|
||||
views.add(createPanel())
|
||||
views.addAll(createZigToolchainExtensionPanels())
|
||||
views.addAll(createZigToolchainExtensionPanels(data))
|
||||
views.forEach { it.reset(toolchain) }
|
||||
myViews = views
|
||||
}
|
||||
|
@ -86,4 +97,8 @@ abstract class ZigToolchainConfigurable<T: ZigToolchain>(
|
|||
myViews = emptyList()
|
||||
super.disposeUIResources()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TOOLCHAIN_KEY: Key<Supplier<ZigToolchain>> = Key.create("TOOLCHAIN")
|
||||
}
|
||||
}
|
|
@ -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<ZigToolchainExtensionsProvider>("com.falsepattern.zigbrains.toolchainExtensionsProvider")
|
||||
|
||||
internal interface ZigToolchainExtensionsProvider {
|
||||
fun <T : ZigToolchain> createExtensionPanel(): ImmutableElementPanel<T>?
|
||||
interface ZigToolchainExtensionsProvider {
|
||||
fun <T : ZigToolchain> createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): ImmutableElementPanel<T>?
|
||||
val index: Int
|
||||
}
|
||||
|
||||
fun <T: ZigToolchain> createZigToolchainExtensionPanels(): List<ImmutableElementPanel<T>> {
|
||||
fun <T: ZigToolchain> createZigToolchainExtensionPanels(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): List<ImmutableElementPanel<T>> {
|
||||
return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull {
|
||||
it.createExtensionPanel()
|
||||
it.createExtensionPanel(sharedState)
|
||||
}
|
||||
}
|
|
@ -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<String, String>): ZigToolchain?
|
||||
fun serialize(toolchain: ZigToolchain): Map<String, String>
|
||||
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<ZigToolchain>
|
||||
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)
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<LocalZigToolchain, ZigVersionInfo>(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<LocalZigToolchain> {
|
||||
return LocalToolchainSelector(component)
|
||||
}
|
||||
override suspend fun downloadVersionList(): List<ZigVersionInfo> {
|
||||
return ZigVersionInfo.downloadVersionList()
|
||||
}
|
||||
|
||||
override fun getSuggestedPath(): Path? {
|
||||
return getSuggestedLocalToolchainPath()
|
||||
}
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<LocalZigToolchain>(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()
|
||||
}
|
||||
}
|
|
@ -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<ZigVersionInfo> {
|
||||
|
@ -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<JsonPrimitive>()?.content ?: ""
|
||||
val docs = data["docs"]?.asSafely<JsonPrimitive>()?.content ?: ""
|
||||
val notes = data["notes"]?.asSafely<JsonPrimitive>()?.content ?: ""
|
||||
val src = data["src"]?.asSafely<JsonObject>()?.let { Json.decodeFromJsonElement<ZigVersionInfo.Tarball>(it) }
|
||||
val src = data["src"]?.asSafely<JsonObject>()?.let { Json.decodeFromJsonElement<Tarball>(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<ZigVersionInfo.Tarball>(tb)
|
||||
}
|
||||
|
||||
private val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile()
|
|
@ -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 <T> getUserData(key: Key<T>): T? {
|
||||
return userData.get(key)
|
||||
}
|
||||
|
||||
override fun <T> withUserData(key: Key<T>, 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<String, String> = 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<String, String>): 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()) {
|
||||
|
|
|
@ -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<LocalZigToolchain>(uuid, toolchain) {
|
||||
toolchain: LocalZigToolchain,
|
||||
data: ZigProjectConfigurationProvider.IUserDataBridge?
|
||||
): ZigToolchainConfigurable<LocalZigToolchain>(uuid, toolchain, data) {
|
||||
override fun createPanel() = LocalZigToolchainPanel()
|
||||
|
||||
override fun setDisplayName(name: String?) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<ZigToolchain>): 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) }
|
||||
}
|
|
@ -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<ZigToolchain> {
|
|||
return ZigToolchainComboBoxHandler.onItemSelected(context, elem)
|
||||
}
|
||||
|
||||
override fun createNamedConfigurable(
|
||||
uuid: UUID,
|
||||
elem: ZigToolchain
|
||||
): NamedConfigurable<UUID> {
|
||||
return elem.createNamedConfigurable(uuid)
|
||||
}
|
||||
|
||||
object ForList: ZigToolchainDriver {
|
||||
override fun constructModelList(): List<ListElemIn<ZigToolchain>> {
|
||||
val modelList = ArrayList<ListElemIn<ZigToolchain>>()
|
||||
|
@ -75,9 +70,16 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver<ZigToolchain> {
|
|||
modelList.add(suggestZigToolchains().asPending())
|
||||
return modelList
|
||||
}
|
||||
|
||||
override fun createNamedConfigurable(
|
||||
uuid: UUID,
|
||||
elem: ZigToolchain
|
||||
): NamedConfigurable<UUID> {
|
||||
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<ListElemIn<ZigToolchain>> {
|
||||
val modelList = ArrayList<ListElemIn<ZigToolchain>>()
|
||||
modelList.add(ListElem.None())
|
||||
|
@ -85,8 +87,15 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver<ZigToolchain> {
|
|||
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<UUID> {
|
||||
return elem.createNamedConfigurable(uuid, data)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ZigToolchain>(ZigToolchainDriver.ForSelector(project, sharedState)),
|
||||
class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge):
|
||||
UUIDMapSelector<ZigToolchain>(ZigToolchainDriver.ForSelector(sharedState)),
|
||||
SubConfigurable<Project>,
|
||||
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<Project>? {
|
||||
return ZigToolchainEditor(project, sharedState).also { it.reset(project) }
|
||||
override fun create(sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>? {
|
||||
return ZigToolchainEditor(sharedState).also { it.reset(sharedState.getUserData(PROJECT_KEY)) }
|
||||
}
|
||||
|
||||
override val index: Int get() = 0
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.falsepattern.zigbrains.shared
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
fun String.asUUID(): UUID? = UUID.fromString(this)
|
||||
fun UUID.asString(): String = toString()
|
|
@ -58,10 +58,10 @@ abstract class UUIDMapSerializable<T, S: Any>(init: S): SerializablePersistentSt
|
|||
updateState {
|
||||
val newMap = HashMap<String, T>()
|
||||
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<T, S: Any>(init: S): SerializablePersistentSt
|
|||
}
|
||||
|
||||
protected fun setStateUUID(uuid: UUID, value: T) {
|
||||
val str = uuid.toString()
|
||||
val str = uuid.asString()
|
||||
updateState {
|
||||
val newMap = HashMap<String, T>()
|
||||
newMap.putAll(getStorage(it))
|
||||
|
@ -82,15 +82,15 @@ abstract class UUIDMapSerializable<T, S: Any>(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<T, S: Any>(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()
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
|
|
@ -20,20 +20,17 @@
|
|||
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<T, V: VersionInfo>(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<T>
|
||||
protected abstract suspend fun downloadVersionList(): List<V>
|
||||
protected abstract fun getSuggestedPath(): Path?
|
||||
|
||||
@RequiresEdt
|
||||
private fun selectToolchain(info: List<ZigVersionInfo>): Pair<Path, ZigVersionInfo>? {
|
||||
private fun selectVersion(info: List<V>, selector: LocalSelector<T>): Pair<Path, V>? {
|
||||
val dialog = DialogBuilder()
|
||||
val theList = ComboBox(DefaultComboBoxModel(info.toTypedArray()))
|
||||
theList.renderer = object: ColoredListCellRenderer<ZigVersionInfo>() {
|
||||
val theList = ComboBox(DefaultComboBoxModel(Vector(info)))
|
||||
theList.renderer = object: ColoredListCellRenderer<V>() {
|
||||
override fun customizeCellRenderer(
|
||||
list: JList<out ZigVersionInfo>,
|
||||
value: ZigVersionInfo?,
|
||||
list: JList<out V>,
|
||||
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<ZigVersionInfo>()
|
||||
?: return null
|
||||
val version = theList.item ?: return null
|
||||
|
||||
return path to version
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<T>(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,
|
||||
)
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Tarball>(tb)
|
||||
}
|
||||
|
||||
val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile()
|
|
@ -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<T>(val driver: UUIDComboBoxDriver<T>): 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<T>(val driver: UUIDComboBoxDriver<T>): 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<T>(val driver: UUIDComboBoxDriver<T>): 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<T>(val driver: UUIDComboBoxDriver<T>): 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<Any>()
|
||||
}
|
||||
|
||||
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<Any>()
|
||||
tryReloadSelection()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,7 +159,6 @@ abstract class UUIDMapSelector<T>(val driver: UUIDComboBoxDriver<T>): Disposable
|
|||
}
|
||||
}.component.let {
|
||||
editButton = it
|
||||
refreshButtonState(comboBox.selectedItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ sealed interface ListElem<T> : ListElemIn<T> {
|
|||
data class Pending<T>(val elems: Flow<ListElem<T>>): ListElem<T>
|
||||
|
||||
companion object {
|
||||
private val fetchGroup = listOf<ListElem<Any>>(Download(), FromDisk())
|
||||
private val fetchGroup: List<ListElem<Any>> = listOf(Download(), FromDisk())
|
||||
fun <T> fetchGroup() = fetchGroup as List<ListElem<T>>
|
||||
}
|
||||
}
|
||||
|
@ -79,4 +79,8 @@ fun <T> Pair<UUID, T>.asActual() = ListElem.One.Actual(first, second)
|
|||
|
||||
fun <T> T.asSuggested() = ListElem.One.Suggested(this)
|
||||
|
||||
fun <T> Flow<T>.asPending() = ListElem.Pending(map { it.asSuggested() })
|
||||
@JvmName("listElemFlowAsPending")
|
||||
fun <T> Flow<ListElem<T>>.asPending() = ListElem.Pending(this)
|
||||
|
||||
fun <T> Flow<T>.asPending() = map { it.asSuggested() }.asPending()
|
||||
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<String>? {
|
||||
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(
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<ZLSService.MyState>(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() ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
val zls: ZLSVersion?
|
||||
get() = zlsUUID?.let { zlsInstallations[it] }
|
||||
|
||||
data class MyState(@JvmField @Attribute var zls: String = "")
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(project: Project): ZLSService = project.service<ZLSService>()
|
||||
}
|
||||
fun <T: ZigToolchain> T.withZLS(uuid: UUID?): T {
|
||||
return withExtraData("zls_uuid", uuid?.asString())
|
||||
}
|
||||
|
||||
val ZigToolchain.zlsUUID: UUID? get() {
|
||||
return extraData["zls_uuid"]?.asUUID()
|
||||
}
|
||||
|
||||
val ZigToolchain.zls: ZLSVersion? get() {
|
||||
return zlsUUID?.let { zlsInstallations[it] }
|
||||
}
|
||||
|
||||
val Project.zls: ZLSVersion? get() = ZigToolchainService.getInstance(this).toolchain?.zls
|
||||
|
|
|
@ -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<ZLSVersion> {
|
||||
data class ZLSVersion(val path: Path, override val name: String? = null, val settings: ZLSSettings = ZLSSettings()): NamedObject<ZLSVersion> {
|
||||
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
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<ZLSVersion, ZLSVersionInfo>(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<ZLSVersion> {
|
||||
return ZLSLocalSelector(component)
|
||||
}
|
||||
|
||||
override suspend fun downloadVersionList(): List<ZLSVersionInfo> {
|
||||
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<Path> {
|
||||
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<Path>()
|
||||
if (xdgDataHome != null && xdgDataHome.isDirectory()) {
|
||||
res.add(xdgDataHome.resolve("zls"))
|
||||
}
|
||||
res.add(home.resolve(".zls"))
|
||||
return res
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<ZLSVersion>(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 }
|
||||
}
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<ZLSVersionInfo> {
|
||||
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<JsonObject>(it) }
|
||||
index.delete()
|
||||
return@withContext listOfNotNull(parseVersion(info))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun parseVersion(data: JsonObject): ZLSVersionInfo? {
|
||||
val versionTag = data["version"]?.asSafely<JsonPrimitive>()?.content
|
||||
|
||||
val version = SemVer.parseFromText(versionTag) ?: return null
|
||||
val date = data["date"]?.asSafely<JsonPrimitive>()?.content ?: ""
|
||||
val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) }
|
||||
?: return null
|
||||
|
||||
return ZLSVersionInfo(version, date, dist)
|
||||
}
|
|
@ -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<ZLSVersion> {
|
||||
sealed interface ZLSDriver: UUIDComboBoxDriver<ZLSVersion> {
|
||||
override val theMap: UUIDMapSerializable.Converting<ZLSVersion, *, *>
|
||||
get() = zlsInstallations
|
||||
|
||||
override fun constructModelList(): List<ListElemIn<ZLSVersion>> {
|
||||
return ListElem.fetchGroup()
|
||||
}
|
||||
|
||||
override fun createContext(model: ZBModel<ZLSVersion>): ZBContext<ZLSVersion> {
|
||||
return ZLSContext(null, model)
|
||||
}
|
||||
|
@ -52,16 +61,67 @@ object ZLSDriver: UUIDComboBoxDriver<ZLSVersion> {
|
|||
return ZLSComboBox(model)
|
||||
}
|
||||
|
||||
override fun createNamedConfigurable(uuid: UUID, elem: ZLSVersion): NamedConfigurable<UUID> {
|
||||
return ZLSConfigurable(uuid, elem)
|
||||
}
|
||||
|
||||
override suspend fun resolvePseudo(
|
||||
context: Component,
|
||||
elem: ListElem.Pseudo<ZLSVersion>
|
||||
): 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<UUID> {
|
||||
//TODO
|
||||
return ZLSConfigurable(uuid, elem)
|
||||
object ForList: ZLSDriver {
|
||||
override fun constructModelList(): List<ListElemIn<ZLSVersion>> {
|
||||
return listOf(ListElem.None(), Separator("", true), ListElem.FromDisk())
|
||||
}
|
||||
}
|
||||
|
||||
@JvmRecord
|
||||
data class ForSelector(private val data: ZigProjectConfigurationProvider.IUserDataBridge?): ZLSDriver {
|
||||
override fun constructModelList(): List<ListElemIn<ZLSVersion>> {
|
||||
val res = ArrayList<ListElemIn<ZLSVersion>>()
|
||||
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<ZLSVersion>
|
||||
): 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<Actual<ZLSVersion>> = 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
|
||||
}
|
|
@ -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<ZLSVersion>(ZLSDriver),
|
||||
SubConfigurable<Project>,
|
||||
class ZLSEditor<T: ZigToolchain>(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge?):
|
||||
UUIDMapSelector<ZLSVersion>(ZLSDriver.ForSelector(sharedState)),
|
||||
ImmutableElementPanel<T>,
|
||||
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<Project>? {
|
||||
return ZLSEditor(project, sharedState)
|
||||
class Provider: ZigToolchainExtensionsProvider {
|
||||
override fun <T : ZigToolchain> createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): ImmutableElementPanel<T>? {
|
||||
return ZLSEditor(sharedState)
|
||||
}
|
||||
|
||||
override val index: Int
|
||||
get() = 50
|
||||
get() = 100
|
||||
|
||||
}
|
||||
}
|
|
@ -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<ZLSVersion>(ZLSDriver) {
|
||||
class ZLSListEditor : UUIDMapEditor<ZLSVersion>(ZLSDriver.ForList) {
|
||||
override fun getEmptySelectionString(): String {
|
||||
return ZLSBundle.message("settings.list.empty")
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
<zlsConfigProvider
|
||||
implementation="com.falsepattern.zigbrains.lsp.ToolchainZLSConfigProvider"
|
||||
/>
|
||||
<projectConfigProvider
|
||||
<toolchainExtensionsProvider
|
||||
implementation="com.falsepattern.zigbrains.lsp.zls.ui.ZLSEditor$Provider"
|
||||
/>
|
||||
</extensions>
|
||||
|
|
Loading…
Add table
Reference in a new issue