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) (https://github.com/ziglang/logo)
These art assets are licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0). 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, 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. developed by HTGAzureX1212 (https://github.com/HTGAzureX1212), licensed under the Apache 2.0 license.
-------------------------------- --------------------------------
@ -37,4 +42,5 @@ All of the licenses listed here are available in the following files, bundled wi
- licenses/CC_BY_SA_4.0.LICENSE - licenses/CC_BY_SA_4.0.LICENSE
- licenses/GPL3.LICENSE - licenses/GPL3.LICENSE
- licenses/INTELLIJ-RUST.LICENSE - licenses/INTELLIJ-RUST.LICENSE
- licenses/LGPL3.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") private val STATE_KEY = Key.create<DirenvState>("DIRENV_STATE")
fun getStateFor(data: UserDataHolder, project: Project?): DirenvState { fun getStateFor(data: UserDataHolder?, project: Project?): DirenvState {
return data.getUserData(STATE_KEY) ?: project?.let { getInstance(project).isEnabled } ?: DirenvState.Disabled return data?.getUserData(STATE_KEY) ?: project?.let { getInstance(project).isEnabled } ?: DirenvState.Disabled
} }
fun setStateFor(data: UserDataHolder, state: DirenvState) { 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.SystemInfo
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.util.EnvironmentUtil import com.intellij.util.EnvironmentUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.NonNls
import java.io.File import java.io.File
import kotlin.io.path.absolute import kotlin.io.path.absolute
@ -41,8 +43,6 @@ data class Env(val env: Map<String, String>) {
private fun getVariable(name: @NonNls String) = private fun getVariable(name: @NonNls String) =
env.getOrElse(name) { EnvironmentUtil.getValue(name) } env.getOrElse(name) { EnvironmentUtil.getValue(name) }
suspend fun findExecutableOnPATH(exe: @NonNls String) = findAllExecutablesOnPATH(exe).firstOrNull()
fun findAllExecutablesOnPATH(exe: @NonNls String) = flow { fun findAllExecutablesOnPATH(exe: @NonNls String) = flow {
val exeName = if (SystemInfo.isWindows) "$exe.exe" else exe val exeName = if (SystemInfo.isWindows) "$exe.exe" else exe
val paths = path ?: return@flow val paths = path ?: return@flow
@ -55,7 +55,7 @@ data class Env(val env: Map<String, String>) {
continue continue
emit(exePath) emit(exePath)
} }
} }.flowOn(Dispatchers.IO)
companion object { companion object {
val empty = Env(emptyMap()) val empty = Env(emptyMap())

View file

@ -44,7 +44,10 @@ abstract class DirenvEditor<T>(private val sharedState: ZigProjectConfigurationP
it.addItemListener { e -> it.addItemListener { e ->
if (e.stateChange != ItemEvent.SELECTED) if (e.stateChange != ItemEvent.SELECTED)
return@addItemListener 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>( abstract class ZigToolchainConfigurable<T: ZigToolchain>(
val uuid: UUID, val uuid: UUID,
tc: T, tc: T,
val data: ZigProjectConfigurationProvider.IUserDataBridge? val data: ZigProjectConfigurationProvider.IUserDataBridge?,
val modal: Boolean
): NamedConfigurable<UUID>() { ): NamedConfigurable<UUID>() {
var toolchain: T = tc var toolchain: T = tc
set(value) { set(value) {
@ -46,9 +47,7 @@ abstract class ZigToolchainConfigurable<T: ZigToolchain>(
} }
init { init {
data?.putUserData(TOOLCHAIN_KEY, Supplier{ data?.putUserData(TOOLCHAIN_KEY, Supplier{toolchain})
myViews.fold(toolchain) { tc, view -> view.apply(tc) ?: tc }
})
} }
private var myViews: List<ImmutableElementPanel<T>> = emptyList() private var myViews: List<ImmutableElementPanel<T>> = emptyList()
@ -59,13 +58,14 @@ abstract class ZigToolchainConfigurable<T: ZigToolchain>(
if (views.isEmpty()) { if (views.isEmpty()) {
views = ArrayList<ImmutableElementPanel<T>>() views = ArrayList<ImmutableElementPanel<T>>()
views.add(createPanel()) views.add(createPanel())
views.addAll(createZigToolchainExtensionPanels(data)) views.addAll(createZigToolchainExtensionPanels(data, if (modal) PanelState.ModalEditor else PanelState.ListEditor))
views.forEach { it.reset(toolchain) }
myViews = views myViews = views
} }
return panel { val p = panel {
views.forEach { it.attach(this@panel) } views.forEach { it.attach(this@panel) }
}.withMinimumWidth(20) }.withMinimumWidth(20)
views.forEach { it.reset(toolchain) }
return p
} }
override fun getEditableObject(): UUID? { override fun getEditableObject(): UUID? {
@ -99,6 +99,6 @@ abstract class ZigToolchainConfigurable<T: ZigToolchain>(
} }
companion object { 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.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel
import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.util.UserDataHolder
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainExtensionsProvider>("com.falsepattern.zigbrains.toolchainExtensionsProvider") private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainExtensionsProvider>("com.falsepattern.zigbrains.toolchainExtensionsProvider")
interface ZigToolchainExtensionsProvider { 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 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 { 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 deserialize(data: Map<String, String>): ZigToolchain?
fun serialize(toolchain: ZigToolchain): Map<String, String> fun serialize(toolchain: ZigToolchain): Map<String, String>
fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean 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> suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow<ZigToolchain>
fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) 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) 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() 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) @OptIn(ExperimentalCoroutinesApi::class)

View file

@ -32,19 +32,10 @@ import java.awt.Component
import java.nio.file.Path import java.nio.file.Path
class LocalToolchainDownloader(component: Component) : Downloader<LocalZigToolchain, ZigVersionInfo>(component) { class LocalToolchainDownloader(component: Component) : Downloader<LocalZigToolchain, ZigVersionInfo>(component) {
override val windowTitle: String get() = ZigBrainsBundle.message("settings.toolchain.downloader.title") override val windowTitle get() = ZigBrainsBundle.message("settings.toolchain.downloader.title")
override val versionInfoFetchTitle: @NlsContexts.ProgressTitle String get() = ZigBrainsBundle.message("settings.toolchain.downloader.progress.fetch") override val versionInfoFetchTitle get() = ZigBrainsBundle.message("settings.toolchain.downloader.progress.fetch")
override fun downloadProgressTitle(version: ZigVersionInfo): @NlsContexts.ProgressTitle String { override fun downloadProgressTitle(version: ZigVersionInfo) = ZigBrainsBundle.message("settings.toolchain.downloader.progress.install", version.version.rawVersion)
return ZigBrainsBundle.message("settings.toolchain.downloader.progress.install", version.version.rawVersion) override fun localSelector() = LocalToolchainSelector(component)
} override suspend fun downloadVersionList() = ZigVersionInfo.downloadVersionList()
override fun localSelector(): LocalSelector<LocalZigToolchain> { override fun getSuggestedPath() = getSuggestedLocalToolchainPath()
return LocalToolchainSelector(component)
}
override suspend fun downloadVersionList(): List<ZigVersionInfo> {
return ZigVersionInfo.downloadVersionList()
}
override fun getSuggestedPath(): Path? {
return getSuggestedLocalToolchainPath()
}
} }

View file

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

View file

@ -100,10 +100,10 @@ class LocalZigToolchainPanel() : ImmutableNamedElementPanelBase<LocalZigToolchai
return toolchain.copy(location = location, std = std, name = nameFieldValue ?: "") return toolchain.copy(location = location, std = std, name = nameFieldValue ?: "")
} }
override fun reset(toolchain: LocalZigToolchain) { override fun reset(toolchain: LocalZigToolchain?) {
nameFieldValue = toolchain.name nameFieldValue = toolchain?.name ?: ""
this.pathToToolchain.text = toolchain.location.pathString this.pathToToolchain.text = toolchain?.location?.pathString ?: ""
val std = toolchain.std val std = toolchain?.std
if (std != null) { if (std != null) {
stdFieldOverride.isSelected = true stdFieldOverride.isSelected = true
pathToStd.text = std.pathString 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.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider 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.project.Project
import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtil
@ -86,10 +87,11 @@ class LocalZigToolchainProvider: ZigToolchainProvider {
override fun createConfigurable( override fun createConfigurable(
uuid: UUID, uuid: UUID,
toolchain: ZigToolchain, toolchain: ZigToolchain,
data: ZigProjectConfigurationProvider.IUserDataBridge? data: ZigProjectConfigurationProvider.IUserDataBridge?,
modal: Boolean
): ZigToolchainConfigurable<*> { ): ZigToolchainConfigurable<*> {
toolchain as LocalZigToolchain toolchain as LocalZigToolchain
return LocalZigToolchainConfigurable(uuid, toolchain, data) return LocalZigToolchainConfigurable(uuid, toolchain, data, modal)
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@ -114,29 +116,8 @@ class LocalZigToolchainProvider: ZigToolchainProvider {
override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) {
toolchain as LocalZigToolchain toolchain as LocalZigToolchain
val name = toolchain.name val name = toolchain.name
val path = presentDetectedPath(toolchain.location.pathString) val path = toolchain.location.pathString
val primary: String renderPathNameComponent(path, name, "Zig", component, isSuggestion, isSelected)
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
} }
} }
@ -172,14 +153,4 @@ private fun getWellKnown(): List<Path> {
} }
res.add(home.resolve(".zig")) res.add(home.resolve(".zig"))
return res 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. * Returned object must be the exact same class as the provided one.
*/ */
fun apply(elem: T): T? 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 { object ForList: ZigToolchainDriver {
override fun constructModelList(): List<ListElemIn<ZigToolchain>> { override suspend fun constructModelList(): List<ListElemIn<ZigToolchain>> {
val modelList = ArrayList<ListElemIn<ZigToolchain>>() val modelList = ArrayList<ListElemIn<ZigToolchain>>()
modelList.addAll(ListElem.fetchGroup()) modelList.addAll(ListElem.fetchGroup())
modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true))
@ -75,12 +75,12 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver<ZigToolchain> {
uuid: UUID, uuid: UUID,
elem: ZigToolchain elem: ZigToolchain
): NamedConfigurable<UUID> { ): NamedConfigurable<UUID> {
return elem.createNamedConfigurable(uuid, ZigProjectConfigurationProvider.UserDataBridge()) return elem.createNamedConfigurable(uuid, ZigProjectConfigurationProvider.UserDataBridge(), false)
} }
} }
class ForSelector(val data: ZigProjectConfigurationProvider.IUserDataBridge): ZigToolchainDriver { 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>>() val modelList = ArrayList<ListElemIn<ZigToolchain>>()
modelList.add(ListElem.None()) modelList.add(ListElem.None())
modelList.addAll(zigToolchainList.map { it.asActual() }.sortedBy { it.instance.name }) modelList.addAll(zigToolchainList.map { it.asActual() }.sortedBy { it.instance.name })
@ -95,7 +95,7 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver<ZigToolchain> {
uuid: UUID, uuid: UUID,
elem: ZigToolchain elem: ZigToolchain
): NamedConfigurable<UUID> { ): 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
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService 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.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.SubConfigurable
import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector
import com.falsepattern.zigbrains.shared.zigCoroutineScope 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.openapi.util.Key
import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.Panel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID
import java.util.function.Supplier
class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge):
UUIDMapSelector<ZigToolchain>(ZigToolchainDriver.ForSelector(sharedState)), UUIDMapSelector<ZigToolchain>(ZigToolchainDriver.ForSelector(sharedState)),
SubConfigurable<Project>, SubConfigurable<Project>,
ZigProjectConfigurationProvider.UserDataListener ZigProjectConfigurationProvider.UserDataListener
{ {
private var myViews: List<ImmutableElementPanel<ZigToolchain>> = emptyList()
init { init {
sharedState.putUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY, Supplier{selectedUUID?.let { zigToolchainList[it] }})
sharedState.addUserDataChangeListener(this) sharedState.addUserDataChangeListener(this)
} }
override fun onUserDataChanged(key: Key<*>) { override fun onUserDataChanged(key: Key<*>) {
if (key == ZigToolchainConfigurable.TOOLCHAIN_KEY)
return
zigCoroutineScope.launch { listChanged() } zigCoroutineScope.launch { listChanged() }
} }
@ -59,24 +69,63 @@ class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvide
) { ) {
attachComboBoxRow(this) 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 { 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) { 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?) { override fun reset(context: Project?) {
val project = context ?: ProjectManager.getInstance().defaultProject 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() { override fun dispose() {
super.dispose() super.dispose()
sharedState.removeUserDataChangeListener(this) sharedState.removeUserDataChangeListener(this)
myViews.forEach { it.dispose() }
myViews = emptyList()
} }
override val newProjectBeforeInitSelector get() = true override val newProjectBeforeInitSelector get() = true

View file

@ -49,7 +49,7 @@ import javax.swing.event.DocumentEvent
import kotlin.io.path.pathString import kotlin.io.path.pathString
abstract class LocalSelector<T>(val component: Component) { 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()) { return withEDTContext(component.asContextElement()) {
doBrowseFromDisk(preSelected) doBrowseFromDisk(preSelected)
} }

View file

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

View file

@ -32,6 +32,7 @@ import com.falsepattern.zigbrains.shared.ui.ListElem
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.application.EDT import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.observable.util.whenListChanged import com.intellij.openapi.observable.util.whenListChanged
import com.intellij.openapi.options.ShowSettingsUtil 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.ui.dsl.builder.Row
import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -54,7 +56,7 @@ abstract class UUIDMapSelector<T>(val driver: UUIDComboBoxDriver<T>): Disposable
private var editButton: JButton? = null private var editButton: JButton? = null
private val changeListener: StorageChangeListener = { this@UUIDMapSelector.listChanged() } private val changeListener: StorageChangeListener = { this@UUIDMapSelector.listChanged() }
init { init {
model = ZBModel(driver.constructModelList()) model = ZBModel(emptyList())
comboBox = driver.createComboBox(model) comboBox = driver.createComboBox(model)
comboBox.addItemListener(::itemStateChanged) comboBox.addItemListener(::itemStateChanged)
driver.theMap.addChangeListener(changeListener) driver.theMap.addChangeListener(changeListener)
@ -67,19 +69,26 @@ abstract class UUIDMapSelector<T>(val driver: UUIDComboBoxDriver<T>): Disposable
comboBox.isPopupVisible = true comboBox.isPopupVisible = true
} }
} }
zigCoroutineScope.launchWithEDT(ModalityState.any()) {
model.updateContents(driver.constructModelList())
}
} }
protected var selectedUUID: UUID? protected var selectedUUID: UUID?
get() = comboBox.selectedUUID get() = comboBox.selectedUUID
set(value) { set(value) {
runInEdt { zigCoroutineScope.launchWithEDT(ModalityState.any()) {
applyUUIDNowOrOnReload(value) applyUUIDNowOrOnReload(value)
} }
} }
protected open fun onSelection(uuid: UUID?) {}
private fun refreshButtonState(item: ListElem<*>) { private fun refreshButtonState(item: ListElem<*>) {
editButton?.isEnabled = item is ListElem.One.Actual<*> val actual = item is ListElem.One.Actual<*>
editButton?.isEnabled = actual
editButton?.repaint() editButton?.repaint()
onSelection(if (actual) item.uuid else null)
} }
private fun itemStateChanged(event: ItemEvent) { private fun itemStateChanged(event: ItemEvent) {
@ -106,6 +115,12 @@ abstract class UUIDMapSelector<T>(val driver: UUIDComboBoxDriver<T>): Disposable
@RequiresEdt @RequiresEdt
private fun tryReloadSelection() { private fun tryReloadSelection() {
val list = model.toList() val list = model.toList()
if (list.size == 1) {
comboBox.selectedItem = list[0]
comboBox.isEnabled = false
return
}
comboBox.isEnabled = true
val onReload = selectOnNextReload val onReload = selectOnNextReload
selectOnNextReload = null selectOnNextReload = null
if (onReload != null) { if (onReload != null) {
@ -116,13 +131,13 @@ abstract class UUIDMapSelector<T>(val driver: UUIDComboBoxDriver<T>): Disposable
if (element == null) { if (element == null) {
selectOnNextReload = onReload selectOnNextReload = onReload
} else { } else {
model.selectedItem = element comboBox.selectedItem = element
return return
} }
} }
val selected = model.selected val selected = model.selected
if (selected != null && list.contains(selected)) { if (selected != null && list.contains(selected)) {
model.selectedItem = selected comboBox.selectedItem = selected
return return
} }
if (selected is ListElem.One.Actual<*>) { 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 is ListElem.One.Actual -> it.uuid == uuid
else -> false else -> false
} } } }
model.selectedItem = element comboBox.selectedItem = element
return return
} }
model.selectedItem = ListElem.None<Any>() comboBox.selectedItem = ListElem.None<Any>()
} }
protected suspend fun listChanged() { 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.application.runInEdt
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox 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.CellRendererPanel
import com.intellij.ui.CollectionComboBoxModel import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.ColoredListCellRenderer import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.GroupHeaderSeparator import com.intellij.ui.GroupHeaderSeparator
import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.panels.OpaquePanel import com.intellij.ui.components.panels.OpaquePanel
import com.intellij.ui.popup.list.ComboBoxPopup import com.intellij.ui.popup.list.ComboBoxPopup
import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.concurrency.annotations.RequiresEdt
@ -50,6 +53,7 @@ import java.util.function.Consumer
import javax.accessibility.AccessibleContext import javax.accessibility.AccessibleContext
import javax.swing.JList import javax.swing.JList
import javax.swing.border.Border import javax.swing.border.Border
import kotlin.io.path.pathString
class ZBComboBoxPopup<T>( class ZBComboBoxPopup<T>(
context: ZBContext<T>, context: ZBContext<T>,
@ -65,7 +69,7 @@ open class ZBComboBox<T>(model: ZBModel<T>, renderer: (() -> ZBModel<T>)-> ZBCel
var selectedUUID: UUID? var selectedUUID: UUID?
set(value) { set(value) {
if (value == null) { if (value == null) {
selectedItem = ListElem.None selectedItem = ListElem.None<Any>()
return return
} }
for (i in 0..<model.size) { 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() { get() {
val item = selectedItem 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) 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.NlsContexts
import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.panel
import java.awt.Dimension
import java.util.UUID import java.util.UUID
import javax.swing.JComponent import javax.swing.JComponent
@ -56,9 +57,11 @@ class ZLSConfigurable(val uuid: UUID, zls: ZLSVersion): NamedConfigurable<UUID>(
view.reset(zls) view.reset(zls)
myView = view myView = view
} }
return panel { val p = panel {
view.attach(this@panel) view.attach(this@panel)
}.withMaximumWidth(20) }
p.preferredSize = Dimension(640, 480)
return p
} }
override fun getDisplayName(): @NlsContexts.ConfigurableName String? { override fun getDisplayName(): @NlsContexts.ConfigurableName String? {

View file

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

View file

@ -23,70 +23,26 @@
package com.falsepattern.zigbrains.lsp.zls.downloader package com.falsepattern.zigbrains.lsp.zls.downloader
import com.falsepattern.zigbrains.lsp.zls.ZLSVersion 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
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.IUserDataBridge
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
import com.falsepattern.zigbrains.shared.downloader.Downloader 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.openapi.util.io.toNioPathOrNull
import com.intellij.util.system.OS import com.intellij.util.system.OS
import java.awt.Component import java.awt.Component
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.isDirectory import kotlin.io.path.isDirectory
class ZLSDownloader(component: Component, private val data: ZigProjectConfigurationProvider.IUserDataBridge?) : Downloader<ZLSVersion, ZLSVersionInfo>(component) { class ZLSDownloader(component: Component, private val data: IUserDataBridge?) : Downloader<ZLSVersion, ZLSVersionInfo>(component) {
override val windowTitle: String override val windowTitle get() = "Install ZLS"
get() = "Install ZLS" override val versionInfoFetchTitle get() = "Fetching zls version information"
override val versionInfoFetchTitle: @NlsContexts.ProgressTitle String override fun downloadProgressTitle(version: ZLSVersionInfo) = "Installing ZLS ${version.version.rawVersion}"
get() = "Fetching zls version information" override fun localSelector() = ZLSLocalSelector(component)
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> { override suspend fun downloadVersionList(): List<ZLSVersionInfo> {
val toolchain = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY)?.get() ?: return emptyList() val toolchain = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY)?.get()
val project = data.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY) val project = data?.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY)
return ZLSVersionInfo.downloadVersionInfoFor(toolchain, project) return ZLSVersionInfo.downloadVersionInfoFor(toolchain, project)
} }
override fun getSuggestedPath() = getSuggestedZLSPath()
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
} }

View file

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

View file

@ -53,12 +53,17 @@ data class ZLSVersionInfo(
): VersionInfo { ): VersionInfo {
companion object { companion object {
@OptIn(ExperimentalSerializationApi::class) @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) { 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 service = DownloadableFileService.getInstance()
val tempFile = FileUtil.createTempFile(tempPluginDir, "zls_version_info", ".json", false, false) 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 downloader = service.createDownloader(listOf(desc), "ZLS version information")
val downloadResults = coroutineToIndicator { val downloadResults = coroutineToIndicator {
downloader.download(tempPluginDir) downloader.download(tempPluginDir)
@ -68,13 +73,27 @@ data class ZLSVersionInfo(
val index = downloadResults[0].first val index = downloadResults[0].first
val info = index.inputStream().use { Json.decodeFromStream<JsonObject>(it) } val info = index.inputStream().use { Json.decodeFromStream<JsonObject>(it) }
index.delete() 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 version = SemVer.parseFromText(versionTag) ?: return null
val date = data["date"]?.asSafely<JsonPrimitive>()?.content ?: "" val date = data["date"]?.asSafely<JsonPrimitive>()?.content ?: ""

View file

@ -22,13 +22,16 @@
package com.falsepattern.zigbrains.lsp.zls.ui 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.ZLSConfigurable
import com.falsepattern.zigbrains.lsp.zls.ZLSVersion import com.falsepattern.zigbrains.lsp.zls.ZLSVersion
import com.falsepattern.zigbrains.lsp.zls.downloader.ZLSDownloader import com.falsepattern.zigbrains.lsp.zls.downloader.ZLSDownloader
import com.falsepattern.zigbrains.lsp.zls.downloader.ZLSLocalSelector import com.falsepattern.zigbrains.lsp.zls.downloader.ZLSLocalSelector
import com.falsepattern.zigbrains.lsp.zls.zlsInstallations import com.falsepattern.zigbrains.lsp.zls.zlsInstallations
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider 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.UUIDMapSerializable
import com.falsepattern.zigbrains.shared.ui.ListElem import com.falsepattern.zigbrains.shared.ui.ListElem
import com.falsepattern.zigbrains.shared.ui.ListElem.One.Actual 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.ZBContext
import com.falsepattern.zigbrains.shared.ui.ZBModel import com.falsepattern.zigbrains.shared.ui.ZBModel
import com.falsepattern.zigbrains.shared.ui.asPending 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.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 com.intellij.util.text.SemVer
import kotlinx.coroutines.async import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.flowOn
import java.awt.Component import java.awt.Component
import java.nio.file.Files
import java.nio.file.Path
import java.util.UUID import java.util.UUID
import kotlin.io.path.isDirectory
import kotlin.io.path.isExecutable
import kotlin.io.path.isRegularFile
sealed interface ZLSDriver: UUIDComboBoxDriver<ZLSVersion> { sealed interface ZLSDriver: UUIDComboBoxDriver<ZLSVersion> {
override val theMap: UUIDMapSerializable.Converting<ZLSVersion, *, *> override val theMap: UUIDMapSerializable.Converting<ZLSVersion, *, *>
@ -70,58 +83,146 @@ sealed interface ZLSDriver: UUIDComboBoxDriver<ZLSVersion> {
elem: ListElem.Pseudo<ZLSVersion> elem: ListElem.Pseudo<ZLSVersion>
): UUID? { ): UUID? {
return when(elem) { return when(elem) {
is ListElem.FromDisk<*> -> ZLSLocalSelector(context).browse() is ListElem.One.Suggested -> zlsInstallations.withUniqueName(elem.instance)
else -> null is ListElem.FromDisk -> ZLSLocalSelector(context).browse()
is ListElem.Download -> ZLSDownloader(context, data).download()
}?.let { zlsInstallations.registerNew(it) } }?.let { zlsInstallations.registerNew(it) }
} }
object ForList: ZLSDriver { val data: ZigProjectConfigurationProvider.IUserDataBridge?
override fun constructModelList(): List<ListElemIn<ZLSVersion>> {
return listOf(ListElem.None(), Separator("", true), ListElem.FromDisk())
}
}
@JvmRecord object ForList: ZLSDriver {
data class ForSelector(private val data: ZigProjectConfigurationProvider.IUserDataBridge?): ZLSDriver { override suspend fun constructModelList(): List<ListElemIn<ZLSVersion>> {
override fun constructModelList(): List<ListElemIn<ZLSVersion>> {
val res = ArrayList<ListElemIn<ZLSVersion>>() val res = ArrayList<ListElemIn<ZLSVersion>>()
res.add(ListElem.None())
res.add(compatibleInstallations().asPending())
res.add(Separator("", true))
res.addAll(ListElem.fetchGroup()) res.addAll(ListElem.fetchGroup())
res.add(Separator(ZLSBundle.message("settings.model.detected.separator"), true))
res.add(suggestZLSVersions().asPending())
return res return res
} }
override suspend fun resolvePseudo( override val data: ZigProjectConfigurationProvider.IUserDataBridge?
context: Component, get() = null
elem: ListElem.Pseudo<ZLSVersion> }
): UUID? {
return when(elem) { @JvmRecord
is ListElem.FromDisk<*> -> ZLSLocalSelector(context).browse() data class ForSelector(override val data: ZigProjectConfigurationProvider.IUserDataBridge?): ZLSDriver {
is ListElem.Download<*> -> ZLSDownloader(context, data).download() override suspend fun constructModelList(): List<ListElemIn<ZLSVersion>> {
else -> null val (project, toolchainVersion) = unpack(data)
}?.let { zlsInstallations.registerNew(it) } if (toolchainVersion == null) {
} return listOf(ListElem.None())
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))
}
} }
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 = toolchain
?.zig
?.getEnv(project)
?.getOrNull()
?.version
?.let { SemVer.parseFromText(it) }
return project to toolchainVersion
}
private fun suggestZLSVersions(project: Project? = null, data: ZigProjectConfigurationProvider.IUserDataBridge? = null, toolchainVersion: SemVer? = null): Flow<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 { private fun numericVersionEquals(a: SemVer, b: SemVer): Boolean {
return a.major == b.major && a.minor == b.minor && a.patch == b.patch 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.withZLS
import com.falsepattern.zigbrains.lsp.zls.zlsUUID import com.falsepattern.zigbrains.lsp.zls.zlsUUID
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider 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.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainExtensionsProvider import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainExtensionsProvider
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel 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) { override fun attach(panel: Panel): Unit = with(panel) {
row("ZLS") { row("Language Server") {
attachComboBoxRow(this) attachComboBoxRow(this)
} }
} }
@ -64,8 +65,12 @@ class ZLSEditor<T: ZigToolchain>(private val sharedState: ZigProjectConfiguratio
return toolchain.withZLS(selectedUUID) return toolchain.withZLS(selectedUUID)
} }
override fun reset(toolchain: T) { override fun reset(toolchain: T?) {
selectedUUID = toolchain.zlsUUID selectedUUID = toolchain?.zlsUUID
zigCoroutineScope.launch {
listChanged()
selectedUUID = toolchain?.zlsUUID
}
} }
override fun dispose() { override fun dispose() {
@ -74,7 +79,10 @@ class ZLSEditor<T: ZigToolchain>(private val sharedState: ZigProjectConfiguratio
} }
class Provider: ZigToolchainExtensionsProvider { 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) 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.ZLSBundle
import com.falsepattern.zigbrains.lsp.zls.ZLSVersion import com.falsepattern.zigbrains.lsp.zls.ZLSVersion
import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor
import com.intellij.openapi.ui.MasterDetailsComponent
import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.NlsContexts
class ZLSListEditor : UUIDMapEditor<ZLSVersion>(ZLSDriver.ForList) { class ZLSListEditor : UUIDMapEditor<ZLSVersion>(ZLSDriver.ForList) {

View file

@ -22,15 +22,15 @@
package com.falsepattern.zigbrains.lsp.zls.ui 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.ZLSBundle
import com.falsepattern.zigbrains.lsp.zls.ZLSVersion 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.ListElem
import com.falsepattern.zigbrains.shared.ui.ZBCellRenderer import com.falsepattern.zigbrains.shared.ui.ZBCellRenderer
import com.falsepattern.zigbrains.shared.ui.ZBComboBox import com.falsepattern.zigbrains.shared.ui.ZBComboBox
import com.falsepattern.zigbrains.shared.ui.ZBContext import com.falsepattern.zigbrains.shared.ui.ZBContext
import com.falsepattern.zigbrains.shared.ui.ZBModel import com.falsepattern.zigbrains.shared.ui.ZBModel
import com.falsepattern.zigbrains.shared.ui.renderPathNameComponent
import com.intellij.icons.AllIcons import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleTextAttributes import com.intellij.ui.SimpleTextAttributes
@ -56,17 +56,13 @@ class ZLSCellRenderer(getModel: () -> ZBModel<ZLSVersion>): ZBCellRenderer<ZLSVe
is ListElem.One -> { is ListElem.One -> {
val (icon, isSuggestion) = when(value) { val (icon, isSuggestion) = when(value) {
is ListElem.One.Suggested -> AllIcons.General.Information to true 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 this.icon = icon
val item = value.instance val item = value.instance
//TODO proper renderer val name = item.name
if (item.name != null) { val path = item.path.pathString
append(item.name) renderPathNameComponent(path, name, "ZLS", this, isSuggestion, index == -1)
append(item.path.pathString, SimpleTextAttributes.GRAYED_ATTRIBUTES)
} else {
append(item.path.pathString)
}
} }
is ListElem.Download -> { 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 lsp.zls.description=The <a href="https://github.com/Zigtools/ZLS">Zig Language Server</a>, via ZigBrains
settings.list.title=ZLS Instances settings.list.title=ZLS Instances
settings.list.empty=Select a ZLS version to view or edit its details here 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.none.text=<No ZLS>
settings.model.loading.text=Loading\u2026 settings.model.loading.text=Loading\u2026
settings.model.from-disk.text=Add ZLS from disk\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