From 25e8e048362cb40d003768ec03a92207deef34c5 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Fri, 21 Feb 2025 20:06:41 +0100 Subject: [PATCH] feat: very basic reference resolving/highlighting --- core/src/main/grammar/Zig.bnf | 19 +- .../zigbrains/zig/psi/ZigTypedElement.java | 31 + .../execution/build/ZigLineMarkerBuild.kt | 2 +- .../project/execution/run/ZigLineMarkerRun.kt | 2 +- .../zigbrains/zig/formatter/ZigBlock.kt | 2 +- .../zig/highlighter/ZigSemanticHighlighter.kt | 119 ++ .../zigbrains/zig/psi/ZigNamedElement.kt | 31 + .../zigbrains/zig/psi/ZigReferenceElement.kt | 29 + .../mixins/ZigContainerMembersMixinImpl.kt | 42 + .../impl/mixins/ZigFnDeclProtoMixinImpl.kt | 55 + .../psi/impl/mixins/ZigParamDeclMixinImpl.kt | 56 + .../mixins/ZigPrimaryTypeExprMixinImpl.kt | 113 ++ .../impl/mixins/ZigVarDeclProtoMixinImpl.kt | 82 + .../psi/mixins/ZigContainerMembersMixin.kt | 29 + .../zigbrains/zig/psi/mixins/ZigExprMixin.kt | 29 + .../zig/psi/mixins/ZigFnDeclProtoMixin.kt | 29 + .../zig/psi/mixins/ZigParamDeclMixin.kt | 29 + .../zig/psi/mixins/ZigPrimaryTypeExprMixin.kt | 30 + .../zig/psi/mixins/ZigVarDeclProtoMixin.kt | 29 + .../zigbrains/zig/references/ZigReference.kt | 129 ++ .../zigbrains/zig/references/ZigType.kt | 37 + .../renaming/ZigElementDescriptionProvider.kt | 54 + .../zig/renaming/ZigNamesValidator.kt | 48 + .../renaming/ZigRenamePsiElementProcessor.kt | 36 + .../zigbrains/zig/util/ZigElementFactory.kt | 46 + .../resources/META-INF/zigbrains-core.xml | 9 + .../build_runner/0.14.0/build_runner.zig | 1469 +++++++++++++++++ .../resources/build_runner/0.14.0/shared.zig | 241 +++ core/src/main/resources/build_runner/LICENSE | 21 + .../src/main/resources/build_runner/README.MD | 5 + 30 files changed, 2847 insertions(+), 6 deletions(-) create mode 100644 core/src/main/java/com/falsepattern/zigbrains/zig/psi/ZigTypedElement.java create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/highlighter/ZigSemanticHighlighter.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/ZigNamedElement.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/ZigReferenceElement.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigContainerMembersMixinImpl.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigFnDeclProtoMixinImpl.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigParamDeclMixinImpl.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigPrimaryTypeExprMixinImpl.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigVarDeclProtoMixinImpl.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigContainerMembersMixin.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigExprMixin.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigFnDeclProtoMixin.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigParamDeclMixin.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigPrimaryTypeExprMixin.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigVarDeclProtoMixin.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/references/ZigReference.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/references/ZigType.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigElementDescriptionProvider.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigNamesValidator.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigRenamePsiElementProcessor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/zig/util/ZigElementFactory.kt create mode 100644 core/src/main/resources/build_runner/0.14.0/build_runner.zig create mode 100644 core/src/main/resources/build_runner/0.14.0/shared.zig create mode 100644 core/src/main/resources/build_runner/LICENSE create mode 100644 core/src/main/resources/build_runner/README.MD diff --git a/core/src/main/grammar/Zig.bnf b/core/src/main/grammar/Zig.bnf index a548e060..01c80765 100644 --- a/core/src/main/grammar/Zig.bnf +++ b/core/src/main/grammar/Zig.bnf @@ -175,6 +175,17 @@ //Mixins mixin("StringLiteral")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigStringLiteralMixinImpl" implements("StringLiteral")="com.falsepattern.zigbrains.zig.psi.mixins.ZigStringLiteralMixin" + mixin("PrimaryTypeExpr")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigPrimaryTypeExprMixinImpl" + implements("PrimaryTypeExpr")="com.falsepattern.zigbrains.zig.psi.mixins.ZigPrimaryTypeExprMixin" + implements("Expr")="com.falsepattern.zigbrains.zig.psi.mixins.ZigExprMixin" + mixin("FnDeclProto")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigFnDeclProtoMixinImpl" + implements("FnDeclProto")="com.falsepattern.zigbrains.zig.psi.mixins.ZigFnDeclProtoMixin" + mixin("VarDeclProto")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigVarDeclProtoMixinImpl" + implements("VarDeclProto")="com.falsepattern.zigbrains.zig.psi.mixins.ZigVarDeclProtoMixin" + mixin("ContainerMembers")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigContainerMembersMixinImpl" + implements("ContainerMembers")="com.falsepattern.zigbrains.zig.psi.mixins.ZigContainerMembersMixin" + mixin("ParamDecl")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigParamDeclMixinImpl" + implements("ParamDecl")="com.falsepattern.zigbrains.zig.psi.mixins.ZigParamDeclMixin" } Root ::= CONTAINER_DOC_COMMENT? ContainerMembers? @@ -189,11 +200,13 @@ TestDecl ::= KEYWORD_TEST (STRING_LITERAL_SINGLE | IDENTIFIER)? Block {pin=1} ComptimeDecl ::= KEYWORD_COMPTIME Block Decl -::= (KEYWORD_EXPORT | KEYWORD_EXTERN STRING_LITERAL_SINGLE? | KEYWORD_INLINE | KEYWORD_NOINLINE)? FnProto (SEMICOLON | Block) +::= (KEYWORD_EXPORT | KEYWORD_EXTERN STRING_LITERAL_SINGLE? | KEYWORD_INLINE | KEYWORD_NOINLINE)? FnDeclProto (SEMICOLON | Block) | (KEYWORD_EXPORT | KEYWORD_EXTERN STRING_LITERAL_SINGLE?)? KEYWORD_THREADLOCAL? GlobalVarDecl | KEYWORD_USINGNAMESPACE Expr SEMICOLON -FnProto ::= KEYWORD_FN IDENTIFIER? LPAREN ParamDeclList RPAREN ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr {pin=1} +FnDeclProto ::= KEYWORD_FN IDENTIFIER LPAREN ParamDeclList RPAREN ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr {pin=1} + +FnTypeProto ::= KEYWORD_FN LPAREN ParamDeclList RPAREN ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr {pin=1} VarDeclProto ::= (KEYWORD_CONST | KEYWORD_VAR) IDENTIFIER (COLON TypeExpr)? ByteAlign? AddrSpace? LinkSection? {pin=1} @@ -337,7 +350,7 @@ PrimaryTypeExpr | DOT InitList | ErrorSetDecl | FLOAT - | FnProto + | FnTypeProto | GroupedExpr | LabeledTypeExpr | IDENTIFIER diff --git a/core/src/main/java/com/falsepattern/zigbrains/zig/psi/ZigTypedElement.java b/core/src/main/java/com/falsepattern/zigbrains/zig/psi/ZigTypedElement.java new file mode 100644 index 00000000..67b8568d --- /dev/null +++ b/core/src/main/java/com/falsepattern/zigbrains/zig/psi/ZigTypedElement.java @@ -0,0 +1,31 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi; + +import com.falsepattern.zigbrains.zig.references.ZigType; + +public interface ZigTypedElement { + default ZigType getType() { + return ZigType.Generic; + } +} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/build/ZigLineMarkerBuild.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/build/ZigLineMarkerBuild.kt index e9b2c534..10199691 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/build/ZigLineMarkerBuild.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/build/ZigLineMarkerBuild.kt @@ -38,7 +38,7 @@ class ZigLineMarkerBuild: ZigTopLevelLineMarker() { return null val parent = element.parent ?: return null - if (parent.elementType != ZigTypes.FN_PROTO) + if (parent.elementType != ZigTypes.FN_DECL_PROTO) return null val file = element.containingFile ?: return null diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/run/ZigLineMarkerRun.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/run/ZigLineMarkerRun.kt index aa2981fe..3855476a 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/run/ZigLineMarkerRun.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/run/ZigLineMarkerRun.kt @@ -38,7 +38,7 @@ class ZigLineMarkerRun: ZigTopLevelLineMarker() { return null val parent = element.parent - if (parent.elementType != ZigTypes.FN_PROTO) + if (parent.elementType != ZigTypes.FN_DECL_PROTO) return null return parent.parent diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/formatter/ZigBlock.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/formatter/ZigBlock.kt index e9ed7e4f..1a7ea74c 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/formatter/ZigBlock.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/formatter/ZigBlock.kt @@ -87,7 +87,7 @@ private fun getIndentBasedOnParentType( //Function declaration parameters if (parentType == PARAM_DECL_LIST || - parentType == FN_PROTO && childType === PLACEHOLDER + parentType == FN_DECL_PROTO && childType === PLACEHOLDER ) return normalIndent diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/highlighter/ZigSemanticHighlighter.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/highlighter/ZigSemanticHighlighter.kt new file mode 100644 index 00000000..2ae671e2 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/highlighter/ZigSemanticHighlighter.kt @@ -0,0 +1,119 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.highlighter + +import com.falsepattern.zigbrains.zig.psi.ZigNamedElement +import com.falsepattern.zigbrains.zig.psi.ZigPrimaryTypeExpr +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.lang.annotation.Annotator +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.psi.PsiElement + +class ZigSemanticHighlighter: Annotator { + override fun annotate(element: PsiElement, holder: AnnotationHolder) { + when(element) { + is ZigPrimaryTypeExpr -> { + val id = element.identifier ?: return + if (primitiveHighlight(id, holder)) { + return + } + if (referenceHighlight(element, holder)) { + return + } + } + is ZigNamedElement -> { + val id = element.identifyingElement ?: return + holder.newSilentAnnotation(HighlightSeverity.INFORMATION) + .range(id.textRange) + .textAttributes(element.declarationAttribute) + .create() + } + } + } + + private fun referenceHighlight(element: PsiElement, holder: AnnotationHolder): Boolean { + val ref = element.reference ?: return false + val resolved = ref.resolve() ?: return false + if (resolved is ZigNamedElement) { + holder.newSilentAnnotation(HighlightSeverity.INFORMATION) + .range(ref.absoluteRange) + .textAttributes(resolved.referenceAttribute) + .create() + return true + } + return false + } + + private fun primitiveHighlight(element: PsiElement, holder: AnnotationHolder): Boolean { + val name = element.text + if (name in primitives) { + holder.newSilentAnnotation(HighlightSeverity.INFORMATION) + .range(element.textRange) + .textAttributes(ZigSyntaxHighlighter.TYPE_REF) + .create() + return true + } + if (name.startsWith('i') || name.startsWith('u')) { + val numeric = name.substring(1) + val num = numeric.toIntOrNull() ?: return false + if (num < 0 || num > 65535) { + return false + } + holder.newSilentAnnotation(HighlightSeverity.INFORMATION) + .range(element.textRange) + .textAttributes(ZigSyntaxHighlighter.TYPE_REF) + .create() + return true + } + return false + } + + private val primitives = setOf( + "isize", + "usize", + "c_char", + "c_short", + "c_ushort", + "c_int", + "c_uint", + "c_long", + "c_ulong", + "c_longlong", + "c_ulonglong", + "c_longdouble", + "f16", + "f32", + "f64", + "f80", + "f128", + "bool", + "anyopaque", + "void", + "noreturn", + "type", + "anyerror", + "comptime_int", + "comptime_float", + ) + +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/ZigNamedElement.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/ZigNamedElement.kt new file mode 100644 index 00000000..bfbd9cf5 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/ZigNamedElement.kt @@ -0,0 +1,31 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi + +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.psi.PsiNameIdentifierOwner + +interface ZigNamedElement: PsiNameIdentifierOwner { + val declarationAttribute: TextAttributesKey + val referenceAttribute: TextAttributesKey +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/ZigReferenceElement.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/ZigReferenceElement.kt new file mode 100644 index 00000000..3d0f154d --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/ZigReferenceElement.kt @@ -0,0 +1,29 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi + +import com.intellij.psi.PsiElement + +interface ZigReferenceElement: PsiElement { + fun rename(name: String): ZigReferenceElement? +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigContainerMembersMixinImpl.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigContainerMembersMixinImpl.kt new file mode 100644 index 00000000..3f5cadd0 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigContainerMembersMixinImpl.kt @@ -0,0 +1,42 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi.impl.mixins + +import com.falsepattern.zigbrains.zig.psi.ZigContainerField +import com.falsepattern.zigbrains.zig.psi.ZigContainerMembers +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.util.childrenOfType +import com.intellij.util.resettableLazy + +abstract class ZigContainerMembersMixinImpl(node: ASTNode): ASTWrapperPsiElement(node), ZigContainerMembers { + private val _isNamespace = resettableLazy { + childrenOfType().isEmpty() + } + override val isNamespace by _isNamespace + + override fun subtreeChanged() { + super.subtreeChanged() + _isNamespace.reset() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigFnDeclProtoMixinImpl.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigFnDeclProtoMixinImpl.kt new file mode 100644 index 00000000..d1855eb0 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigFnDeclProtoMixinImpl.kt @@ -0,0 +1,55 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi.impl.mixins + +import com.falsepattern.zigbrains.zig.highlighter.ZigSyntaxHighlighter +import com.falsepattern.zigbrains.zig.psi.ZigFnDeclProto +import com.falsepattern.zigbrains.zig.util.ZigElementFactory +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.psi.PsiElement +import com.intellij.psi.util.startOffset + +abstract class ZigFnDeclProtoMixinImpl(node: ASTNode): ASTWrapperPsiElement(node), ZigFnDeclProto { + override fun getName(): String? { + return identifier?.text + } + + override fun setName(name: String): PsiElement? { + identifier?.replace(ZigElementFactory.createZigIdentifier(project, name) ?: return null) ?: return null + return this + } + + override fun getNameIdentifier(): PsiElement? { + return identifier + } + + override fun getTextOffset(): Int { + return identifier?.startOffset ?: 0 + } + + override val declarationAttribute get() = ZigSyntaxHighlighter.FUNCTION_DECL + + override val referenceAttribute get() = ZigSyntaxHighlighter.FUNCTION_REF +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigParamDeclMixinImpl.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigParamDeclMixinImpl.kt new file mode 100644 index 00000000..d3d43af9 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigParamDeclMixinImpl.kt @@ -0,0 +1,56 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi.impl.mixins + +import com.falsepattern.zigbrains.zig.highlighter.ZigSyntaxHighlighter +import com.falsepattern.zigbrains.zig.psi.ZigParamDecl +import com.falsepattern.zigbrains.zig.util.ZigElementFactory +import com.falsepattern.zigbrains.zig.psi.ZigVarDeclProto +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.psi.PsiElement +import com.intellij.psi.util.startOffset + +abstract class ZigParamDeclMixinImpl(node: ASTNode): ASTWrapperPsiElement(node), ZigParamDecl { + override fun getName(): String? { + return identifier?.text + } + + override fun setName(name: String): PsiElement? { + identifier?.replace(ZigElementFactory.createZigIdentifier(project, name) ?: return null) ?: return null + return this + } + + override fun getNameIdentifier(): PsiElement? { + return identifier + } + + override fun getTextOffset(): Int { + return identifier?.startOffset ?: 0 + } + + override val declarationAttribute get() = ZigSyntaxHighlighter.PARAMETER + + override val referenceAttribute get() = ZigSyntaxHighlighter.VARIABLE_REF +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigPrimaryTypeExprMixinImpl.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigPrimaryTypeExprMixinImpl.kt new file mode 100644 index 00000000..b34a24af --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigPrimaryTypeExprMixinImpl.kt @@ -0,0 +1,113 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi.impl.mixins + +import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider +import com.falsepattern.zigbrains.zig.psi.ZigFnCallArguments +import com.falsepattern.zigbrains.zig.util.ZigElementFactory +import com.falsepattern.zigbrains.zig.psi.ZigPrimaryTypeExpr +import com.falsepattern.zigbrains.zig.references.ZigReference +import com.falsepattern.zigbrains.zig.psi.ZigReferenceElement +import com.falsepattern.zigbrains.zig.psi.ZigTypes +import com.falsepattern.zigbrains.zig.references.ZigType +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.openapi.util.TextRange +import com.intellij.psi.util.elementType +import com.intellij.util.resettableLazy +import kotlinx.coroutines.runBlocking +import kotlin.io.path.exists + +abstract class ZigPrimaryTypeExprMixinImpl(node: ASTNode): ASTWrapperPsiElement(node), ZigPrimaryTypeExpr { + private val ref = resettableLazy { + identifier?.let { return@resettableLazy ZigReference(this, it.textRangeInParent) } + //TODO import resolving +// val children = children +// if (children.size == 2) run { +// val a = children[0] +// val b = children[1] +// if (a.elementType != ZigTypes.BUILTINIDENTIFIER || !a.text.equals("@import")) { +// return@run +// } +// if (b !is ZigFnCallArguments) { +// return@run +// } +// val exprList = b.exprList?.exprList ?: return@run +// if (exprList.size != 1) +// return@run +// val expr = exprList[0] as? ZigPrimaryTypeExpr ?: return@run +// val stringLiteral = expr.stringLiteral ?: return@run +// val esc = stringLiteral.createLiteralTextEscaper() +// val sb = StringBuilder() +// esc.decode(TextRange(0, stringLiteral.textLength), sb) +// val str = sb.toString() +// if (str == "std") { +// val tc = project.zigProjectSettings.state.toolchain ?: return@run +// val env = runBlocking { +// tc.zig.getEnv(project) +// } +// val stdPath = env.stdPath(tc, project) ?: return@run +// val stdFile = stdPath.resolve("std.zig") +// if (!stdFile.exists()) { +// return@run +// } +// } +// } + return@resettableLazy null + } + override fun getReference() = ref.value + + override fun subtreeChanged() { + super.subtreeChanged() + ref.reset() + } + + override fun rename(name: String): ZigReferenceElement? { + identifier?.replace(ZigElementFactory.createZigIdentifier(project, name) ?: return null) ?: return null + return this + } + + override fun getType(): ZigType { + containerDecl?.containerDeclAuto?.let { containerDecl -> + val type = containerDecl.containerDeclType + if (type.keywordStruct != null) { + if (containerDecl.containerMembers?.isNamespace == true) { + return ZigType.Namespace + } + return ZigType.Struct + } + if (type.keywordEnum != null) { + return ZigType.EnumType + } + if (type.keywordUnion != null) { + return ZigType.Union + } + } + errorSetDecl?.let { + return ZigType.ErrorType + } + return ZigType.Generic + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigVarDeclProtoMixinImpl.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigVarDeclProtoMixinImpl.kt new file mode 100644 index 00000000..16154c41 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/impl/mixins/ZigVarDeclProtoMixinImpl.kt @@ -0,0 +1,82 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi.impl.mixins + +import com.falsepattern.zigbrains.zig.highlighter.ZigSyntaxHighlighter +import com.falsepattern.zigbrains.zig.psi.ZigGlobalVarDecl +import com.falsepattern.zigbrains.zig.psi.ZigTypedElement +import com.falsepattern.zigbrains.zig.psi.ZigVarDeclExprStatement +import com.falsepattern.zigbrains.zig.util.ZigElementFactory +import com.falsepattern.zigbrains.zig.psi.ZigVarDeclProto +import com.falsepattern.zigbrains.zig.references.ZigType +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.psi.PsiElement +import com.intellij.psi.util.startOffset + +abstract class ZigVarDeclProtoMixinImpl(node: ASTNode): ASTWrapperPsiElement(node), ZigVarDeclProto { + override fun getName(): String? { + return identifier?.text + } + + override fun setName(name: String): PsiElement? { + identifier?.replace(ZigElementFactory.createZigIdentifier(project, name) ?: return null) ?: return null + return this + } + + override fun getNameIdentifier(): PsiElement? { + return identifier + } + + override fun getTextOffset(): Int { + return identifier?.startOffset ?: 0 + } + + private fun resolveType(): ZigType { + val parent = parent + when(parent) { + is ZigGlobalVarDecl -> { + parent.expr?.type?.let { return it } + } + is ZigVarDeclExprStatement -> run { + val varDeclList = parent.varDeclProtoList + if (varDeclList.size != 1) + return@run + val varDecl = varDeclList[0] + if (varDecl != this) + return@run + val exprList = parent.exprList + if (exprList.size != 1) + return@run + val expr = exprList[0] + return expr.type + } + } + return ZigType.Generic + } + + override val declarationAttribute get() = resolveType().declaration + + override val referenceAttribute get() = resolveType().reference +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigContainerMembersMixin.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigContainerMembersMixin.kt new file mode 100644 index 00000000..c6628561 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigContainerMembersMixin.kt @@ -0,0 +1,29 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi.mixins + +import com.intellij.psi.PsiElement + +interface ZigContainerMembersMixin: PsiElement { + val isNamespace: Boolean +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigExprMixin.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigExprMixin.kt new file mode 100644 index 00000000..2ca8181f --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigExprMixin.kt @@ -0,0 +1,29 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi.mixins + +import com.falsepattern.zigbrains.zig.psi.ZigTypedElement +import com.intellij.psi.PsiElement + +interface ZigExprMixin: PsiElement, ZigTypedElement { +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigFnDeclProtoMixin.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigFnDeclProtoMixin.kt new file mode 100644 index 00000000..82d48d37 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigFnDeclProtoMixin.kt @@ -0,0 +1,29 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi.mixins + +import com.falsepattern.zigbrains.zig.psi.ZigNamedElement + +interface ZigFnDeclProtoMixin: ZigNamedElement { + +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigParamDeclMixin.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigParamDeclMixin.kt new file mode 100644 index 00000000..1020e0d9 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigParamDeclMixin.kt @@ -0,0 +1,29 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi.mixins + +import com.falsepattern.zigbrains.zig.psi.ZigNamedElement + +interface ZigParamDeclMixin: ZigNamedElement { + +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigPrimaryTypeExprMixin.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigPrimaryTypeExprMixin.kt new file mode 100644 index 00000000..36ed453e --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigPrimaryTypeExprMixin.kt @@ -0,0 +1,30 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi.mixins + +import com.falsepattern.zigbrains.zig.psi.ZigReferenceElement +import com.falsepattern.zigbrains.zig.psi.ZigTypedElement + +interface ZigPrimaryTypeExprMixin: ZigReferenceElement { + +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigVarDeclProtoMixin.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigVarDeclProtoMixin.kt new file mode 100644 index 00000000..bb195ec7 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/psi/mixins/ZigVarDeclProtoMixin.kt @@ -0,0 +1,29 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.psi.mixins + +import com.falsepattern.zigbrains.zig.psi.ZigNamedElement + +interface ZigVarDeclProtoMixin: ZigNamedElement { + +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/references/ZigReference.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/references/ZigReference.kt new file mode 100644 index 00000000..a6aa950b --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/references/ZigReference.kt @@ -0,0 +1,129 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.references + +import com.falsepattern.zigbrains.zig.psi.ZigBlock +import com.falsepattern.zigbrains.zig.psi.ZigContainerDeclaration +import com.falsepattern.zigbrains.zig.psi.ZigContainerMembers +import com.falsepattern.zigbrains.zig.psi.ZigDecl +import com.falsepattern.zigbrains.zig.psi.ZigFnDeclProto +import com.falsepattern.zigbrains.zig.psi.ZigParamDecl +import com.falsepattern.zigbrains.zig.psi.ZigReferenceElement +import com.falsepattern.zigbrains.zig.psi.ZigStatement +import com.falsepattern.zigbrains.zig.psi.ZigVarDeclProto +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementResolveResult +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.PsiPolyVariantReferenceBase +import com.intellij.psi.ResolveResult +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.intellij.psi.util.childrenOfType + +class ZigReference(element: T, textRange: TextRange): PsiPolyVariantReferenceBase(element, textRange) { + override fun multiResolve(incompleteCode: Boolean): Array { + element.putUserData(NAME_RANGE_KEY, rangeInElement) + return resolveOrEmpty(element) + } + + companion object { + private val NAME_RANGE_KEY = Key.create("ZIG_REF_NAME_RANGE") + private fun resolveOrEmpty(element: PsiElement): Array { + return CachedValuesManager.getCachedValue(element) { + val range = element.getUserData(NAME_RANGE_KEY) + if (range == null) + return@getCachedValue null + val name = element.text.substring(range.startOffset, range.endOffset) + resolveCached(element, name) + } ?: emptyArray() + } + private fun resolveCached(element: PsiElement, name: String): CachedValueProvider.Result>? { + val results = ArrayList() + val dependencies = ArrayList() + var prevContainer: PsiElement = element + var container: PsiElement? = prevContainer.parent + while (container != null) { + when (container) { + is ZigBlock -> { + var stmt: PsiElement? = prevContainer + while (stmt != null) { + (stmt as? ZigStatement)?.varDeclExprStatement?.varDeclProtoList?.forEach { varDeclProto -> + if (validate(varDeclProto, name)) { + results.add(varDeclProto) + dependencies.add(varDeclProto) + } + } + stmt = stmt.prevSibling + } + } + + is ZigDecl -> { + container.fnDeclProto?.paramDeclList?.paramDeclList?.forEach { paramDecl -> + if (validate(paramDecl, name)) { + results.add(paramDecl) + dependencies.add(paramDecl) + return@forEach + } + } + } + + is ZigContainerMembers -> { + container.childrenOfType().forEach { + val decl = it.decl ?: return@forEach + decl.fnDeclProto?.let { fnProto -> + if (validate(fnProto, name)) { + results.add(fnProto) + dependencies.add(fnProto) + return@forEach + } + } + decl.globalVarDecl?.varDeclProto?.let { varDeclProto -> + if (validate(varDeclProto, name)) { + results.add(varDeclProto) + dependencies.add(varDeclProto) + return@forEach + } + } + } + } + } + prevContainer = container + container = container.parent + } + if (results.isEmpty()) + return null + val res = results.map { PsiElementResolveResult(it) }.toTypedArray() + return CachedValueProvider.Result(res, *dependencies.toArray()) + } + private fun validate(paramDecl: ZigParamDecl, name: String) = paramDecl.identifier?.text?.equals(name) == true + private fun validate(fnProto: ZigFnDeclProto, name: String) = fnProto.identifier?.text?.equals(name) == true + private fun validate(varDeclProto: ZigVarDeclProto, name: String) = varDeclProto.identifier?.text?.equals(name) == true + } + + + override fun handleElementRename(newElementName: String): PsiElement? { + return element.rename(newElementName) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/references/ZigType.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/references/ZigType.kt new file mode 100644 index 00000000..94bfcc20 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/references/ZigType.kt @@ -0,0 +1,37 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.references + +import com.falsepattern.zigbrains.zig.highlighter.ZigSyntaxHighlighter +import com.intellij.openapi.editor.colors.TextAttributesKey + +enum class ZigType(val declaration: TextAttributesKey, val reference: TextAttributesKey) { + Namespace(ZigSyntaxHighlighter.NAMESPACE_DECL, ZigSyntaxHighlighter.NAMESPACE_REF), + ErrorType(ZigSyntaxHighlighter.TYPE_DECL, ZigSyntaxHighlighter.TYPE_REF), + ErrorTag(ZigSyntaxHighlighter.ERROR_TAG_DECL, ZigSyntaxHighlighter.ERROR_TAG_REF), + EnumType(ZigSyntaxHighlighter.ENUM_DECL, ZigSyntaxHighlighter.ENUM_REF), + EnumMember(ZigSyntaxHighlighter.ENUM_MEMBER_DECL, ZigSyntaxHighlighter.ENUM_MEMBER_REF), + Struct(ZigSyntaxHighlighter.STRUCT_DECL, ZigSyntaxHighlighter.STRUCT_REF), + Union(ZigSyntaxHighlighter.STRUCT_DECL, ZigSyntaxHighlighter.STRUCT_REF), + Generic(ZigSyntaxHighlighter.VARIABLE_DECL, ZigSyntaxHighlighter.VARIABLE_REF), +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigElementDescriptionProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigElementDescriptionProvider.kt new file mode 100644 index 00000000..b4cdd944 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigElementDescriptionProvider.kt @@ -0,0 +1,54 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.renaming + +import com.falsepattern.zigbrains.zig.psi.ZigFnDeclProto +import com.falsepattern.zigbrains.zig.psi.ZigGlobalVarDecl +import com.falsepattern.zigbrains.zig.psi.ZigVarDeclExprStatement +import com.falsepattern.zigbrains.zig.psi.ZigVarDeclProto +import com.intellij.psi.ElementDescriptionLocation +import com.intellij.psi.ElementDescriptionProvider +import com.intellij.psi.PsiElement +import com.intellij.usageView.UsageViewTypeLocation + +class ZigElementDescriptionProvider: ElementDescriptionProvider { + override fun getElementDescription( + element: PsiElement, + location: ElementDescriptionLocation + ): String? { + if (location != UsageViewTypeLocation.INSTANCE) + return null + return when(element) { + is ZigVarDeclProto -> { + val type = if (element.keywordConst != null) "constant" else "variable" + when(element.parent) { + is ZigGlobalVarDecl -> "global $type" + is ZigVarDeclExprStatement -> "local $type" + else -> type + } + } + is ZigFnDeclProto -> "function" + else -> null + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigNamesValidator.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigNamesValidator.kt new file mode 100644 index 00000000..e60cc579 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigNamesValidator.kt @@ -0,0 +1,48 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.renaming + +import com.falsepattern.zigbrains.zig.lexer.ZigLexerAdapter +import com.falsepattern.zigbrains.zig.psi.ZigTypes +import com.intellij.lang.refactoring.NamesValidator +import com.intellij.openapi.project.Project +import com.intellij.psi.tree.IElementType + +class ZigNamesValidator: NamesValidator { + override fun isKeyword(name: String, project: Project?) = matches(name) { token -> + keywords.contains(token) + } + + override fun isIdentifier(name: String, project: Project?) = matches(name) { token -> + token == ZigTypes.IDENTIFIER + } + + private inline fun matches(name: String, matcher: (IElementType) -> Boolean): Boolean { + val lexer = ZigLexerAdapter() + lexer.start(name) + val token = lexer.tokenType + return token != null && lexer.tokenEnd == lexer.bufferEnd && matcher(token) + } + + val keywords = ZigTypes::class.java.declaredFields.mapNotNullTo(HashSet()) { if (it.name.startsWith("KEYWORD_")) it.get(null) as IElementType else null } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigRenamePsiElementProcessor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigRenamePsiElementProcessor.kt new file mode 100644 index 00000000..461493e5 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/renaming/ZigRenamePsiElementProcessor.kt @@ -0,0 +1,36 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.renaming + +import com.falsepattern.zigbrains.zig.psi.ZigFnDeclProto +import com.falsepattern.zigbrains.zig.psi.ZigVarDeclProto +import com.intellij.psi.PsiElement +import com.intellij.refactoring.rename.RenamePsiElementProcessorBase + +class ZigRenamePsiElementProcessor: RenamePsiElementProcessorBase() { + override fun canProcessElement(element: PsiElement) = when(element) { + is ZigVarDeclProto -> true + is ZigFnDeclProto -> true + else -> false + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/zig/util/ZigElementFactory.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/util/ZigElementFactory.kt new file mode 100644 index 00000000..82f8eae8 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/zig/util/ZigElementFactory.kt @@ -0,0 +1,46 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.zig.util + +import com.falsepattern.zigbrains.zig.ZigFileType +import com.falsepattern.zigbrains.zig.psi.ZigTypes +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.impl.source.tree.LeafElement + +object ZigElementFactory { + private val LOG = Logger.getInstance(ZigElementFactory::class.java) + fun createZigFile(project: Project, text: CharSequence): PsiFile { + return PsiFileFactory.getInstance(project).createFileFromText("a.zig", ZigFileType, text) + } + + fun createZigIdentifier(project: Project, name: String): PsiElement? { + val file = createZigFile(project, "const $name = undefined;") + val identifier = file.findElementAt("const ".length) ?: return null + LOG.assertTrue(identifier is LeafElement && identifier.elementType == ZigTypes.IDENTIFIER, name) + return identifier + } +} \ No newline at end of file diff --git a/core/src/main/resources/META-INF/zigbrains-core.xml b/core/src/main/resources/META-INF/zigbrains-core.xml index 0a005035..387f35d0 100644 --- a/core/src/main/resources/META-INF/zigbrains-core.xml +++ b/core/src/main/resources/META-INF/zigbrains-core.xml @@ -2,6 +2,7 @@ zigbrains.Bundle + + + + ww.trigger(), + else => process.exit(1), + } + } else |err| switch (err) { + error.EndOfStream => process.exit(0), + else => process.exit(1), + } + } + }.do, .{&w}); + message_thread.detach(); + + const gpa = arena; + var transport = Transport.init(.{ + .gpa = gpa, + .in = std.io.getStdIn(), + .out = std.io.getStdOut(), + }); + defer transport.deinit(); + + run.transport = &transport; + + var step_stack = try stepNamesToStepStack(gpa, builder, targets.items, check_step_only); + if (step_stack.count() == 0) { + // This means that `enable_build_on_save == null` and the project contains no "check" step. + return; + } + + prepare(gpa, builder, &step_stack, &run, seed) catch |err| switch (err) { + error.UncleanExit => process.exit(1), + else => return err, + }; + + rebuild: while (true) : (run.cycle += 1) { + runSteps( + builder, + &step_stack, + main_progress_node, + &run, + ) catch |err| switch (err) { + error.UncleanExit => process.exit(1), + else => return err, + }; + + try w.update(gpa, step_stack.keys()); + + // Wait until a file system notification arrives. Read all such events + // until the buffer is empty. Then wait for a debounce interval, resetting + // if any more events come in. After the debounce interval has passed, + // trigger a rebuild on all steps with modified inputs, as well as their + // recursive dependants. + var debounce_timeout: std.Build.Watch.Timeout = .none; + while (true) switch (try w.wait(gpa, debounce_timeout)) { + .timeout => { + markFailedStepsDirty(gpa, step_stack.keys()); + continue :rebuild; + }, + .dirty => if (debounce_timeout == .none) { + debounce_timeout = .{ .ms = debounce_interval_ms }; + }, + .clean => {}, + }; + } +} + +fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void { + for (all_steps) |step| switch (step.state) { + .dependency_failure, .failure, .skipped => step.recursiveReset(gpa), + else => continue, + }; + // Now that all dirty steps have been found, the remaining steps that + // succeeded from last run shall be marked "cached". + for (all_steps) |step| switch (step.state) { + .success => step.result_cached = true, + else => continue, + }; +} + +/// A wrapper around `std.Build.Watch` that supports manually triggering recompilations. +const Watch = struct { + fs_watch: std.Build.Watch, + supports_fs_watch: bool, + manual_event: std.Thread.ResetEvent, + steps: []const *Step, + + fn init() !Watch { + return .{ + .fs_watch = if (@TypeOf(std.Build.Watch) != void) try std.Build.Watch.init() else {}, + .supports_fs_watch = @TypeOf(std.Build.Watch) != void and shared.BuildOnSaveSupport.isSupportedRuntime(builtin.zig_version) == .supported, + .manual_event = .{}, + .steps = &.{}, + }; + } + + fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void { + if (@TypeOf(std.Build.Watch) != void and w.supports_fs_watch) { + return try w.fs_watch.update(gpa, steps); + } + w.steps = steps; + } + + fn trigger(w: *Watch) void { + if (w.supports_fs_watch) { + @panic("received manualy filesystem event even though std.Build.Watch is supported"); + } + w.manual_event.set(); + } + + fn wait(w: *Watch, gpa: Allocator, timeout: std.Build.Watch.Timeout) !std.Build.Watch.WaitResult { + if (@TypeOf(std.Build.Watch) != void and w.supports_fs_watch) { + return try w.fs_watch.wait(gpa, timeout); + } + switch (timeout) { + .none => w.manual_event.wait(), + .ms => |ms| w.manual_event.timedWait(@as(u64, ms) * std.time.ns_per_ms) catch return .timeout, + } + w.manual_event.reset(); + markStepsDirty(gpa, w.steps); + return .dirty; + } + + fn markStepsDirty(gpa: Allocator, all_steps: []const *Step) void { + for (all_steps) |step| switch (step.state) { + .precheck_done => continue, + else => step.recursiveReset(gpa), + }; + } +}; + +const Run = struct { + max_rss: u64, + max_rss_is_default: bool, + max_rss_mutex: std.Thread.Mutex, + skip_oom_steps: bool, + memory_blocked_steps: std.ArrayList(*Step), + thread_pool: std.Thread.Pool, + + claimed_rss: usize, + + transport: ?*Transport, + cycle: u32, +}; + +fn stepNamesToStepStack( + gpa: Allocator, + b: *std.Build, + step_names: []const []const u8, + check_step_only: bool, +) !std.AutoArrayHashMapUnmanaged(*Step, void) { + var step_stack: std.AutoArrayHashMapUnmanaged(*Step, void) = .{}; + errdefer step_stack.deinit(gpa); + + if (step_names.len == 0) { + if (b.top_level_steps.get("check")) |tls| { + try step_stack.put(gpa, &tls.step, {}); + } else if (!check_step_only) { + try step_stack.put(gpa, b.default_step, {}); + } + } else { + try step_stack.ensureUnusedCapacity(gpa, step_names.len); + for (0..step_names.len) |i| { + const step_name = step_names[step_names.len - i - 1]; + const s = b.top_level_steps.get(step_name) orelse { + std.debug.print("no step named '{s}'\n access the help menu with 'zig build -h'\n", .{step_name}); + process.exit(1); + }; + step_stack.putAssumeCapacity(&s.step, {}); + } + } + + return step_stack; +} + +fn prepare( + gpa: Allocator, + b: *std.Build, + step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void), + run: *Run, + seed: u32, +) error{ OutOfMemory, UncleanExit }!void { + const starting_steps = try gpa.dupe(*Step, step_stack.keys()); + defer gpa.free(starting_steps); + + var rng = std.Random.DefaultPrng.init(seed); + const rand = rng.random(); + rand.shuffle(*Step, starting_steps); + + for (starting_steps) |s| { + constructGraphAndCheckForDependencyLoop(b, s, step_stack, rand) catch |err| switch (err) { + error.DependencyLoopDetected => return uncleanExit(), + else => |e| return e, + }; + } + + { + // Check that we have enough memory to complete the build. + var any_problems = false; + for (step_stack.keys()) |s| { + if (s.max_rss == 0) continue; + if (s.max_rss > run.max_rss) { + if (run.skip_oom_steps) { + s.state = .skipped_oom; + } else { + std.debug.print("{s}{s}: this step declares an upper bound of {d} bytes of memory, exceeding the available {d} bytes of memory\n", .{ + s.owner.dep_prefix, s.name, s.max_rss, run.max_rss, + }); + any_problems = true; + } + } + } + if (any_problems) { + if (run.max_rss_is_default) { + std.debug.print("note: use --maxrss to override the default", .{}); + } + return uncleanExit(); + } + } +} + +fn runSteps( + b: *std.Build, + steps_stack: *const std.AutoArrayHashMapUnmanaged(*Step, void), + parent_prog_node: std.Progress.Node, + run: *Run, +) error{ OutOfMemory, UncleanExit }!void { + const thread_pool = &run.thread_pool; + const steps = steps_stack.keys(); + + var step_prog = parent_prog_node.start("steps", steps.len); + defer step_prog.end(); + + var wait_group: std.Thread.WaitGroup = .{}; + defer wait_group.wait(); + + // Here we spawn the initial set of tasks with a nice heuristic - + // dependency order. Each worker when it finishes a step will then + // check whether it should run any dependants. + for (steps) |step| { + if (step.state == .skipped_oom) continue; + + wait_group.start(); + thread_pool.spawn(workerMakeOneStep, .{ + &wait_group, b, steps_stack, step, step_prog, run, + }) catch @panic("OOM"); + } + + if (run.transport) |transport| { + for (steps) |step| { + const step_id: u32 = @intCast(steps_stack.getIndex(step).?); + // missing fields: + // - result_error_msgs + // - result_stderr + serveWatchErrorBundle(transport, step_id, run.cycle, step.result_error_bundle) catch @panic("failed to send watch errors"); + } + } +} + +/// Traverse the dependency graph depth-first and make it undirected by having +/// steps know their dependants (they only know dependencies at start). +/// Along the way, check that there is no dependency loop, and record the steps +/// in traversal order in `step_stack`. +/// Each step has its dependencies traversed in random order, this accomplishes +/// two things: +/// - `step_stack` will be in randomized-depth-first order, so the build runner +/// spawns steps in a random (but optimized) order +/// - each step's `dependants` list is also filled in a random order, so that +/// when it finishes executing in `workerMakeOneStep`, it spawns next steps +/// to run in random order +fn constructGraphAndCheckForDependencyLoop( + b: *std.Build, + s: *Step, + step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void), + rand: std.Random, +) error{ OutOfMemory, DependencyLoopDetected }!void { + switch (s.state) { + .precheck_started => return error.DependencyLoopDetected, + .precheck_unstarted => { + s.state = .precheck_started; + + try step_stack.ensureUnusedCapacity(b.allocator, s.dependencies.items.len); + + // We dupe to avoid shuffling the steps in the summary, it depends + // on s.dependencies' order. + const deps = b.allocator.dupe(*Step, s.dependencies.items) catch @panic("OOM"); + rand.shuffle(*Step, deps); + + for (deps) |dep| { + try step_stack.put(b.allocator, dep, {}); + try dep.dependants.append(b.allocator, s); + try constructGraphAndCheckForDependencyLoop(b, dep, step_stack, rand); + } + + s.state = .precheck_done; + }, + .precheck_done => {}, + + // These don't happen until we actually run the step graph. + .dependency_failure, + .running, + .success, + .failure, + .skipped, + .skipped_oom, + => {}, + } +} + +fn workerMakeOneStep( + wg: *std.Thread.WaitGroup, + b: *std.Build, + steps_stack: *const std.AutoArrayHashMapUnmanaged(*Step, void), + s: *Step, + prog_node: std.Progress.Node, + run: *Run, +) void { + defer wg.finish(); + const thread_pool = &run.thread_pool; + + // First, check the conditions for running this step. If they are not met, + // then we return without doing the step, relying on another worker to + // queue this step up again when dependencies are met. + for (s.dependencies.items) |dep| { + switch (@atomicLoad(Step.State, &dep.state, .seq_cst)) { + .success, .skipped => continue, + .failure, .dependency_failure, .skipped_oom => { + @atomicStore(Step.State, &s.state, .dependency_failure, .seq_cst); + return; + }, + .precheck_done, .running => { + // dependency is not finished yet. + return; + }, + .precheck_unstarted => unreachable, + .precheck_started => unreachable, + } + } + + if (s.max_rss != 0) { + run.max_rss_mutex.lock(); + defer run.max_rss_mutex.unlock(); + + // Avoid running steps twice. + if (s.state != .precheck_done) { + // Another worker got the job. + return; + } + + const new_claimed_rss = run.claimed_rss + s.max_rss; + if (new_claimed_rss > run.max_rss) { + // Running this step right now could possibly exceed the allotted RSS. + // Add this step to the queue of memory-blocked steps. + run.memory_blocked_steps.append(s) catch @panic("OOM"); + return; + } + + run.claimed_rss = new_claimed_rss; + s.state = .running; + } else { + // Avoid running steps twice. + if (@cmpxchgStrong(Step.State, &s.state, .precheck_done, .running, .seq_cst, .seq_cst) != null) { + // Another worker got the job. + return; + } + } + + var sub_prog_node = prog_node.start(s.name, 0); + defer sub_prog_node.end(); + + const make_result = s.make(.{ + .progress_node = sub_prog_node, + .thread_pool = thread_pool, + .watch = true, + }); + + if (run.transport) |transport| { + const step_id: u32 = @intCast(steps_stack.getIndex(s).?); + // missing fields: + // - result_error_msgs + // - result_stderr + serveWatchErrorBundle(transport, step_id, run.cycle, s.result_error_bundle) catch @panic("failed to send watch errors"); + } + + handle_result: { + if (make_result) |_| { + @atomicStore(Step.State, &s.state, .success, .seq_cst); + } else |err| switch (err) { + error.MakeFailed => { + @atomicStore(Step.State, &s.state, .failure, .seq_cst); + break :handle_result; + }, + error.MakeSkipped => @atomicStore(Step.State, &s.state, .skipped, .seq_cst), + } + + // Successful completion of a step, so we queue up its dependants as well. + for (s.dependants.items) |dep| { + wg.start(); + thread_pool.spawn(workerMakeOneStep, .{ + wg, b, steps_stack, dep, prog_node, run, + }) catch @panic("OOM"); + } + } + + // If this is a step that claims resources, we must now queue up other + // steps that are waiting for resources. + if (s.max_rss != 0) { + run.max_rss_mutex.lock(); + defer run.max_rss_mutex.unlock(); + + // Give the memory back to the scheduler. + run.claimed_rss -= s.max_rss; + // Avoid kicking off too many tasks that we already know will not have + // enough resources. + var remaining = run.max_rss - run.claimed_rss; + var i: usize = 0; + var j: usize = 0; + while (j < run.memory_blocked_steps.items.len) : (j += 1) { + const dep = run.memory_blocked_steps.items[j]; + assert(dep.max_rss != 0); + if (dep.max_rss <= remaining) { + remaining -= dep.max_rss; + + wg.start(); + thread_pool.spawn(workerMakeOneStep, .{ + wg, b, steps_stack, dep, prog_node, run, + }) catch @panic("OOM"); + } else { + run.memory_blocked_steps.items[i] = dep; + i += 1; + } + } + run.memory_blocked_steps.shrinkRetainingCapacity(i); + } +} + +fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 { + if (idx.* >= args.len) return null; + defer idx.* += 1; + return args[idx.*]; +} + +fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 { + return nextArg(args, idx) orelse { + std.debug.print("expected argument after '{s}'\n access the help menu with 'zig build -h'\n", .{args[idx.* - 1]}); + process.exit(1); + }; +} + +fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 { + if (idx >= args.len) return null; + return args[idx..]; +} + +/// Perhaps in the future there could be an Advanced Options flag such as +/// --debug-build-runner-leaks which would make this function return instead of +/// calling exit. +fn cleanExit() void { + std.debug.lockStdErr(); + process.exit(0); +} + +/// Perhaps in the future there could be an Advanced Options flag such as +/// --debug-build-runner-leaks which would make this function return instead of +/// calling exit. +fn uncleanExit() error{UncleanExit} { + std.debug.lockStdErr(); + process.exit(1); +} + +fn fatal(comptime f: []const u8, args: anytype) noreturn { + std.debug.print(f ++ "\n", args); + process.exit(1); +} + +fn validateSystemLibraryOptions(b: *std.Build) void { + var bad = false; + for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| { + switch (v) { + .user_disabled, .user_enabled => { + // The user tried to enable or disable a system library integration, but + // the build script did not recognize that option. + std.debug.print("system library name not recognized by build script: '{s}'\n", .{k}); + bad = true; + }, + .declared_disabled, .declared_enabled => {}, + } + } + if (bad) { + std.debug.print(" access the help menu with 'zig build -h'\n", .{}); + process.exit(1); + } +} + +/// Starting from all top-level steps in `b`, traverses the entire step graph +/// and adds all step dependencies implied by module graphs. +fn createModuleDependencies(b: *std.Build) Allocator.Error!void { + const arena = b.graph.arena; + + var all_steps: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty; + var next_step_idx: usize = 0; + + try all_steps.ensureUnusedCapacity(arena, b.top_level_steps.count()); + for (b.top_level_steps.values()) |tls| { + all_steps.putAssumeCapacityNoClobber(&tls.step, {}); + } + + while (next_step_idx < all_steps.count()) { + const step = all_steps.keys()[next_step_idx]; + next_step_idx += 1; + + // Set up any implied dependencies for this step. It's important that we do this first, so + // that the loop below discovers steps implied by the module graph. + try createModuleDependenciesForStep(step); + + try all_steps.ensureUnusedCapacity(arena, step.dependencies.items.len); + for (step.dependencies.items) |other_step| { + all_steps.putAssumeCapacity(other_step, {}); + } + } +} + +/// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which +/// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`. +fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void { + const root_module = if (step.cast(Step.Compile)) |cs| root: { + break :root cs.root_module; + } else return; // not a compile step so no module dependencies + + // Starting from `root_module`, discover all modules in this graph. + const modules = root_module.getGraph().modules; + + // For each of those modules, set up the implied step dependencies. + for (modules) |mod| { + if (mod.root_source_file) |lp| lp.addStepDependencies(step); + for (mod.include_dirs.items) |include_dir| switch (include_dir) { + .path, + .path_system, + .path_after, + .framework_path, + .framework_path_system, + => |lp| lp.addStepDependencies(step), + + .other_step => |other| { + other.getEmittedIncludeTree().addStepDependencies(step); + step.dependOn(&other.step); + }, + + .config_header_step => |other| step.dependOn(&other.step), + }; + for (mod.lib_paths.items) |lp| lp.addStepDependencies(step); + for (mod.rpaths.items) |rpath| switch (rpath) { + .lazy_path => |lp| lp.addStepDependencies(step), + .special => {}, + }; + for (mod.link_objects.items) |link_object| switch (link_object) { + .static_path, + .assembly_file, + => |lp| lp.addStepDependencies(step), + .other_step => |other| step.dependOn(&other.step), + .system_lib => {}, + .c_source_file => |source| source.file.addStepDependencies(step), + .c_source_files => |source_files| source_files.root.addStepDependencies(step), + .win32_resource_file => |rc_source| { + rc_source.file.addStepDependencies(step); + for (rc_source.include_paths) |lp| lp.addStepDependencies(step); + }, + }; + } +} + +// +// +// ZLS code +// +// + +const shared = @import("shared.zig"); +const Transport = shared.Transport; +const BuildConfig = shared.BuildConfig; + +const Packages = struct { + allocator: std.mem.Allocator, + + /// Outer key is the package name, inner key is the file path. + packages: std.StringArrayHashMapUnmanaged(std.StringArrayHashMapUnmanaged(void)) = .{}, + + /// Returns true if the package was already present. + pub fn addPackage(self: *Packages, name: []const u8, path: []const u8) !bool { + const name_gop_result = try self.packages.getOrPutValue(self.allocator, name, .{}); + const path_gop_result = try name_gop_result.value_ptr.getOrPut(self.allocator, path); + return path_gop_result.found_existing; + } + + pub fn toPackageList(self: *Packages) ![]BuildConfig.Package { + var result: std.ArrayListUnmanaged(BuildConfig.Package) = .{}; + errdefer result.deinit(self.allocator); + + const Context = struct { + keys: [][]const u8, + + pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { + return std.mem.lessThan(u8, ctx.keys[a_index], ctx.keys[b_index]); + } + }; + + self.packages.sort(Context{ .keys = self.packages.keys() }); + + for (self.packages.keys(), self.packages.values()) |name, path_hashmap| { + for (path_hashmap.keys()) |path| { + try result.append(self.allocator, .{ .name = name, .path = path }); + } + } + + return try result.toOwnedSlice(self.allocator); + } + + pub fn deinit(self: *Packages) void { + for (self.packages.values()) |*path_hashmap| { + path_hashmap.deinit(self.allocator); + } + self.packages.deinit(self.allocator); + } +}; + +fn extractBuildInformation( + gpa: Allocator, + b: *std.Build, + arena: Allocator, + main_progress_node: std.Progress.Node, + run: *Run, + seed: u32, +) !void { + var steps = std.AutoArrayHashMapUnmanaged(*Step, void){}; + defer steps.deinit(gpa); + + // collect the set of all steps + { + var stack: std.ArrayListUnmanaged(*Step) = .{}; + defer stack.deinit(gpa); + + try stack.ensureUnusedCapacity(gpa, b.top_level_steps.count()); + for (b.top_level_steps.values()) |tls| { + stack.appendAssumeCapacity(&tls.step); + } + + while (switch (comptime builtin.zig_version.order(replace_pop_or_null_version)) { + .lt => stack.popOrNull(), + .eq, .gt => stack.pop(), + }) |step| { + const gop = try steps.getOrPut(gpa, step); + if (gop.found_existing) continue; + + try stack.appendSlice(gpa, step.dependencies.items); + } + } + + const helper = struct { + fn addStepDependencies(allocator: Allocator, set: *std.AutoArrayHashMapUnmanaged(*Step, void), lazy_path: std.Build.LazyPath) !void { + switch (lazy_path) { + .src_path, .cwd_relative, .dependency => {}, + .generated => |gen| try set.put(allocator, gen.file.step, {}), + } + } + fn addModuleDependencies(allocator: Allocator, set: *std.AutoArrayHashMapUnmanaged(*Step, void), module: *std.Build.Module) !void { + if (module.root_source_file) |root_source_file| { + try addStepDependencies(allocator, set, root_source_file); + } + + for (module.import_table.values()) |import| { + if (import.root_source_file) |root_source_file| { + try addStepDependencies(allocator, set, root_source_file); + } + } + + for (module.include_dirs.items) |include_dir| { + switch (include_dir) { + .path, + .path_system, + .path_after, + .framework_path, + .framework_path_system, + => |include_path| try addStepDependencies(allocator, set, include_path), + .config_header_step => |config_header| try set.put(allocator, config_header.output_file.step, {}), + .other_step => |other| { + if (other.generated_h) |header| { + try set.put(allocator, header.step, {}); + } + if (other.installed_headers_include_tree) |include_tree| { + try set.put(allocator, include_tree.generated_directory.step, {}); + } + }, + } + } + } + + fn processItem( + allocator: Allocator, + module: *std.Build.Module, + compile: ?*std.Build.Step.Compile, + name: []const u8, + packages: *Packages, + include_dirs: *std.StringArrayHashMapUnmanaged(void), + c_macros: *std.StringArrayHashMapUnmanaged(void), + ) !void { + if (module.root_source_file) |root_source_file| { + _ = try packages.addPackage(name, root_source_file.getPath(module.owner)); + } + + if (compile) |exe| { + try processPkgConfig(allocator, include_dirs, c_macros, exe); + } + + try c_macros.ensureUnusedCapacity(allocator, module.c_macros.items.len); + for (module.c_macros.items) |c_macro| { + c_macros.putAssumeCapacity(c_macro, {}); + } + + for (module.include_dirs.items) |include_dir| { + switch (include_dir) { + .path, + .path_system, + .path_after, + .framework_path, + .framework_path_system, + => |include_path| try include_dirs.put(allocator, include_path.getPath(module.owner), {}), + + .other_step => |other| { + if (other.generated_h) |header| { + try include_dirs.put( + allocator, + std.fs.path.dirname(header.getPath()).?, + {}, + ); + } + if (other.installed_headers_include_tree) |include_tree| { + try include_dirs.put( + allocator, + include_tree.generated_directory.getPath(), + {}, + ); + } + }, + .config_header_step => |config_header| { + const full_file_path = config_header.output_file.getPath(); + const header_dir_path = full_file_path[0 .. full_file_path.len - config_header.include_path.len]; + try include_dirs.put( + allocator, + header_dir_path, + {}, + ); + }, + } + } + } + }; + + var step_dependencies: std.AutoArrayHashMapUnmanaged(*Step, void) = .{}; + defer step_dependencies.deinit(gpa); + + // collect step dependencies + { + var modules: std.AutoArrayHashMapUnmanaged(*std.Build.Module, void) = .{}; + defer modules.deinit(gpa); + + // collect root modules of `Step.Compile` + for (steps.keys()) |step| { + const compile = step.cast(Step.Compile) orelse continue; + const graph = compile.root_module.getGraph(); + try modules.ensureUnusedCapacity(gpa, graph.modules.len); + for (graph.modules) |module| modules.putAssumeCapacity(module, {}); + } + + // collect public modules + for (b.modules.values()) |root_module| { + const graph = root_module.getGraph(); + try modules.ensureUnusedCapacity(gpa, graph.modules.len); + for (graph.modules) |module| modules.putAssumeCapacity(module, {}); + } + + // collect all dependencies of all found modules + for (modules.keys()) |module| { + try helper.addModuleDependencies(gpa, &step_dependencies, module); + } + } + + prepare(gpa, b, &step_dependencies, run, seed) catch |err| switch (err) { + error.UncleanExit => process.exit(1), + else => return err, + }; + + // run all steps that are dependencies + try runSteps( + b, + &step_dependencies, + main_progress_node, + run, + ); + + var include_dirs: std.StringArrayHashMapUnmanaged(void) = .{}; + defer include_dirs.deinit(gpa); + + var c_macros: std.StringArrayHashMapUnmanaged(void) = .{}; + defer c_macros.deinit(gpa); + + var packages: Packages = .{ .allocator = gpa }; + defer packages.deinit(); + + // extract packages and include paths + { + for (steps.keys()) |step| { + const compile = step.cast(Step.Compile) orelse continue; + const graph = compile.root_module.getGraph(); + try helper.processItem(gpa, compile.root_module, compile, "root", &packages, &include_dirs, &c_macros); + for (graph.modules) |module| { + for (module.import_table.keys(), module.import_table.values()) |name, import| { + try helper.processItem(gpa, import, null, name, &packages, &include_dirs, &c_macros); + } + } + } + + for (b.modules.values()) |root_module| { + const graph = root_module.getGraph(); + try helper.processItem(gpa, root_module, null, "root", &packages, &include_dirs, &c_macros); + for (graph.modules) |module| { + for (module.import_table.keys(), module.import_table.values()) |name, import| { + try helper.processItem(gpa, import, null, name, &packages, &include_dirs, &c_macros); + } + } + } + } + + // Sample `@dependencies` structure: + // pub const packages = struct { + // pub const @"1220363c7e27b2d3f39de6ff6e90f9537a0634199860fea237a55ddb1e1717f5d6a5" = struct { + // pub const build_root = "/home/rad/.cache/zig/p/1220363c7e27b2d3f39de6ff6e90f9537a0634199860fea237a55ddb1e1717f5d6a5"; + // pub const build_zig = @import("1220363c7e27b2d3f39de6ff6e90f9537a0634199860fea237a55ddb1e1717f5d6a5"); + // pub const deps: []const struct { []const u8, []const u8 } = &.{}; + // }; + // ... + // }; + // pub const root_deps: []const struct { []const u8, []const u8 } = &.{ + // .{ "known_folders", "1220bb12c9bfe291eed1afe6a2070c7c39918ab1979f24a281bba39dfb23f5bcd544" }, + // .{ "diffz", "122089a8247a693cad53beb161bde6c30f71376cd4298798d45b32740c3581405864" }, + // }; + + var deps_build_roots: std.ArrayListUnmanaged(BuildConfig.DepsBuildRoots) = .{}; + for (dependencies.root_deps) |root_dep| { + inline for (comptime std.meta.declarations(dependencies.packages)) |package| blk: { + if (std.mem.eql(u8, package.name, root_dep[1])) { + const package_info = @field(dependencies.packages, package.name); + if (!@hasDecl(package_info, "build_root")) break :blk; + if (!@hasDecl(package_info, "build_zig")) break :blk; + try deps_build_roots.append(arena, .{ + .name = root_dep[0], + .path = try std.fs.path.join(arena, &.{ package_info.build_root, "build.zig" }), + }); + } + } + } + + var available_options: std.json.ArrayHashMap(BuildConfig.AvailableOption) = .{}; + try available_options.map.ensureTotalCapacity(arena, b.available_options_map.count()); + + var it = b.available_options_map.iterator(); + while (it.next()) |available_option| { + available_options.map.putAssumeCapacityNoClobber(available_option.key_ptr.*, available_option.value_ptr.*); + } + + try std.json.stringify( + BuildConfig{ + .deps_build_roots = deps_build_roots.items, + .packages = try packages.toPackageList(), + .include_dirs = include_dirs.keys(), + .top_level_steps = b.top_level_steps.keys(), + .available_options = available_options, + .c_macros = c_macros.keys(), + }, + .{ + .whitespace = .indent_2, + }, + std.io.getStdOut().writer(), + ); +} + +fn processPkgConfig( + allocator: std.mem.Allocator, + include_dirs: *std.StringArrayHashMapUnmanaged(void), + c_macros: *std.StringArrayHashMapUnmanaged(void), + exe: *Step.Compile, +) !void { + for (exe.root_module.link_objects.items) |link_object| { + if (link_object != .system_lib) continue; + const system_lib = link_object.system_lib; + + if (system_lib.use_pkg_config == .no) continue; + + const args = copied_from_zig.runPkgConfig(exe, system_lib.name) catch |err| switch (err) { + error.PkgConfigInvalidOutput, + error.PkgConfigCrashed, + error.PkgConfigFailed, + error.PkgConfigNotInstalled, + error.PackageNotFound, + => switch (system_lib.use_pkg_config) { + .yes => { + // pkg-config failed, so zig will not add any include paths + continue; + }, + .force => { + std.log.warn("pkg-config failed for library {s}", .{system_lib.name}); + continue; + }, + .no => unreachable, + }, + else => |e| return e, + }; + for (args) |arg| { + if (std.mem.startsWith(u8, arg, "-I")) { + const candidate = arg[2..]; + try include_dirs.put(allocator, candidate, {}); + } else if (std.mem.startsWith(u8, arg, "-D")) { + try c_macros.put(allocator, arg, {}); + } + } + } +} + +// TODO: Having a copy of this is not very nice +const copied_from_zig = struct { + /// Run pkg-config for the given library name and parse the output, returning the arguments + /// that should be passed to zig to link the given library. + fn runPkgConfig(self: *Step.Compile, lib_name: []const u8) ![]const []const u8 { + const b = self.step.owner; + const pkg_name = match: { + // First we have to map the library name to pkg config name. Unfortunately, + // there are several examples where this is not straightforward: + // -lSDL2 -> pkg-config sdl2 + // -lgdk-3 -> pkg-config gdk-3.0 + // -latk-1.0 -> pkg-config atk + const pkgs = try getPkgConfigList(b); + + // Exact match means instant winner. + for (pkgs) |pkg| { + if (mem.eql(u8, pkg.name, lib_name)) { + break :match pkg.name; + } + } + + // Next we'll try ignoring case. + for (pkgs) |pkg| { + if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) { + break :match pkg.name; + } + } + + // Now try appending ".0". + for (pkgs) |pkg| { + if (std.ascii.indexOfIgnoreCase(pkg.name, lib_name)) |pos| { + if (pos != 0) continue; + if (mem.eql(u8, pkg.name[lib_name.len..], ".0")) { + break :match pkg.name; + } + } + } + + // Trimming "-1.0". + if (mem.endsWith(u8, lib_name, "-1.0")) { + const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len]; + for (pkgs) |pkg| { + if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) { + break :match pkg.name; + } + } + } + + return error.PackageNotFound; + }; + + var code: u8 = undefined; + const stdout = if (b.runAllowFail(&.{ + "pkg-config", + pkg_name, + "--cflags", + "--libs", + }, &code, .Ignore)) |stdout| stdout else |err| switch (err) { + error.ProcessTerminated => return error.PkgConfigCrashed, + error.ExecNotSupported => return error.PkgConfigFailed, + error.ExitCodeFailure => return error.PkgConfigFailed, + error.FileNotFound => return error.PkgConfigNotInstalled, + else => return err, + }; + + var zig_args = ArrayList([]const u8).init(b.allocator); + defer zig_args.deinit(); + + var it = mem.tokenizeAny(u8, stdout, " \r\n\t"); + while (it.next()) |tok| { + if (mem.eql(u8, tok, "-I")) { + const dir = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&.{ "-I", dir }); + } else if (mem.startsWith(u8, tok, "-I")) { + try zig_args.append(tok); + } else if (mem.eql(u8, tok, "-L")) { + const dir = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&.{ "-L", dir }); + } else if (mem.startsWith(u8, tok, "-L")) { + try zig_args.append(tok); + } else if (mem.eql(u8, tok, "-l")) { + const lib = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&.{ "-l", lib }); + } else if (mem.startsWith(u8, tok, "-l")) { + try zig_args.append(tok); + } else if (mem.eql(u8, tok, "-D")) { + const macro = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&.{ "-D", macro }); + } else if (mem.startsWith(u8, tok, "-D")) { + try zig_args.append(tok); + } else if (b.debug_pkg_config) { + return self.step.fail("unknown pkg-config flag '{s}'", .{tok}); + } + } + + return zig_args.toOwnedSlice(); + } + + fn execPkgConfigList(self: *std.Build, out_code: *u8) (std.Build.PkgConfigError || std.Build.RunError)![]const std.Build.PkgConfigPkg { + const stdout = try self.runAllowFail(&.{ "pkg-config", "--list-all" }, out_code, .Ignore); + var list = ArrayList(std.Build.PkgConfigPkg).init(self.allocator); + errdefer list.deinit(); + var line_it = mem.tokenizeAny(u8, stdout, "\r\n"); + while (line_it.next()) |line| { + if (mem.trim(u8, line, " \t").len == 0) continue; + var tok_it = mem.tokenizeAny(u8, line, " \t"); + try list.append(.{ + .name = tok_it.next() orelse return error.PkgConfigInvalidOutput, + .desc = tok_it.rest(), + }); + } + return list.toOwnedSlice(); + } + + fn getPkgConfigList(self: *std.Build) ![]const std.Build.PkgConfigPkg { + if (self.pkg_config_pkg_list) |res| { + return res; + } + var code: u8 = undefined; + if (execPkgConfigList(self, &code)) |list| { + self.pkg_config_pkg_list = list; + return list; + } else |err| { + const result = switch (err) { + error.ProcessTerminated => error.PkgConfigCrashed, + error.ExecNotSupported => error.PkgConfigFailed, + error.ExitCodeFailure => error.PkgConfigFailed, + error.FileNotFound => error.PkgConfigNotInstalled, + error.InvalidName => error.PkgConfigNotInstalled, + error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput, + else => return err, + }; + self.pkg_config_pkg_list = result; + return result; + } + } +}; + +fn serveWatchErrorBundle( + transport: *Transport, + step_id: u32, + cycle: u32, + error_bundle: std.zig.ErrorBundle, +) !void { + const eb_hdr: shared.ServerToClient.ErrorBundle = .{ + .step_id = step_id, + .cycle = cycle, + .extra_len = @intCast(error_bundle.extra.len), + .string_bytes_len = @intCast(error_bundle.string_bytes.len), + }; + const bytes_len = @sizeOf(shared.ServerToClient.ErrorBundle) + 4 * error_bundle.extra.len + error_bundle.string_bytes.len; + try transport.serveMessage(.{ + .tag = @intFromEnum(shared.ServerToClient.Tag.watch_error_bundle), + .bytes_len = @intCast(bytes_len), + }, &.{ + std.mem.asBytes(&eb_hdr), + std.mem.sliceAsBytes(error_bundle.extra), + error_bundle.string_bytes, + }); +} diff --git a/core/src/main/resources/build_runner/0.14.0/shared.zig b/core/src/main/resources/build_runner/0.14.0/shared.zig new file mode 100644 index 00000000..b27e1fbb --- /dev/null +++ b/core/src/main/resources/build_runner/0.14.0/shared.zig @@ -0,0 +1,241 @@ +//! Contains shared code between ZLS and it's custom build runner + +const std = @import("std"); +const builtin = @import("builtin"); +const native_endian = builtin.target.cpu.arch.endian(); +const need_bswap = native_endian != .little; + +pub const BuildConfig = struct { + deps_build_roots: []DepsBuildRoots, + packages: []Package, + include_dirs: []const []const u8, + top_level_steps: []const []const u8, + available_options: std.json.ArrayHashMap(AvailableOption), + c_macros: []const []const u8 = &.{}, + + pub const DepsBuildRoots = Package; + pub const Package = struct { + name: []const u8, + path: []const u8, + }; + pub const AvailableOption = std.meta.FieldType(std.meta.FieldType(std.Build, .available_options_map).KV, .value); +}; + +pub const Transport = struct { + in: std.fs.File, + out: std.fs.File, + poller: std.io.Poller(StreamEnum), + + const StreamEnum = enum { in }; + + pub const Header = extern struct { + tag: u32, + /// Size of the body only; does not include this Header. + bytes_len: u32, + }; + + pub const Options = struct { + gpa: std.mem.Allocator, + in: std.fs.File, + out: std.fs.File, + }; + + pub fn init(options: Options) Transport { + return .{ + .in = options.in, + .out = options.out, + .poller = std.io.poll(options.gpa, StreamEnum, .{ .in = options.in }), + }; + } + + pub fn deinit(transport: *Transport) void { + transport.poller.deinit(); + transport.* = undefined; + } + + pub fn receiveMessage(transport: *Transport, timeout_ns: ?u64) !Header { + const fifo = transport.poller.fifo(.in); + + poll: while (true) { + while (fifo.readableLength() < @sizeOf(Header)) { + if (!(if (timeout_ns) |timeout| try transport.poller.pollTimeout(timeout) else try transport.poller.poll())) break :poll; + } + const header = fifo.reader().readStructEndian(Header, .little) catch unreachable; + while (fifo.readableLength() < header.bytes_len) { + if (!(if (timeout_ns) |timeout| try transport.poller.pollTimeout(timeout) else try transport.poller.poll())) break :poll; + } + return header; + } + return error.EndOfStream; + } + + pub fn reader(transport: *Transport) std.io.PollFifo.Reader { + return transport.poller.fifo(.in).reader(); + } + + pub fn discard(transport: *Transport, bytes: usize) void { + transport.poller.fifo(.in).discard(bytes); + } + + pub fn receiveBytes( + transport: *Transport, + allocator: std.mem.Allocator, + len: usize, + ) (std.mem.Allocator.Error || std.fs.File.ReadError || error{EndOfStream})![]u8 { + return try transport.receiveSlice(allocator, u8, len); + } + + pub fn receiveSlice( + transport: *Transport, + allocator: std.mem.Allocator, + comptime T: type, + len: usize, + ) (std.mem.Allocator.Error || std.fs.File.ReadError || error{EndOfStream})![]T { + const bytes = try allocator.alignedAlloc(u8, @alignOf(T), len * @sizeOf(T)); + errdefer allocator.free(bytes); + const amt = try transport.reader().readAll(bytes); + if (amt != len * @sizeOf(T)) return error.EndOfStream; + const result = std.mem.bytesAsSlice(T, bytes); + std.debug.assert(result.len == len); + if (need_bswap) { + for (result) |*item| { + item.* = @byteSwap(item.*); + } + } + return result; + } + + pub fn serveMessage( + client: *const Transport, + header: Header, + bufs: []const []const u8, + ) std.fs.File.WriteError!void { + std.debug.assert(bufs.len < 10); + var iovecs: [10]std.posix.iovec_const = undefined; + var header_le = header; + if (need_bswap) std.mem.byteSwapAllFields(Header, &header_le); + const header_bytes = std.mem.asBytes(&header_le); + iovecs[0] = .{ .base = header_bytes.ptr, .len = header_bytes.len }; + for (bufs, iovecs[1 .. bufs.len + 1]) |buf, *iovec| { + iovec.* = .{ + .base = buf.ptr, + .len = buf.len, + }; + } + try client.out.writevAll(iovecs[0 .. bufs.len + 1]); + } +}; + +pub const ServerToClient = struct { + pub const Tag = enum(u32) { + /// Body is an ErrorBundle. + watch_error_bundle, + + _, + }; + + /// Trailing: + /// * extra: [extra_len]u32, + /// * string_bytes: [string_bytes_len]u8, + /// See `std.zig.ErrorBundle`. + pub const ErrorBundle = extern struct { + step_id: u32, + cycle: u32, + extra_len: u32, + string_bytes_len: u32, + }; +}; + +pub const BuildOnSaveSupport = union(enum) { + supported, + invalid_linux_kernel_version: if (builtin.os.tag == .linux) std.meta.FieldType(std.posix.utsname, .release) else noreturn, + unsupported_linux_kernel_version: if (builtin.os.tag == .linux) std.SemanticVersion else noreturn, + unsupported_zig_version: if (@TypeOf(minimum_zig_version) != void) void else noreturn, + unsupported_os: if (@TypeOf(minimum_zig_version) == void) void else noreturn, + + const linux_support_version = std.SemanticVersion.parse("0.14.0-dev.283+1d20ff11d") catch unreachable; + const windows_support_version = std.SemanticVersion.parse("0.14.0-dev.625+2de0e2eca") catch unreachable; + const kqueue_support_version = std.SemanticVersion.parse("0.14.0-dev.2046+b8795b4d0") catch unreachable; + + // We can't rely on `std.Build.Watch.have_impl` because we need to + // check the runtime Zig version instead of Zig version that ZLS + // has been built with. + pub const minimum_zig_version = switch (builtin.os.tag) { + .linux => linux_support_version, + .windows => windows_support_version, + .dragonfly, + .freebsd, + .netbsd, + .openbsd, + .ios, + .macos, + .tvos, + .visionos, + .watchos, + .haiku, + => kqueue_support_version, + else => {}, + }; + + /// std.build.Watch requires `AT_HANDLE_FID` which is Linux 6.5+ + /// https://github.com/ziglang/zig/issues/20720 + pub const minimum_linux_version: std.SemanticVersion = .{ .major = 6, .minor = 5, .patch = 0 }; + + /// Returns true if is comptime known that build on save is supported. + pub inline fn isSupportedComptime() bool { + if (!std.process.can_spawn) return false; + if (builtin.single_threaded) return false; + return true; + } + + pub fn isSupportedRuntime(runtime_zig_version: std.SemanticVersion) BuildOnSaveSupport { + comptime std.debug.assert(isSupportedComptime()); + + if (builtin.os.tag == .linux) blk: { + const utsname = std.posix.uname(); + const unparsed_version = std.mem.sliceTo(&utsname.release, 0); + const version = parseUnameKernelVersion(unparsed_version) catch + return .{ .invalid_linux_kernel_version = utsname.release }; + + if (version.order(minimum_linux_version) != .lt) break :blk; + std.debug.assert(version.build == null and version.pre == null); // Otherwise, returning the `std.SemanticVersion` would be unsafe + return .{ + .unsupported_linux_kernel_version = version, + }; + } + + if (@TypeOf(minimum_zig_version) == void) { + return .unsupported_os; + } + + if (runtime_zig_version.order(minimum_zig_version) == .lt) { + return .unsupported_zig_version; + } + + return .supported; + } +}; + +/// Parses a Linux Kernel Version. The result will ignore pre-release and build metadata. +fn parseUnameKernelVersion(kernel_version: []const u8) !std.SemanticVersion { + const extra_index = std.mem.indexOfAny(u8, kernel_version, "-+"); + const required = kernel_version[0..(extra_index orelse kernel_version.len)]; + var it = std.mem.splitScalar(u8, required, '.'); + return .{ + .major = try std.fmt.parseUnsigned(usize, it.next() orelse return error.InvalidVersion, 10), + .minor = try std.fmt.parseUnsigned(usize, it.next() orelse return error.InvalidVersion, 10), + .patch = try std.fmt.parseUnsigned(usize, it.next() orelse return error.InvalidVersion, 10), + }; +} + +test parseUnameKernelVersion { + try std.testing.expectFmt("5.17.0", "{}", .{try parseUnameKernelVersion("5.17.0")}); + try std.testing.expectFmt("6.12.9", "{}", .{try parseUnameKernelVersion("6.12.9-rc7")}); + try std.testing.expectFmt("6.6.71", "{}", .{try parseUnameKernelVersion("6.6.71-42-generic")}); + try std.testing.expectFmt("5.15.167", "{}", .{try parseUnameKernelVersion("5.15.167.4-microsoft-standard-WSL2")}); // WSL2 + try std.testing.expectFmt("4.4.0", "{}", .{try parseUnameKernelVersion("4.4.0-20241-Microsoft")}); // WSL1 + + try std.testing.expectError(error.InvalidCharacter, parseUnameKernelVersion("")); + try std.testing.expectError(error.InvalidVersion, parseUnameKernelVersion("5")); + try std.testing.expectError(error.InvalidVersion, parseUnameKernelVersion("5.5")); +} diff --git a/core/src/main/resources/build_runner/LICENSE b/core/src/main/resources/build_runner/LICENSE new file mode 100644 index 00000000..2bf33e97 --- /dev/null +++ b/core/src/main/resources/build_runner/LICENSE @@ -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. \ No newline at end of file diff --git a/core/src/main/resources/build_runner/README.MD b/core/src/main/resources/build_runner/README.MD new file mode 100644 index 00000000..be78975d --- /dev/null +++ b/core/src/main/resources/build_runner/README.MD @@ -0,0 +1,5 @@ +This folder contains build runners taken from the Zig Language Server. + +The build runners are based on the following ZLS commits + +0.14.0: https://github.com/zigtools/zls/tree/23f57730a20f7eec5a36a848bedb226ace2b56c6/src/build_runner