Skip to content

feat: add zls toolchain support#653

Merged
aherrmann merged 13 commits into
aherrmann:mainfrom
cerisier:zls-support
May 20, 2026
Merged

feat: add zls toolchain support#653
aherrmann merged 13 commits into
aherrmann:mainfrom
cerisier:zls-support

Conversation

@cerisier
Copy link
Copy Markdown
Contributor

@cerisier cerisier commented May 13, 2026

Closes #403

This is an adapted port of an internal zls support code I've had for more than 6 months now.

Summary

  • Adds ZLS support to rules_zig.
  • Adds a zls_completion macro that produces a Bazel-backed ZLS launcher for a set of Zig targets.
  • Adds ZLS toolchain resolution, repository generation, and version metadata so the launcher can use the ZLS version associated with the active Zig toolchain.
  • Adds coverage for ZLS bzlmod/toolchain generation, build config generation, and Zig 0.15 / 0.16 runner compatibility.

Usage

Define a completion target for the Zig targets that should be visible to ZLS:

load("@rules_zig//zig/zls:defs.bzl", "zls_completion")

zls_completion(
    name = "completion",
    visibility = ["//visibility:public"],
    deps = [
        "//path/to:zig_target",
        "//path/to:zig_target2",
        "...",
    ],
)

Configure the editor to run a small wrapper script, e.g.:

#!/usr/bin/env bash
cd "$(dirname "${BASH_SOURCE[0]}")"
cd "$(bazel info workspace)"
exec bazel run -- //:completion "${@}"

How It Works

  • The editor does not invoke ZLS directly. It invokes the wrapper script, which runs the generated :completion target through Bazel.
  • :completion is a generated Zig runner. It resolves the selected ZLS binary, Zig executable, Zig lib directory, and custom build runner through Bazel runfiles, writes a temporary ZLS config, then launches ZLS with --config-path.
  • That config points ZLS at the Bazel-backed build runner instead of ZLS discovering build information through the normal Zig project flow.
  • When ZLS asks for build information, the custom build runner calls back into Bazel, runs the generated print_build_config target, and returns a ZLS build config derived from the declared Zig deps.

Implementation Notes

  • The generated build config is built from Zig providers/aspects and canonicalized so editor paths, workspace paths, and Bazel execution-root paths line up.
  • The ZLS extension follows the same overall repository-generation approach as the Zig toolchain extension: read a version index, create per-platform repositories, generate an aggregate toolchains repo, and select toolchains through Bazel constraints/settings.
  • zig_version is used for selection and zls_version is the downloaded artifact version, so they can differ when needed, e.g. Zig 0.15.2 using ZLS 0.15.1.

Validation

  • Implemented tests for zls_completion with the default Zig toolchain and with --@zig_toolchains//:version=0.15.2.
  • Added golden build-config coverage and ZLS bzlmod/toolchain tests.

@cerisier cerisier marked this pull request as ready for review May 18, 2026 12:27
Copy link
Copy Markdown
Owner

@aherrmann aherrmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! A few small comments.

I tried to use this locally on the tests that this adds:

Zed settings:

{
  "lsp": {
    "zls": {
      "binary": {
        "path": "tools/zls.sh"
      }
    }
  }
}

zls.sh:

#!/usr/bin/env bash
cd "$(dirname "${BASH_SOURCE[0]}")"
cd "$(bazel info workspace)"
exec bazel run -- //zig/tests/zls-completion:completion "${@}"

and an empty build.zig in the workspace root.

I notice a few things:

  • The ZLS log does show that it's picking up the Bazel provided ZLS: Starting ZLS 0.16.0 @ '...BAZEL_CACHE.../external/+zls+zls_0.16.0_x86_64-linux/zls'
  • It is loading the generated config Loaded config: /tmp/1779204056137495202.json
  • It configures the various config options, including global_cache_path.
  • However, it still generates a local cache (.zig-cache/ in the Bazel workspace root) - is that intentional? It would seem more plausible to match the Bazel Zig toolchain setting for both local and global cache.
  • In the end I'm seeing failed to receive message from zig build-on-save runner: error.EndOfStream, not sure if that's expected.
  • Things like go-to-definition from app.zig on mid.value() do not work - is that expected?

Comment thread zig/private/BUILD.bazel Outdated
Comment thread zig/zls/workspace_printer.zig Outdated
Comment thread zig/zls/zls_write_build_config.bzl
Comment thread zig/zls/zls_write_build_config.bzl Outdated
Comment thread README.md
@cerisier
Copy link
Copy Markdown
Contributor Author

All addressed, as for the mid.zig, that's because in this test, zls_completion only refers to lib and not the rest. Better to try on a real workspace !

@aherrmann
Copy link
Copy Markdown
Owner

All addressed, as for the mid.zig, that's because in this test, zls_completion only refers to lib and not the rest. Better to try on a real workspace !

Got it, just adding those targets to the list go-to-definition worked. Nice!

Copy link
Copy Markdown
Owner

@aherrmann aherrmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding this! ZLS is a great feature to have! 🎉

@aherrmann aherrmann merged commit b1eafe0 into aherrmann:main May 20, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zls Support

2 participants