toolchain+lsp management feature complete!

This commit is contained in:
FalsePattern 2025-04-11 16:47:01 +02:00
parent 281ce0ed4e
commit 8336d2bcc5
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
35 changed files with 756 additions and 237 deletions

View file

@ -25,6 +25,11 @@ which are the property of the Zig Software Foundation.
(https://github.com/ziglang/logo)
These art assets are licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0).
--------------------------------
The art assets inside src/art/zls, and all copies of them, are derived from the Zig Language Server,
which are the property of the zigtools organization.
(https://github.com/zigtools/zls)
These art assets are licensed under MIT license.
--------------------------------
Parts of the codebase are based on the intellij-zig plugin,
developed by HTGAzureX1212 (https://github.com/HTGAzureX1212), licensed under the Apache 2.0 license.
--------------------------------
@ -38,3 +43,4 @@ All of the licenses listed here are available in the following files, bundled wi
- licenses/GPL3.LICENSE
- licenses/INTELLIJ-RUST.LICENSE
- licenses/LGPL3.LICENSE
- licenses/ZLS.LICENSE

View file

@ -143,8 +143,8 @@ class DirenvService(val project: Project): SerializablePersistentStateComponent<
private val STATE_KEY = Key.create<DirenvState>("DIRENV_STATE")
fun getStateFor(data: UserDataHolder, project: Project?): DirenvState {
return data.getUserData(STATE_KEY) ?: project?.let { getInstance(project).isEnabled } ?: DirenvState.Disabled
fun getStateFor(data: UserDataHolder?, project: Project?): DirenvState {
return data?.getUserData(STATE_KEY) ?: project?.let { getInstance(project).isEnabled } ?: DirenvState.Disabled
}
fun setStateFor(data: UserDataHolder, state: DirenvState) {

View file

@ -25,8 +25,10 @@ package com.falsepattern.zigbrains.direnv
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.util.EnvironmentUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import org.jetbrains.annotations.NonNls
import java.io.File
import kotlin.io.path.absolute
@ -41,8 +43,6 @@ data class Env(val env: Map<String, String>) {
private fun getVariable(name: @NonNls String) =
env.getOrElse(name) { EnvironmentUtil.getValue(name) }
suspend fun findExecutableOnPATH(exe: @NonNls String) = findAllExecutablesOnPATH(exe).firstOrNull()
fun findAllExecutablesOnPATH(exe: @NonNls String) = flow {
val exeName = if (SystemInfo.isWindows) "$exe.exe" else exe
val paths = path ?: return@flow
@ -55,7 +55,7 @@ data class Env(val env: Map<String, String>) {
continue
emit(exePath)
}
}
}.flowOn(Dispatchers.IO)
companion object {
val empty = Env(emptyMap())

View file

@ -44,7 +44,10 @@ abstract class DirenvEditor<T>(private val sharedState: ZigProjectConfigurationP
it.addItemListener { e ->
if (e.stateChange != ItemEvent.SELECTED)
return@addItemListener
DirenvService.setStateFor(sharedState, DirenvState.Auto)
val item = e.item
if (item !is DirenvState)
return@addItemListener
DirenvService.setStateFor(sharedState, item)
}
}
}

View file

@ -37,7 +37,8 @@ import javax.swing.JComponent
abstract class ZigToolchainConfigurable<T: ZigToolchain>(
val uuid: UUID,
tc: T,
val data: ZigProjectConfigurationProvider.IUserDataBridge?
val data: ZigProjectConfigurationProvider.IUserDataBridge?,
val modal: Boolean
): NamedConfigurable<UUID>() {
var toolchain: T = tc
set(value) {
@ -46,9 +47,7 @@ abstract class ZigToolchainConfigurable<T: ZigToolchain>(
}
init {
data?.putUserData(TOOLCHAIN_KEY, Supplier{
myViews.fold(toolchain) { tc, view -> view.apply(tc) ?: tc }
})
data?.putUserData(TOOLCHAIN_KEY, Supplier{toolchain})
}
private var myViews: List<ImmutableElementPanel<T>> = emptyList()
@ -59,13 +58,14 @@ abstract class ZigToolchainConfigurable<T: ZigToolchain>(
if (views.isEmpty()) {
views = ArrayList<ImmutableElementPanel<T>>()
views.add(createPanel())
views.addAll(createZigToolchainExtensionPanels(data))
views.forEach { it.reset(toolchain) }
views.addAll(createZigToolchainExtensionPanels(data, if (modal) PanelState.ModalEditor else PanelState.ListEditor))
myViews = views
}
return panel {
val p = panel {
views.forEach { it.attach(this@panel) }
}.withMinimumWidth(20)
views.forEach { it.reset(toolchain) }
return p
}
override fun getEditableObject(): UUID? {
@ -99,6 +99,6 @@ abstract class ZigToolchainConfigurable<T: ZigToolchain>(
}
companion object {
val TOOLCHAIN_KEY: Key<Supplier<ZigToolchain>> = Key.create("TOOLCHAIN")
val TOOLCHAIN_KEY: Key<Supplier<ZigToolchain?>> = Key.create("TOOLCHAIN")
}
}

View file

@ -25,17 +25,22 @@ 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")
interface ZigToolchainExtensionsProvider {
fun <T : ZigToolchain> createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): ImmutableElementPanel<T>?
fun <T : ZigToolchain> createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?, state: PanelState): ImmutableElementPanel<T>?
val index: Int
}
fun <T: ZigToolchain> createZigToolchainExtensionPanels(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): List<ImmutableElementPanel<T>> {
fun <T: ZigToolchain> createZigToolchainExtensionPanels(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?, state: PanelState): List<ImmutableElementPanel<T>> {
return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull {
it.createExtensionPanel(sharedState)
it.createExtensionPanel(sharedState, state)
}
}
enum class PanelState {
ProjectEditor,
ListEditor,
ModalEditor
}

View file

@ -47,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, data: ZigProjectConfigurationProvider.IUserDataBridge?): ZigToolchainConfigurable<*>
fun createConfigurable(uuid: UUID, toolchain: ZigToolchain, data: ZigProjectConfigurationProvider.IUserDataBridge?, modal: Boolean): ZigToolchainConfigurable<*>
suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow<ZigToolchain>
fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean)
}
@ -64,9 +64,9 @@ fun ZigToolchain.toRef(): ZigToolchain.Ref {
return ZigToolchain.Ref(provider.serialMarker, provider.serialize(this), this.extraData)
}
fun ZigToolchain.createNamedConfigurable(uuid: UUID, data: ZigProjectConfigurationProvider.IUserDataBridge?): ZigToolchainConfigurable<*> {
fun ZigToolchain.createNamedConfigurable(uuid: UUID, data: ZigProjectConfigurationProvider.IUserDataBridge?, modal: Boolean): ZigToolchainConfigurable<*> {
val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException()
return provider.createConfigurable(uuid, this, data)
return provider.createConfigurable(uuid, this, data, modal)
}
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -32,19 +32,10 @@ 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()
}
override val windowTitle get() = ZigBrainsBundle.message("settings.toolchain.downloader.title")
override val versionInfoFetchTitle get() = ZigBrainsBundle.message("settings.toolchain.downloader.progress.fetch")
override fun downloadProgressTitle(version: ZigVersionInfo) = ZigBrainsBundle.message("settings.toolchain.downloader.progress.install", version.version.rawVersion)
override fun localSelector() = LocalToolchainSelector(component)
override suspend fun downloadVersionList() = ZigVersionInfo.downloadVersionList()
override fun getSuggestedPath() = getSuggestedLocalToolchainPath()
}

View file

@ -29,8 +29,9 @@ import java.util.UUID
class LocalZigToolchainConfigurable(
uuid: UUID,
toolchain: LocalZigToolchain,
data: ZigProjectConfigurationProvider.IUserDataBridge?
): ZigToolchainConfigurable<LocalZigToolchain>(uuid, toolchain, data) {
data: ZigProjectConfigurationProvider.IUserDataBridge?,
modal: Boolean
): ZigToolchainConfigurable<LocalZigToolchain>(uuid, toolchain, data, modal) {
override fun createPanel() = LocalZigToolchainPanel()
override fun setDisplayName(name: String?) {

View file

@ -100,10 +100,10 @@ class LocalZigToolchainPanel() : ImmutableNamedElementPanelBase<LocalZigToolchai
return toolchain.copy(location = location, std = std, name = nameFieldValue ?: "")
}
override fun reset(toolchain: LocalZigToolchain) {
nameFieldValue = toolchain.name
this.pathToToolchain.text = toolchain.location.pathString
val std = toolchain.std
override fun reset(toolchain: LocalZigToolchain?) {
nameFieldValue = toolchain?.name ?: ""
this.pathToToolchain.text = toolchain?.location?.pathString ?: ""
val std = toolchain?.std
if (std != null) {
stdFieldOverride.isSelected = true
pathToStd.text = std.pathString

View file

@ -28,6 +28,7 @@ import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvid
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider
import com.falsepattern.zigbrains.shared.ui.renderPathNameComponent
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.io.FileUtil
@ -86,10 +87,11 @@ class LocalZigToolchainProvider: ZigToolchainProvider {
override fun createConfigurable(
uuid: UUID,
toolchain: ZigToolchain,
data: ZigProjectConfigurationProvider.IUserDataBridge?
data: ZigProjectConfigurationProvider.IUserDataBridge?,
modal: Boolean
): ZigToolchainConfigurable<*> {
toolchain as LocalZigToolchain
return LocalZigToolchainConfigurable(uuid, toolchain, data)
return LocalZigToolchainConfigurable(uuid, toolchain, data, modal)
}
@OptIn(ExperimentalCoroutinesApi::class)
@ -114,29 +116,8 @@ class LocalZigToolchainProvider: ZigToolchainProvider {
override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) {
toolchain as LocalZigToolchain
val name = toolchain.name
val path = presentDetectedPath(toolchain.location.pathString)
val primary: String
var secondary: String?
val tooltip: String?
if (isSuggestion) {
primary = path
secondary = name
} else {
primary = name ?: "Zig"
secondary = path
}
if (isSelected) {
tooltip = secondary
secondary = null
} else {
tooltip = null
}
component.append(primary)
if (secondary != null) {
component.append(" ")
component.append(secondary, SimpleTextAttributes.GRAYED_ATTRIBUTES)
}
component.toolTipText = tooltip
val path = toolchain.location.pathString
renderPathNameComponent(path, name, "Zig", component, isSuggestion, isSelected)
}
}
@ -173,13 +154,3 @@ private fun getWellKnown(): List<Path> {
res.add(home.resolve(".zig"))
return res
}
private fun presentDetectedPath(home: String, maxLength: Int = 50, suffixLength: Int = 30): String {
//for macOS, let's try removing Bundle internals
var home = home
home = StringUtil.trimEnd(home, "/Contents/Home") //NON-NLS
home = StringUtil.trimEnd(home, "/Contents/MacOS") //NON-NLS
home = FileUtil.getLocationRelativeToUserHome(home, false)
home = StringUtil.shortenTextWithEllipsis(home, maxLength, suffixLength)
return home
}

View file

@ -32,5 +32,5 @@ interface ImmutableElementPanel<T>: Disposable {
* Returned object must be the exact same class as the provided one.
*/
fun apply(elem: T): T?
fun reset(elem: T)
fun reset(elem: T?)
}

View file

@ -63,7 +63,7 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver<ZigToolchain> {
}
object ForList: ZigToolchainDriver {
override fun constructModelList(): List<ListElemIn<ZigToolchain>> {
override suspend fun constructModelList(): List<ListElemIn<ZigToolchain>> {
val modelList = ArrayList<ListElemIn<ZigToolchain>>()
modelList.addAll(ListElem.fetchGroup())
modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true))
@ -75,12 +75,12 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver<ZigToolchain> {
uuid: UUID,
elem: ZigToolchain
): NamedConfigurable<UUID> {
return elem.createNamedConfigurable(uuid, ZigProjectConfigurationProvider.UserDataBridge())
return elem.createNamedConfigurable(uuid, ZigProjectConfigurationProvider.UserDataBridge(), false)
}
}
class ForSelector(val data: ZigProjectConfigurationProvider.IUserDataBridge): ZigToolchainDriver {
override fun constructModelList(): List<ListElemIn<ZigToolchain>> {
override suspend fun constructModelList(): List<ListElemIn<ZigToolchain>> {
val modelList = ArrayList<ListElemIn<ZigToolchain>>()
modelList.add(ListElem.None())
modelList.addAll(zigToolchainList.map { it.asActual() }.sortedBy { it.instance.name })
@ -95,7 +95,7 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver<ZigToolchain> {
uuid: UUID,
elem: ZigToolchain
): NamedConfigurable<UUID> {
return elem.createNamedConfigurable(uuid, data)
return elem.createNamedConfigurable(uuid, data, true)
}
}
}

View file

@ -26,7 +26,11 @@ import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.toolchain.base.PanelState
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.createZigToolchainExtensionPanels
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.SubConfigurable
import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector
import com.falsepattern.zigbrains.shared.zigCoroutineScope
@ -35,17 +39,23 @@ import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.Key
import com.intellij.ui.dsl.builder.Panel
import kotlinx.coroutines.launch
import java.util.UUID
import java.util.function.Supplier
class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge):
UUIDMapSelector<ZigToolchain>(ZigToolchainDriver.ForSelector(sharedState)),
SubConfigurable<Project>,
ZigProjectConfigurationProvider.UserDataListener
{
private var myViews: List<ImmutableElementPanel<ZigToolchain>> = emptyList()
init {
sharedState.putUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY, Supplier{selectedUUID?.let { zigToolchainList[it] }})
sharedState.addUserDataChangeListener(this)
}
override fun onUserDataChanged(key: Key<*>) {
if (key == ZigToolchainConfigurable.TOOLCHAIN_KEY)
return
zigCoroutineScope.launch { listChanged() }
}
@ -59,24 +69,63 @@ class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvide
) {
attachComboBoxRow(this)
}
var views = myViews
if (views.isEmpty()) {
views = ArrayList<ImmutableElementPanel<ZigToolchain>>()
views.addAll(createZigToolchainExtensionPanels(sharedState, PanelState.ProjectEditor))
myViews = views
}
views.forEach { it.attach(p) }
}
override fun onSelection(uuid: UUID?) {
sharedState.putUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY, Supplier{selectedUUID?.let { zigToolchainList[it] }})
refreshViews(uuid)
}
private fun refreshViews(uuid: UUID?) {
val toolchain = uuid?.let { zigToolchainList[it] }
myViews.forEach { it.reset(toolchain) }
}
override fun isModified(context: Project): Boolean {
return ZigToolchainService.getInstance(context).toolchainUUID != selectedUUID
val uuid = selectedUUID
if (ZigToolchainService.getInstance(context).toolchainUUID != selectedUUID) {
return true
}
if (uuid == null)
return false
val tc = zigToolchainList[uuid]
if (tc == null)
return false
return myViews.any { it.isModified(tc) }
}
override fun apply(context: Project) {
ZigToolchainService.getInstance(context).toolchainUUID = selectedUUID
val uuid = selectedUUID
ZigToolchainService.getInstance(context).toolchainUUID = uuid
if (uuid == null)
return
val tc = zigToolchainList[uuid]
if (tc == null)
return
val finalTc = myViews.fold(tc) { acc, view -> view.apply(acc) ?: acc }
zigToolchainList[uuid] = finalTc
}
override fun reset(context: Project?) {
val project = context ?: ProjectManager.getInstance().defaultProject
selectedUUID = ZigToolchainService.getInstance(project).toolchainUUID
val svc = ZigToolchainService.getInstance(project)
val uuid = svc.toolchainUUID
selectedUUID = uuid
refreshViews(uuid)
}
override fun dispose() {
super.dispose()
sharedState.removeUserDataChangeListener(this)
myViews.forEach { it.dispose() }
myViews = emptyList()
}
override val newProjectBeforeInitSelector get() = true

View file

@ -49,7 +49,7 @@ import javax.swing.event.DocumentEvent
import kotlin.io.path.pathString
abstract class LocalSelector<T>(val component: Component) {
suspend fun browse(preSelected: Path? = null): T? {
suspend open fun browse(preSelected: Path? = null): T? {
return withEDTContext(component.asContextElement()) {
doBrowseFromDisk(preSelected)
}

View file

@ -29,7 +29,7 @@ import java.util.UUID
interface UUIDComboBoxDriver<T> {
val theMap: UUIDMapSerializable.Converting<T, *, *>
fun constructModelList(): List<ListElemIn<T>>
suspend fun constructModelList(): List<ListElemIn<T>>
fun createContext(model: ZBModel<T>): ZBContext<T>
fun createComboBox(model: ZBModel<T>): ZBComboBox<T>
suspend fun resolvePseudo(context: Component, elem: ListElem.Pseudo<T>): UUID?

View file

@ -25,11 +25,13 @@ package com.falsepattern.zigbrains.shared.ui
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.shared.StorageChangeListener
import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.observable.util.whenListChanged
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.MasterDetailsComponent
@ -45,6 +47,7 @@ abstract class UUIDMapEditor<T>(val driver: UUIDComboBoxDriver<T>): MasterDetail
private var isTreeInitialized = false
private var registered: Boolean = false
private var selectOnNextReload: UUID? = null
private var disposed: Boolean = false
private val changeListener: StorageChangeListener = { this@UUIDMapEditor.listChanged() }
override fun createComponent(): JComponent {
@ -62,6 +65,9 @@ abstract class UUIDMapEditor<T>(val driver: UUIDComboBoxDriver<T>): MasterDetail
override fun createActions(fromPopup: Boolean): List<AnAction> {
val add = object : DumbAwareAction({ ZigBrainsBundle.message("settings.shared.list.add-action.name") }, Presentation.NULL_STRING, IconUtil.addIcon) {
override fun actionPerformed(e: AnActionEvent) {
zigCoroutineScope.launchWithEDT(ModalityState.current()) {
if (disposed)
return@launchWithEDT
val modelList = driver.constructModelList()
val model = ZBModel(modelList)
val context = driver.createContext(model)
@ -72,6 +78,7 @@ abstract class UUIDMapEditor<T>(val driver: UUIDComboBoxDriver<T>): MasterDetail
popup.showInBestPositionFor(e.dataContext)
}
}
}
return listOf(add, MyDeleteAction())
}
@ -86,6 +93,8 @@ abstract class UUIDMapEditor<T>(val driver: UUIDComboBoxDriver<T>): MasterDetail
if (elem !is ListElem.Pseudo)
return
zigCoroutineScope.launch(myWholePanel.asContextElement()) {
if (disposed)
return@launch
val uuid = driver.resolvePseudo(myWholePanel, elem)
if (uuid != null) {
withEDTContext(myWholePanel.asContextElement()) {
@ -103,6 +112,7 @@ abstract class UUIDMapEditor<T>(val driver: UUIDComboBoxDriver<T>): MasterDetail
override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.shared.list.empty")
override fun disposeUIResources() {
disposed = true
super.disposeUIResources()
if (registered) {
driver.theMap.removeChangeListener(changeListener)
@ -115,8 +125,12 @@ abstract class UUIDMapEditor<T>(val driver: UUIDComboBoxDriver<T>): MasterDetail
}
private fun reloadTree() {
if (disposed)
return
val currentSelection = selectedObject?.asSafely<UUID>()
selectedNode = null
myRoot.removeAllChildren()
(myTree.model as DefaultTreeModel).reload()
val onReload = selectOnNextReload
selectOnNextReload = null
var hasOnReload = false
@ -128,12 +142,10 @@ abstract class UUIDMapEditor<T>(val driver: UUIDComboBoxDriver<T>): MasterDetail
}
(myTree.model as DefaultTreeModel).reload()
if (hasOnReload) {
selectNodeInTree(onReload)
selectedNode = findNodeByObject(myRoot, onReload)
return
}
currentSelection?.let {
selectNodeInTree(it)
}
selectedNode = currentSelection?.let { findNodeByObject(myRoot, it) }
}
@RequiresEdt
@ -148,6 +160,8 @@ abstract class UUIDMapEditor<T>(val driver: UUIDComboBoxDriver<T>): MasterDetail
}
private suspend fun listChanged() {
if (disposed)
return
withEDTContext(myWholePanel.asContextElement()) {
reloadTree()
}

View file

@ -32,6 +32,7 @@ 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.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.observable.util.whenListChanged
import com.intellij.openapi.options.ShowSettingsUtil
@ -40,6 +41,7 @@ import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Row
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -54,7 +56,7 @@ abstract class UUIDMapSelector<T>(val driver: UUIDComboBoxDriver<T>): Disposable
private var editButton: JButton? = null
private val changeListener: StorageChangeListener = { this@UUIDMapSelector.listChanged() }
init {
model = ZBModel(driver.constructModelList())
model = ZBModel(emptyList())
comboBox = driver.createComboBox(model)
comboBox.addItemListener(::itemStateChanged)
driver.theMap.addChangeListener(changeListener)
@ -67,19 +69,26 @@ abstract class UUIDMapSelector<T>(val driver: UUIDComboBoxDriver<T>): Disposable
comboBox.isPopupVisible = true
}
}
zigCoroutineScope.launchWithEDT(ModalityState.any()) {
model.updateContents(driver.constructModelList())
}
}
protected var selectedUUID: UUID?
get() = comboBox.selectedUUID
set(value) {
runInEdt {
zigCoroutineScope.launchWithEDT(ModalityState.any()) {
applyUUIDNowOrOnReload(value)
}
}
protected open fun onSelection(uuid: UUID?) {}
private fun refreshButtonState(item: ListElem<*>) {
editButton?.isEnabled = item is ListElem.One.Actual<*>
val actual = item is ListElem.One.Actual<*>
editButton?.isEnabled = actual
editButton?.repaint()
onSelection(if (actual) item.uuid else null)
}
private fun itemStateChanged(event: ItemEvent) {
@ -106,6 +115,12 @@ abstract class UUIDMapSelector<T>(val driver: UUIDComboBoxDriver<T>): Disposable
@RequiresEdt
private fun tryReloadSelection() {
val list = model.toList()
if (list.size == 1) {
comboBox.selectedItem = list[0]
comboBox.isEnabled = false
return
}
comboBox.isEnabled = true
val onReload = selectOnNextReload
selectOnNextReload = null
if (onReload != null) {
@ -116,13 +131,13 @@ abstract class UUIDMapSelector<T>(val driver: UUIDComboBoxDriver<T>): Disposable
if (element == null) {
selectOnNextReload = onReload
} else {
model.selectedItem = element
comboBox.selectedItem = element
return
}
}
val selected = model.selected
if (selected != null && list.contains(selected)) {
model.selectedItem = selected
comboBox.selectedItem = selected
return
}
if (selected is ListElem.One.Actual<*>) {
@ -131,10 +146,10 @@ abstract class UUIDMapSelector<T>(val driver: UUIDComboBoxDriver<T>): Disposable
is ListElem.One.Actual -> it.uuid == uuid
else -> false
} }
model.selectedItem = element
comboBox.selectedItem = element
return
}
model.selectedItem = ListElem.None<Any>()
comboBox.selectedItem = ListElem.None<Any>()
}
protected suspend fun listChanged() {

View file

@ -29,11 +29,14 @@ import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.CellRendererPanel
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.GroupHeaderSeparator
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.panels.OpaquePanel
import com.intellij.ui.popup.list.ComboBoxPopup
import com.intellij.util.concurrency.annotations.RequiresEdt
@ -50,6 +53,7 @@ import java.util.function.Consumer
import javax.accessibility.AccessibleContext
import javax.swing.JList
import javax.swing.border.Border
import kotlin.io.path.pathString
class ZBComboBoxPopup<T>(
context: ZBContext<T>,
@ -65,7 +69,7 @@ open class ZBComboBox<T>(model: ZBModel<T>, renderer: (() -> ZBModel<T>)-> ZBCel
var selectedUUID: UUID?
set(value) {
if (value == null) {
selectedItem = ListElem.None
selectedItem = ListElem.None<Any>()
return
}
for (i in 0..<model.size) {
@ -77,7 +81,7 @@ open class ZBComboBox<T>(model: ZBModel<T>, renderer: (() -> ZBModel<T>)-> ZBCel
}
}
}
selectedItem = ListElem.None
selectedItem = ListElem.None<Any>()
}
get() {
val item = selectedItem
@ -252,4 +256,40 @@ abstract class ZBCellRenderer<T>(val getModel: () -> ZBModel<T>) : ColoredListCe
)
}
fun renderPathNameComponent(path: String, name: String?, nameFallback: String, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) {
val path = presentDetectedPath(path)
val primary: String
var secondary: String?
val tooltip: String?
if (isSuggestion) {
primary = path
secondary = name
} else {
primary = name ?: nameFallback
secondary = path
}
if (isSelected) {
tooltip = secondary
secondary = null
} else {
tooltip = null
}
component.append(primary)
if (secondary != null) {
component.append(" ")
component.append(secondary, SimpleTextAttributes.GRAYED_ATTRIBUTES)
}
component.toolTipText = tooltip
}
fun presentDetectedPath(home: String, maxLength: Int = 50, suffixLength: Int = 30): String {
//for macOS, let's try removing Bundle internals
var home = home
home = StringUtil.trimEnd(home, "/Contents/Home") //NON-NLS
home = StringUtil.trimEnd(home, "/Contents/MacOS") //NON-NLS
home = FileUtil.getLocationRelativeToUserHome(home, false)
home = StringUtil.shortenTextWithEllipsis(home, maxLength, suffixLength)
return home
}
private val EMPTY_ICON = EmptyIcon.create(1, 16)

21
licenses/ZLS.LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) ZLS contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,32 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.lsp
import com.intellij.openapi.util.IconLoader
import org.jetbrains.annotations.NonNls
@NonNls
object LSPIcons {
@JvmField
val ZLS = IconLoader.getIcon("/icons/zls.svg", LSPIcons::class.java)
}

View file

@ -0,0 +1,236 @@
/*
* 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.settings
import com.falsepattern.zigbrains.lsp.ZLSBundle
import com.falsepattern.zigbrains.lsp.config.SemanticTokens
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.util.Disposer
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.fields.ExtendableTextField
import com.intellij.ui.components.textFieldWithBrowseButton
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.Row
import org.jetbrains.annotations.PropertyKey
@Suppress("PrivatePropertyName")
class ZLSSettingsPanel() : ImmutableElementPanel<ZLSSettings> {
private val zlsConfigPath = textFieldWithBrowseButton(
null,
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor()
.withTitle(ZLSBundle.message("settings.zls-config-path.browse.title"))
).also { Disposer.register(this, it) }
private val inlayHints = JBCheckBox()
private val enable_snippets = JBCheckBox()
private val enable_argument_placeholders = JBCheckBox()
private val completion_label_details = JBCheckBox()
private val enable_build_on_save = JBCheckBox()
private val build_on_save_args = ExtendableTextField()
private val semantic_tokens = ComboBox(SemanticTokens.entries.toTypedArray())
private val inlay_hints_show_variable_type_hints = JBCheckBox()
private val inlay_hints_show_struct_literal_field_type = JBCheckBox()
private val inlay_hints_show_parameter_name = JBCheckBox()
private val inlay_hints_show_builtin = JBCheckBox()
private val inlay_hints_exclude_single_argument = JBCheckBox()
private val inlay_hints_hide_redundant_param_names = JBCheckBox()
private val inlay_hints_hide_redundant_param_names_last_token = JBCheckBox()
private val warn_style = JBCheckBox()
private val highlight_global_var_declarations = JBCheckBox()
private val skip_std_references = JBCheckBox()
private val prefer_ast_check_as_child_process = JBCheckBox()
private val builtin_path = ExtendableTextField()
private val build_runner_path = ExtendableTextField()
private val global_cache_path = ExtendableTextField()
override fun attach(p: Panel): Unit = with(p) {
fancyRow(
"settings.zls-config-path.label",
"settings.zls-config-path.tooltip"
) { cell(zlsConfigPath).align(AlignX.FILL) }
fancyRow(
"settings.enable_snippets.label",
"settings.enable_snippets.tooltip"
) { cell(enable_snippets) }
fancyRow(
"settings.enable_argument_placeholders.label",
"settings.enable_argument_placeholders.tooltip"
) { cell(enable_argument_placeholders) }
fancyRow(
"settings.completion_label_details.label",
"settings.completion_label_details.tooltip"
) { cell(completion_label_details) }
fancyRow(
"settings.enable_build_on_save.label",
"settings.enable_build_on_save.tooltip"
) { cell(enable_build_on_save) }
fancyRow(
"settings.build_on_save_args.label",
"settings.build_on_save_args.tooltip"
) { cell(build_on_save_args).resizableColumn().align(AlignX.FILL) }
fancyRow(
"settings.semantic_tokens.label",
"settings.semantic_tokens.tooltip"
) { cell(semantic_tokens) }
collapsibleGroup(ZLSBundle.message("settings.inlay-hints-group.label"), indent = false) {
fancyRow(
"settings.inlay-hints-enable.label",
"settings.inlay-hints-enable.tooltip"
) { cell(inlayHints) }
fancyRow(
"settings.inlay_hints_show_variable_type_hints.label",
"settings.inlay_hints_show_variable_type_hints.tooltip"
) { cell(inlay_hints_show_variable_type_hints) }
fancyRow(
"settings.inlay_hints_show_struct_literal_field_type.label",
"settings.inlay_hints_show_struct_literal_field_type.tooltip"
) { cell(inlay_hints_show_struct_literal_field_type) }
fancyRow(
"settings.inlay_hints_show_parameter_name.label",
"settings.inlay_hints_show_parameter_name.tooltip"
) { cell(inlay_hints_show_parameter_name) }
fancyRow(
"settings.inlay_hints_show_builtin.label",
"settings.inlay_hints_show_builtin.tooltip"
) { cell(inlay_hints_show_builtin) }
fancyRow(
"settings.inlay_hints_exclude_single_argument.label",
"settings.inlay_hints_exclude_single_argument.tooltip"
) { cell(inlay_hints_exclude_single_argument) }
fancyRow(
"settings.inlay_hints_hide_redundant_param_names.label",
"settings.inlay_hints_hide_redundant_param_names.tooltip"
) { cell(inlay_hints_hide_redundant_param_names) }
fancyRow(
"settings.inlay_hints_hide_redundant_param_names_last_token.label",
"settings.inlay_hints_hide_redundant_param_names_last_token.tooltip"
) { cell(inlay_hints_hide_redundant_param_names_last_token) }
}
fancyRow(
"settings.warn_style.label",
"settings.warn_style.tooltip"
) { cell(warn_style) }
fancyRow(
"settings.highlight_global_var_declarations.label",
"settings.highlight_global_var_declarations.tooltip"
) { cell(highlight_global_var_declarations) }
fancyRow(
"settings.skip_std_references.label",
"settings.skip_std_references.tooltip"
) { cell(skip_std_references) }
fancyRow(
"settings.prefer_ast_check_as_child_process.label",
"settings.prefer_ast_check_as_child_process.tooltip"
) { cell(prefer_ast_check_as_child_process) }
fancyRow(
"settings.builtin_path.label",
"settings.builtin_path.tooltip"
) { cell(builtin_path).resizableColumn().align(AlignX.FILL) }
fancyRow(
"settings.build_runner_path.label",
"settings.build_runner_path.tooltip"
) { cell(build_runner_path).resizableColumn().align(AlignX.FILL) }
fancyRow(
"settings.global_cache_path.label",
"settings.global_cache_path.tooltip"
) { cell(global_cache_path).resizableColumn().align(AlignX.FILL) }
}
override fun isModified(elem: ZLSSettings): Boolean {
return elem != data
}
override fun apply(elem: ZLSSettings): ZLSSettings? {
return data
}
override fun reset(elem: ZLSSettings?) {
data = elem ?: ZLSSettings()
}
private var data
get() = ZLSSettings(
zlsConfigPath.text,
inlayHints.isSelected,
enable_snippets.isSelected,
enable_argument_placeholders.isSelected,
completion_label_details.isSelected,
enable_build_on_save.isSelected,
build_on_save_args.text,
semantic_tokens.item ?: SemanticTokens.full,
inlay_hints_show_variable_type_hints.isSelected,
inlay_hints_show_struct_literal_field_type.isSelected,
inlay_hints_show_parameter_name.isSelected,
inlay_hints_show_builtin.isSelected,
inlay_hints_exclude_single_argument.isSelected,
inlay_hints_hide_redundant_param_names.isSelected,
inlay_hints_hide_redundant_param_names_last_token.isSelected,
warn_style.isSelected,
highlight_global_var_declarations.isSelected,
skip_std_references.isSelected,
prefer_ast_check_as_child_process.isSelected,
builtin_path.text?.ifBlank { null },
build_runner_path.text?.ifBlank { null },
global_cache_path.text?.ifBlank { null },
)
set(value) {
zlsConfigPath.text = value.zlsConfigPath
inlayHints.isSelected = value.inlayHints
enable_snippets.isSelected = value.enable_snippets
enable_argument_placeholders.isSelected = value.enable_argument_placeholders
completion_label_details.isSelected = value.completion_label_details
enable_build_on_save.isSelected = value.enable_build_on_save
build_on_save_args.text = value.build_on_save_args
semantic_tokens.item = value.semantic_tokens
inlay_hints_show_variable_type_hints.isSelected = value.inlay_hints_show_variable_type_hints
inlay_hints_show_struct_literal_field_type.isSelected = value.inlay_hints_show_struct_literal_field_type
inlay_hints_show_parameter_name.isSelected = value.inlay_hints_show_parameter_name
inlay_hints_show_builtin.isSelected = value.inlay_hints_show_builtin
inlay_hints_exclude_single_argument.isSelected = value.inlay_hints_exclude_single_argument
inlay_hints_hide_redundant_param_names.isSelected = value.inlay_hints_hide_redundant_param_names
inlay_hints_hide_redundant_param_names_last_token.isSelected =
value.inlay_hints_hide_redundant_param_names_last_token
warn_style.isSelected = value.warn_style
highlight_global_var_declarations.isSelected = value.highlight_global_var_declarations
skip_std_references.isSelected = value.skip_std_references
prefer_ast_check_as_child_process.isSelected = value.prefer_ast_check_as_child_process
builtin_path.text = value.builtin_path ?: ""
build_runner_path.text = value.build_runner_path ?: ""
global_cache_path.text = value.global_cache_path ?: ""
}
override fun dispose() {
zlsConfigPath.dispose()
}
}
private fun Panel.fancyRow(
label: @PropertyKey(resourceBundle = "zigbrains.lsp.Bundle") String,
tooltip: @PropertyKey(resourceBundle = "zigbrains.lsp.Bundle") String,
cb: Row.() -> Unit
) = row(ZLSBundle.message(label)) {
contextHelp(ZLSBundle.message(tooltip))
cb()
}

View file

@ -26,6 +26,7 @@ import com.intellij.openapi.ui.NamedConfigurable
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.dsl.builder.panel
import java.awt.Dimension
import java.util.UUID
import javax.swing.JComponent
@ -56,9 +57,11 @@ class ZLSConfigurable(val uuid: UUID, zls: ZLSVersion): NamedConfigurable<UUID>(
view.reset(zls)
myView = view
}
return panel {
val p = panel {
view.attach(this@panel)
}.withMaximumWidth(20)
}
p.preferredSize = Dimension(640, 480)
return p
}
override fun getDisplayName(): @NlsContexts.ConfigurableName String? {

View file

@ -22,6 +22,7 @@
package com.falsepattern.zigbrains.lsp.zls
import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsPanel
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableNamedElementPanelBase
import com.falsepattern.zigbrains.shared.cli.call
@ -58,6 +59,7 @@ class ZLSPanel() : ImmutableNamedElementPanelBase<ZLSVersion>() {
Disposer.register(this, it)
}
private val zlsVersion = JBTextArea().also { it.isEditable = false }
private var settingsPanel: ZLSSettingsPanel? = null
private var debounce: Job? = null
override fun attach(p: Panel): Unit = with(p) {
@ -68,22 +70,28 @@ class ZLSPanel() : ImmutableNamedElementPanelBase<ZLSVersion>() {
row("Version:") {
cell(zlsVersion)
}
val sp = ZLSSettingsPanel()
p.collapsibleGroup("Settings", indent = false) {
sp.attach(this@collapsibleGroup)
}
settingsPanel = sp
}
override fun isModified(version: ZLSVersion): Boolean {
val name = nameFieldValue ?: return false
val path = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() ?: return false
return name != version.name || version.path != path
return name != version.name || version.path != path || settingsPanel?.isModified(version.settings) == true
}
override fun apply(version: ZLSVersion): ZLSVersion? {
val path = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() ?: return null
return version.copy(path = path, name = nameFieldValue ?: "")
return version.copy(path = path, name = nameFieldValue ?: "", settings = settingsPanel?.apply(version.settings) ?: version.settings)
}
override fun reset(version: ZLSVersion) {
nameFieldValue = version.name
this.pathToZLS.text = version.path.pathString
override fun reset(version: ZLSVersion?) {
nameFieldValue = version?.name ?: ""
this.pathToZLS.text = version?.path?.pathString ?: ""
settingsPanel?.reset(version?.settings)
dispatchUpdateUI()
}
@ -127,5 +135,7 @@ class ZLSPanel() : ImmutableNamedElementPanelBase<ZLSVersion>() {
override fun dispose() {
debounce?.cancel("Disposed")
settingsPanel?.dispose()
settingsPanel = null
}
}

View file

@ -63,10 +63,6 @@ data class ZLSVersion(val path: Path, override val name: String? = null, val set
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

View file

@ -23,70 +23,26 @@
package com.falsepattern.zigbrains.lsp.zls.downloader
import com.falsepattern.zigbrains.lsp.zls.ZLSVersion
import com.falsepattern.zigbrains.lsp.zls.ui.getSuggestedZLSPath
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.IUserDataBridge
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
import com.falsepattern.zigbrains.shared.downloader.Downloader
import 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)
}
class ZLSDownloader(component: Component, private val data: IUserDataBridge?) : Downloader<ZLSVersion, ZLSVersionInfo>(component) {
override val windowTitle get() = "Install ZLS"
override val versionInfoFetchTitle get() = "Fetching zls version information"
override fun downloadProgressTitle(version: ZLSVersionInfo) = "Installing ZLS ${version.version.rawVersion}"
override fun localSelector() = 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)
val toolchain = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY)?.get()
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
override fun getSuggestedPath() = getSuggestedZLSPath()
}

View file

@ -29,8 +29,10 @@ import com.falsepattern.zigbrains.shared.withUniqueName
import com.intellij.icons.AllIcons
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.util.SystemInfo
import java.awt.Component
import java.nio.file.Path
import kotlin.io.path.isDirectory
class ZLSLocalSelector(component: Component) : LocalSelector<ZLSVersion>(component) {
override val windowTitle: String
@ -38,6 +40,13 @@ class ZLSLocalSelector(component: Component) : LocalSelector<ZLSVersion>(compone
override val descriptor: FileChooserDescriptor
get() = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor().withTitle("ZLS binary")
override suspend fun browse(preSelected: Path?): ZLSVersion? {
if (preSelected?.isDirectory() == true) {
return super.browse(preSelected.resolve(if (SystemInfo.isWindows) "zls.exe" else "zls"))
}
return super.browse(preSelected)
}
override suspend fun verify(path: Path): VerifyResult {
var zls = resolve(path, null)
var result: VerifyResult

View file

@ -53,12 +53,17 @@ data class ZLSVersionInfo(
): VersionInfo {
companion object {
@OptIn(ExperimentalSerializationApi::class)
suspend fun downloadVersionInfoFor(toolchain: ZigToolchain, project: Project?): List<ZLSVersionInfo> {
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 single = toolchain != null
val url = if (single) {
getToolchainURL(toolchain, project) ?: return@withContext emptyList()
} else {
multiURL
}
val service = DownloadableFileService.getInstance()
val tempFile = FileUtil.createTempFile(tempPluginDir, "zls_version_info", ".json", false, false)
val desc = service.createFileDescription("https://releases.zigtools.org/v1/zls/select-version?zig_version=${URLEncoder.encode(zigVersion, Charsets.UTF_8)}&compatibility=only-runtime", tempFile.name)
val desc = service.createFileDescription(url, tempFile.name)
val downloader = service.createDownloader(listOf(desc), "ZLS version information")
val downloadResults = coroutineToIndicator {
downloader.download(tempPluginDir)
@ -68,13 +73,27 @@ data class ZLSVersionInfo(
val index = downloadResults[0].first
val info = index.inputStream().use { Json.decodeFromStream<JsonObject>(it) }
index.delete()
return@withContext listOfNotNull(parseVersion(info))
return@withContext if (single) {
listOfNotNull(parseVersion(null, info))
} else {
info.mapNotNull { (key, value) -> parseVersion(key, value) }
}
}
}
}
private fun parseVersion(data: JsonObject): ZLSVersionInfo? {
val versionTag = data["version"]?.asSafely<JsonPrimitive>()?.content
}
private suspend fun getToolchainURL(toolchain: ZigToolchain, project: Project?): String? {
val zigVersion = toolchain.zig.getEnv(project).getOrNull()?.version ?: return null
return "https://releases.zigtools.org/v1/zls/select-version?zig_version=${URLEncoder.encode(zigVersion, Charsets.UTF_8)}&compatibility=only-runtime"
}
private const val multiURL: String = "https://builds.zigtools.org/index.json"
private fun parseVersion(versionKey: String?, data: JsonElement): ZLSVersionInfo? {
if (data !is JsonObject) {
return null
}
val versionTag = data["version"]?.asSafely<JsonPrimitive>()?.content ?: versionKey
val version = SemVer.parseFromText(versionTag) ?: return null
val date = data["date"]?.asSafely<JsonPrimitive>()?.content ?: ""

View file

@ -22,13 +22,16 @@
package com.falsepattern.zigbrains.lsp.zls.ui
import com.falsepattern.zigbrains.direnv.DirenvService
import com.falsepattern.zigbrains.direnv.Env
import com.falsepattern.zigbrains.lsp.ZLSBundle
import com.falsepattern.zigbrains.lsp.zls.ZLSConfigurable
import com.falsepattern.zigbrains.lsp.zls.ZLSVersion
import com.falsepattern.zigbrains.lsp.zls.downloader.ZLSDownloader
import com.falsepattern.zigbrains.lsp.zls.downloader.ZLSLocalSelector
import com.falsepattern.zigbrains.lsp.zls.zlsInstallations
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable.Companion.TOOLCHAIN_KEY
import com.falsepattern.zigbrains.shared.UUIDMapSerializable
import com.falsepattern.zigbrains.shared.ui.ListElem
import com.falsepattern.zigbrains.shared.ui.ListElem.One.Actual
@ -39,15 +42,25 @@ 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.falsepattern.zigbrains.shared.withUniqueName
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.NamedConfigurable
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.util.system.OS
import com.intellij.util.text.SemVer
import kotlinx.coroutines.async
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.flowOn
import java.awt.Component
import java.nio.file.Files
import java.nio.file.Path
import java.util.UUID
import kotlin.io.path.isDirectory
import kotlin.io.path.isExecutable
import kotlin.io.path.isRegularFile
sealed interface ZLSDriver: UUIDComboBoxDriver<ZLSVersion> {
override val theMap: UUIDMapSerializable.Converting<ZLSVersion, *, *>
@ -70,58 +83,146 @@ sealed interface ZLSDriver: UUIDComboBoxDriver<ZLSVersion> {
elem: ListElem.Pseudo<ZLSVersion>
): UUID? {
return when(elem) {
is ListElem.FromDisk<*> -> ZLSLocalSelector(context).browse()
else -> null
is ListElem.One.Suggested -> zlsInstallations.withUniqueName(elem.instance)
is ListElem.FromDisk -> ZLSLocalSelector(context).browse()
is ListElem.Download -> ZLSDownloader(context, data).download()
}?.let { zlsInstallations.registerNew(it) }
}
object ForList: ZLSDriver {
override fun constructModelList(): List<ListElemIn<ZLSVersion>> {
return listOf(ListElem.None(), Separator("", true), ListElem.FromDisk())
}
}
val data: ZigProjectConfigurationProvider.IUserDataBridge?
@JvmRecord
data class ForSelector(private val data: ZigProjectConfigurationProvider.IUserDataBridge?): ZLSDriver {
override fun constructModelList(): List<ListElemIn<ZLSVersion>> {
object ForList: ZLSDriver {
override suspend 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())
res.add(Separator(ZLSBundle.message("settings.model.detected.separator"), true))
res.add(suggestZLSVersions().asPending())
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) }
override val data: ZigProjectConfigurationProvider.IUserDataBridge?
get() = null
}
private fun compatibleInstallations(): Flow<Actual<ZLSVersion>> = flow {
@JvmRecord
data class ForSelector(override val data: ZigProjectConfigurationProvider.IUserDataBridge?): ZLSDriver {
override suspend fun constructModelList(): List<ListElemIn<ZLSVersion>> {
val (project, toolchainVersion) = unpack(data)
if (toolchainVersion == null) {
return listOf(ListElem.None())
}
val res = ArrayList<ListElemIn<ZLSVersion>>()
res.add(ListElem.None())
res.addAll(compatibleInstallations(toolchainVersion))
res.add(Separator("", true))
res.addAll(ListElem.fetchGroup())
res.add(Separator(ZLSBundle.message("settings.model.detected.separator"), true))
res.add(suggestZLSVersions(project, data, toolchainVersion).asPending())
return res
}
}
}
private suspend fun unpack(data: ZigProjectConfigurationProvider.IUserDataBridge?): Pair<Project?, SemVer?> {
val toolchain = data?.getUserData(TOOLCHAIN_KEY)?.get()
val project = data?.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY)
val toolchainVersion = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY)
?.get()
val toolchainVersion = toolchain
?.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))
return project to toolchainVersion
}
private fun suggestZLSVersions(project: Project? = null, data: ZigProjectConfigurationProvider.IUserDataBridge? = null, toolchainVersion: SemVer? = null): Flow<ZLSVersion> = flow {
val env = if (project != null && DirenvService.getStateFor(data, project).isEnabled(project)) {
DirenvService.getInstance(project).import()
} else {
Env.empty
}
val existing = zlsInstallations.map { (_, zls) -> zls }
env.findAllExecutablesOnPATH("zls").collect { path ->
if (existing.any { it.path == path }) {
return@collect
}
emitIfCompatible(path, toolchainVersion)
}
val exe = if (SystemInfo.isWindows) "zls.exe" else "zls"
getWellKnownZLS().forEach { wellKnown ->
runCatching {
Files.newDirectoryStream(wellKnown).use { stream ->
stream.asSequence().filterNotNull().forEach { dir ->
val path = dir.resolve(exe)
if (!path.isRegularFile() || !path.isExecutable()) {
return@forEach
}
if (existing.any { it.path == path }) {
return@forEach
}
emitIfCompatible(path, toolchainVersion)
}
}
}
}
}.flowOn(Dispatchers.IO)
private suspend fun FlowCollector<ZLSVersion>.emitIfCompatible(path: Path, toolchainVersion: SemVer?) {
val ver = ZLSVersion.tryFromPath(path) ?: return
if (isCompatible(ver, toolchainVersion)) {
emit(ver)
}
}
private suspend fun compatibleInstallations(toolchainVersion: SemVer): List<Actual<ZLSVersion>> {
return zlsInstallations.mapNotNull { (uuid, version) ->
if (!isCompatible(version, toolchainVersion)) {
return@mapNotNull null
}
Actual(uuid, version)
}
}
private suspend fun isCompatible(version: ZLSVersion, toolchainVersion: SemVer?): Boolean {
if (toolchainVersion == null)
return true
val zlsVersion = version.version() ?: return false
return numericVersionEquals(zlsVersion, toolchainVersion)
}
private fun numericVersionEquals(a: SemVer, b: SemVer): Boolean {
return a.major == b.major && a.minor == b.minor && a.patch == b.patch
}
fun getSuggestedZLSPath(): Path? {
return getWellKnownZLS().getOrNull(0)
}
/**
* Returns the paths to the following list of folders:
*
* 1. DATA/zls
* 2. HOME/.zig
*
* Where DATA is:
* - ~/Library on macOS
* - %LOCALAPPDATA% on Windows
* - $XDG_DATA_HOME (or ~/.local/share if not set) on other OSes
*
* and HOME is the user home path
*/
private fun getWellKnownZLS(): List<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
}

View file

@ -28,6 +28,7 @@ import com.falsepattern.zigbrains.lsp.zls.ZLSVersion
import com.falsepattern.zigbrains.lsp.zls.withZLS
import com.falsepattern.zigbrains.lsp.zls.zlsUUID
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.toolchain.base.PanelState
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainExtensionsProvider
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel
@ -51,7 +52,7 @@ class ZLSEditor<T: ZigToolchain>(private val sharedState: ZigProjectConfiguratio
}
override fun attach(panel: Panel): Unit = with(panel) {
row("ZLS") {
row("Language Server") {
attachComboBoxRow(this)
}
}
@ -64,8 +65,12 @@ class ZLSEditor<T: ZigToolchain>(private val sharedState: ZigProjectConfiguratio
return toolchain.withZLS(selectedUUID)
}
override fun reset(toolchain: T) {
selectedUUID = toolchain.zlsUUID
override fun reset(toolchain: T?) {
selectedUUID = toolchain?.zlsUUID
zigCoroutineScope.launch {
listChanged()
selectedUUID = toolchain?.zlsUUID
}
}
override fun dispose() {
@ -74,7 +79,10 @@ class ZLSEditor<T: ZigToolchain>(private val sharedState: ZigProjectConfiguratio
}
class Provider: ZigToolchainExtensionsProvider {
override fun <T : ZigToolchain> createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): ImmutableElementPanel<T>? {
override fun <T : ZigToolchain> createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?, state: PanelState): ImmutableElementPanel<T>? {
if (state == PanelState.ModalEditor) {
return null
}
return ZLSEditor(sharedState)
}

View file

@ -25,7 +25,6 @@ package com.falsepattern.zigbrains.lsp.zls.ui
import com.falsepattern.zigbrains.lsp.ZLSBundle
import com.falsepattern.zigbrains.lsp.zls.ZLSVersion
import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor
import com.intellij.openapi.ui.MasterDetailsComponent
import com.intellij.openapi.util.NlsContexts
class ZLSListEditor : UUIDMapEditor<ZLSVersion>(ZLSDriver.ForList) {

View file

@ -22,15 +22,15 @@
package com.falsepattern.zigbrains.lsp.zls.ui
import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.lsp.LSPIcons
import com.falsepattern.zigbrains.lsp.ZLSBundle
import com.falsepattern.zigbrains.lsp.zls.ZLSVersion
import com.falsepattern.zigbrains.project.toolchain.base.render
import com.falsepattern.zigbrains.shared.ui.ListElem
import com.falsepattern.zigbrains.shared.ui.ZBCellRenderer
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.renderPathNameComponent
import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleTextAttributes
@ -56,17 +56,13 @@ class ZLSCellRenderer(getModel: () -> ZBModel<ZLSVersion>): ZBCellRenderer<ZLSVe
is ListElem.One -> {
val (icon, isSuggestion) = when(value) {
is ListElem.One.Suggested -> AllIcons.General.Information to true
is ListElem.One.Actual -> Icons.Zig to false
is ListElem.One.Actual -> LSPIcons.ZLS to false
}
this.icon = icon
val item = value.instance
//TODO proper renderer
if (item.name != null) {
append(item.name)
append(item.path.pathString, SimpleTextAttributes.GRAYED_ATTRIBUTES)
} else {
append(item.path.pathString)
}
val name = item.name
val path = item.path.pathString
renderPathNameComponent(path, name, "ZLS", this, isSuggestion, index == -1)
}
is ListElem.Download -> {

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 125.88 74.012" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="scale(1 2) translate(-4.3455 -50.664)">
<g transform="translate(67.635 -26.611)" fill="#f7a41d" stroke-width="1.1227" aria-label="ZLS">
<path d="m-62.391 113.61q-0.35928-0.9431-0.58383-2.4251-0.22455-1.5269-0.26946-3.1437 0-1.6617 0.08982-3.1437 0.13473-1.5269 0.4491-2.47 1.3024-0.494 2.8742-1.1227t3.2335-1.976q1.6617-1.3922 3.1886-3.997 1.5718-2.6497 2.7395-7.0957l-1.3473-0.26946q-0.80838 3.0539-1.976 5.3892-1.1677 2.3353-3.1886 3.7724-1.976 1.4371-5.2544 1.8862-0.4491-2.8742-0.67365-6.0628-0.17964-3.2335-0.17964-6.2425 0.04491-3.009 0.22455-5.2994 0.22455-2.3353 0.49401-3.4131 3.8173 0.13473 7.2754 0.26946 3.4581 0.08982 6.9161 0.17964 3.503 0.04491 7.2754 0.04491 2.6048 0 5.5688-0.04491 3.009-0.04491 5.8383-0.08982 2.8742-0.08982 5.0748-0.13473 2.2006-0.08982 3.2335-0.17964 0.4491 1.0778 0.67365 2.6497 0.22455 1.5718 0.22455 3.2784 0.04491 1.6617-0.13473 3.1437-0.17964 1.4371-0.58383 2.2455-3.7275 0.85329-6.467 4.3563-2.6946 3.503-4.4012 10.06l1.3473 0.31437q0.8982-3.2784 2.1108-5.8383 1.2575-2.5599 3.0988-3.997 1.8862-1.4371 4.6257-1.2575 0.26946 1.8413 0.40419 4.2215 0.13473 2.3353 0.13473 4.8503 0 2.515-0.08982 4.8503-0.08982 2.2904-0.26946 4.0868-0.17964 1.7515-0.40419 2.6048-3.4131-0.0449-6.467-0.22455-3.009-0.13472-6.2425-0.31436-3.2335-0.13473-7.2754-0.13473-8.8023 0-13.967 0.22455-5.1646 0.22454-7.3203 0.44909z"/>
<path d="m-18.514 113.61q-0.40419-1.0778-0.58383-2.5598-0.17964-1.482-0.22455-3.0539t0.08982-2.9191q0.17964-1.3922 0.4491-2.2006 0.53892-0.17964 1.2126-0.71856 0.71856-0.58382 1.2126-2.0209 0.53892-1.4371 0.53892-4.2215 0-2.6946-0.4491-4.1766-0.40419-1.482-1.0778-2.1108t-1.4371-0.71856q-0.40419-0.94311-0.58383-2.3802-0.17964-1.482-0.17964-3.0539 0.04491-1.5718 0.26946-3.009 0.22455-1.482 0.67365-2.4251 1.0778 0.08982 2.7844 0.22455 1.7066 0.08982 3.6377 0.22455 1.976 0.08982 3.8173 0.17964 1.8413 0.04491 3.2335 0.04491 2.0209 0 4.1766-0.08982 2.2006-0.13473 4.0419-0.31437 1.8862-0.17964 3.009-0.26946 0.40419 0.94311 0.53892 2.5149 0.17964 1.5718 0.13473 3.3233-0.04491 1.7066-0.22455 3.2784-0.17964 1.5718-0.53892 2.5149-1.0778 0-1.6168 0.85329-0.49401 0.80838-0.67365 2.1557-0.13473 1.3473-0.13473 2.8742v4.6706h1.3024q-0.08982-2.1108 0-4.0868 0.13473-1.976 0.31437-2.8293 2.0659-0.35928 4.6706-0.4491 2.6497-0.13473 5.2994 0 2.6946 0.08982 4.8053 0.4491 0.26946 1.1677 0.40419 3.3233 0.17964 2.1108 0.17964 4.7155 0.04491 2.5599 0 5.0748-0.04491 2.47-0.22455 4.4012-0.13473 1.9311-0.35928 2.7844-4.491-0.0898-9.3861-0.40418-4.8503-0.26946-9.9251-0.26946-2.5149 0-5.4341 0.0449-2.8742 0.0898-5.6586 0.17964-2.7844 0.13473-4.9401 0.22454-2.1557 0.13473-3.1437 0.22455z"/>
<path d="m48.357 114.28q-6.3772 0-9.5209-1.5269-3.0988-1.5718-3.1886-5.5239-0.58383 1.6617-0.40419 3.0988 0.22455 1.4371 0.71856 2.8293-4.8053 0.13473-7.9939 0.26945-3.1886 0.17964-5.7934 0.13473-0.67365-3.5928-0.76347-7.0957-0.04491-3.5479 0.53892-7.4999 2.47 0 4.9401 0.49401 2.47 0.49401 4.6706 1.3922 2.2006 0.85329 3.7724 2.1108l0.8982-0.9431q-1.5269-1.2126-3.3233-2.0209-1.7515-0.85329-3.5479-1.4371-2.2904-0.80838-4.1766-2.0658-1.8413-1.2575-2.9191-3.1437-1.0778-1.8862-1.0778-4.491 0-3.2335 1.482-5.8383 1.482-2.6497 4.8503-4.1766 3.4131-1.5718 9.1616-1.5718 5.7035 0 8.7125 1.6168 3.009 1.5718 3.0988 5.8383 0.62874-1.7066 0.40419-3.1886-0.22455-1.482-0.71856-3.1437 3.2335-0.04491 5.3892-0.08982 2.1557-0.08982 3.8173-0.17964 1.6617-0.08982 3.3682-0.08982 0.67365 3.2335 0.71856 7.0059 0.08982 3.7275-0.49401 7.6347-2.5599-0.04491-4.5808-0.4491-1.976-0.4491-3.6826-1.3024-1.7066-0.85329-3.503-2.1108l-0.80838 1.0329q1.7066 1.2575 3.503 2.1557 1.7964 0.85329 3.3682 1.5718 2.3353 1.0329 3.9521 2.2904 1.6168 1.2575 2.47 3.0539 0.8982 1.7964 0.8982 4.491 0 3.7275-1.3024 5.9281-1.2575 2.2006-3.3682 3.2784-2.1108 1.0329-4.6257 1.3473-2.47 0.31437-4.9401 0.31437z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -68,7 +68,7 @@ lsp.zls.name=Zig Language Server
lsp.zls.description=The <a href="https://github.com/Zigtools/ZLS">Zig Language Server</a>, via ZigBrains
settings.list.title=ZLS Instances
settings.list.empty=Select a ZLS version to view or edit its details here
settings.model.detected.separator=Detected ZLS version
settings.model.detected.separator=Detected ZLS versions
settings.model.none.text=<No ZLS>
settings.model.loading.text=Loading\u2026
settings.model.from-disk.text=Add ZLS from disk\u2026

19
src/art/zls/zls.svg Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="475.77" height="139.86" version="1.1" viewBox="0 0 125.88 37.006" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(-4.3455 -50.664)">
<g transform="translate(67.635 -26.611)" fill="#f7a41d" stroke-width="1.1227" aria-label="ZLS">
<path d="m-62.391 113.61q-0.35928-0.9431-0.58383-2.4251-0.22455-1.5269-0.26946-3.1437 0-1.6617 0.08982-3.1437 0.13473-1.5269 0.4491-2.47 1.3024-0.494 2.8742-1.1227t3.2335-1.976q1.6617-1.3922 3.1886-3.997 1.5718-2.6497 2.7395-7.0957l-1.3473-0.26946q-0.80838 3.0539-1.976 5.3892-1.1677 2.3353-3.1886 3.7724-1.976 1.4371-5.2544 1.8862-0.4491-2.8742-0.67365-6.0628-0.17964-3.2335-0.17964-6.2425 0.04491-3.009 0.22455-5.2994 0.22455-2.3353 0.49401-3.4131 3.8173 0.13473 7.2754 0.26946 3.4581 0.08982 6.9161 0.17964 3.503 0.04491 7.2754 0.04491 2.6048 0 5.5688-0.04491 3.009-0.04491 5.8383-0.08982 2.8742-0.08982 5.0748-0.13473 2.2006-0.08982 3.2335-0.17964 0.4491 1.0778 0.67365 2.6497 0.22455 1.5718 0.22455 3.2784 0.04491 1.6617-0.13473 3.1437-0.17964 1.4371-0.58383 2.2455-3.7275 0.85329-6.467 4.3563-2.6946 3.503-4.4012 10.06l1.3473 0.31437q0.8982-3.2784 2.1108-5.8383 1.2575-2.5599 3.0988-3.997 1.8862-1.4371 4.6257-1.2575 0.26946 1.8413 0.40419 4.2215 0.13473 2.3353 0.13473 4.8503 0 2.515-0.08982 4.8503-0.08982 2.2904-0.26946 4.0868-0.17964 1.7515-0.40419 2.6048-3.4131-0.0449-6.467-0.22455-3.009-0.13472-6.2425-0.31436-3.2335-0.13473-7.2754-0.13473-8.8023 0-13.967 0.22455-5.1646 0.22454-7.3203 0.44909z"/>
<path d="m-18.514 113.61q-0.40419-1.0778-0.58383-2.5598-0.17964-1.482-0.22455-3.0539t0.08982-2.9191q0.17964-1.3922 0.4491-2.2006 0.53892-0.17964 1.2126-0.71856 0.71856-0.58382 1.2126-2.0209 0.53892-1.4371 0.53892-4.2215 0-2.6946-0.4491-4.1766-0.40419-1.482-1.0778-2.1108t-1.4371-0.71856q-0.40419-0.94311-0.58383-2.3802-0.17964-1.482-0.17964-3.0539 0.04491-1.5718 0.26946-3.009 0.22455-1.482 0.67365-2.4251 1.0778 0.08982 2.7844 0.22455 1.7066 0.08982 3.6377 0.22455 1.976 0.08982 3.8173 0.17964 1.8413 0.04491 3.2335 0.04491 2.0209 0 4.1766-0.08982 2.2006-0.13473 4.0419-0.31437 1.8862-0.17964 3.009-0.26946 0.40419 0.94311 0.53892 2.5149 0.17964 1.5718 0.13473 3.3233-0.04491 1.7066-0.22455 3.2784-0.17964 1.5718-0.53892 2.5149-1.0778 0-1.6168 0.85329-0.49401 0.80838-0.67365 2.1557-0.13473 1.3473-0.13473 2.8742v4.6706h1.3024q-0.08982-2.1108 0-4.0868 0.13473-1.976 0.31437-2.8293 2.0659-0.35928 4.6706-0.4491 2.6497-0.13473 5.2994 0 2.6946 0.08982 4.8053 0.4491 0.26946 1.1677 0.40419 3.3233 0.17964 2.1108 0.17964 4.7155 0.04491 2.5599 0 5.0748-0.04491 2.47-0.22455 4.4012-0.13473 1.9311-0.35928 2.7844-4.491-0.0898-9.3861-0.40418-4.8503-0.26946-9.9251-0.26946-2.5149 0-5.4341 0.0449-2.8742 0.0898-5.6586 0.17964-2.7844 0.13473-4.9401 0.22454-2.1557 0.13473-3.1437 0.22455z"/>
<path d="m48.357 114.28q-6.3772 0-9.5209-1.5269-3.0988-1.5718-3.1886-5.5239-0.58383 1.6617-0.40419 3.0988 0.22455 1.4371 0.71856 2.8293-4.8053 0.13473-7.9939 0.26945-3.1886 0.17964-5.7934 0.13473-0.67365-3.5928-0.76347-7.0957-0.04491-3.5479 0.53892-7.4999 2.47 0 4.9401 0.49401 2.47 0.49401 4.6706 1.3922 2.2006 0.85329 3.7724 2.1108l0.8982-0.9431q-1.5269-1.2126-3.3233-2.0209-1.7515-0.85329-3.5479-1.4371-2.2904-0.80838-4.1766-2.0658-1.8413-1.2575-2.9191-3.1437-1.0778-1.8862-1.0778-4.491 0-3.2335 1.482-5.8383 1.482-2.6497 4.8503-4.1766 3.4131-1.5718 9.1616-1.5718 5.7035 0 8.7125 1.6168 3.009 1.5718 3.0988 5.8383 0.62874-1.7066 0.40419-3.1886-0.22455-1.482-0.71856-3.1437 3.2335-0.04491 5.3892-0.08982 2.1557-0.08982 3.8173-0.17964 1.6617-0.08982 3.3682-0.08982 0.67365 3.2335 0.71856 7.0059 0.08982 3.7275-0.49401 7.6347-2.5599-0.04491-4.5808-0.4491-1.976-0.4491-3.6826-1.3024-1.7066-0.85329-3.503-2.1108l-0.80838 1.0329q1.7066 1.2575 3.503 2.1557 1.7964 0.85329 3.3682 1.5718 2.3353 1.0329 3.9521 2.2904 1.6168 1.2575 2.47 3.0539 0.8982 1.7964 0.8982 4.491 0 3.7275-1.3024 5.9281-1.2575 2.2006-3.3682 3.2784-2.1108 1.0329-4.6257 1.3473-2.47 0.31437-4.9401 0.31437z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB