Zls LSP not displaying enum/union functions?

Hey everyone, hope this is the right place for this question.
I was experimenting a bit with unions, trying to create a small neat interface for generating ANSI color escape codes at comptime. I settled on trying a combination of a color enum, a code union and a generator function which will generate the final output given a slice of codes:

Example usage:

print(ansi(&.{ .bold, .bg(.Green), .fg256(231) }) ++ " DEBUG " ++ ansi(&.{.reset}));

print(ansi(&.{ .blink, .bgRGB(24, 87, 69), .fg(.Red) }) ++ " TEST " ++ ansi(&.{.reset}));
Full Code
// Codes ripped from ANSI cheatsheet: https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b
//
//
//! Small utility providing a small interface to generate ANSI color codes.
//!
//! Example:
//! const text = comptime ansi(&.{ .bold, .bg(.Green), .fg256(231) }) ++ "I will be styled!" ++ ansi(&.{.reset});
//! const text = comptime ansi(&.{ .blink, .fgRGB(23, 180, 156) }) ++ "I will be styled!" ++ ansi(&.{.reset});
//!

const std = @import("std");

// TODO: Impl "Bright" colors?
pub const AnsiColor = enum(u8) {
    Black = 0,
    Red = 1,
    Green = 2,
    Yellow = 3,
    Blue = 4,
    Magenta = 5,
    Cyan = 6,
    White = 7,
};

pub const AnsiCode = union(enum) {
    // NOTE: Don't use these directly, the fg, bg, fg256, bg256, fgRGB, bgRGB functions
    // provide a more compact interface instead!
    _fg: AnsiColor,
    _bg: AnsiColor,
    _fg_256: u8,
    _bg_256: u8,
    _fg_rgb: @Vector(3, u8),
    _bg_rgb: @Vector(3, u8),

    reset,
    bold,
    dim,
    italic,
    underline,
    blink,
    reverse,
    hidden,
    strike,

    resetBoldDim, // Resets both as per spec
    resetItalic,
    resetUnderline,
    resetBlink,
    resetReverse,
    resetHidden,
    resetStrike,

    pub fn fg(color: AnsiColor) AnsiCode {
        return .{ ._fg = color };
    }
    pub fn bg(color: AnsiColor) AnsiCode {
        return .{ ._bg = color };
    }
    pub fn fg256(color: u8) AnsiCode {
        return .{ ._fg_256 = color };
    }
    pub fn bg256(color: u8) AnsiCode {
        return .{ ._bg_256 = color };
    }
    pub fn fgRGB(r: u8, g: u8, b: u8) AnsiCode {
        return .{ ._fg_rgb = .{ r, g,  b } };
    }
    pub fn bgRGB(r: u8, g: u8, b: u8) AnsiCode {
        return .{ ._bg_rgb = .{ r, g,  b } };
    }

    /// Returns the individual ANSI code number string.
    /// NOTE: This is only intended for internal use to build a full formating string!
    fn toCodePointString(self: AnsiCode) []const u8 {
        return switch (self) {
            .reset => "0",
            .bold => "1",
            .dim => "2",
            .italic => "3",
            .underline => "4",
            .blink => "5",
            .reverse => "7",
            .hidden => "8",
            .strike => "9",

            .resetBoldDim => "22",
            .resetItalic => "23",
            .resetUnderline => "24",
            .resetBlink => "25",
            .resetReverse => "27",
            .resetHidden => "28",
            .resetStrike => "29",

            ._fg => |col| std.fmt.comptimePrint("3{d}", .{@intFromEnum(col)}),
            ._bg => |col| std.fmt.comptimePrint("4{d}", .{@intFromEnum(col)}),

            ._fg_256 => |col| std.fmt.comptimePrint("38;5;{d}", .{col}),
            ._bg_256 => |col| std.fmt.comptimePrint("48;5;{d}", .{col}),

            ._fg_rgb => |col| std.fmt.comptimePrint("38;2;{d};{d};{d}", .{ col[0], col[1], col[2] }),
            ._bg_rgb => |col| std.fmt.comptimePrint("48;2;{d};{d};{d}", .{ col[0], col[1], col[2] }),
        };
    }

    /// Returns the full ANSI formatting string.
    pub fn toString(self: AnsiCode) []const u8 {
        return "\x1b[" ++ self.toCodePointString() ++ "m";
    }
};

pub fn ansi(comptime codes: []const AnsiCode) []const u8 {
    comptime {
        var buf: []const u8 = "\x1b[";
        for (codes, 0..) |code, i| {
            buf = buf ++ code.toCodePointString();
            if (i + 1 < codes.len) buf = buf ++ ";";
        }
        buf = buf ++ "m";
        return buf;
    }
}

When I use the ansi though:

  1. I don’t get any LSP hints for the []const AnsiCode argument. It compiles and works just fine, but I never get any hints but rather just a generic list:
  2. I also tried it with enum functions and there I did get the expected hints for the enum values themselves, but also no hints for any functions on the enum:
    https://imgur.com/a/1tNjymC (sry, not allowed to embed > 1 image)

I didn’t find any info on this… so i am curious, is this some issue with my LSP setup (nvim maybe?) or an issue with ZLS itself?

There is this old issue Enum completions are broken (missing functions & declarations) · Issue #1266 · zigtools/zls · GitHub talking about missing enum functions, but it was closed some time ago…
I am not sure if this is a new issue, or if it is related to the fact that I am using them inside the []const AnsiCode slice.

Are the functions defined with pub, I wonder whether that would have an influence on whether ZLS considers these for completion?

Yea they are pub. I did also try it without pub, then it doesn’t compile as expected. Rly seems like a zls issue at this point. Was wondering tho if there is something about member functions of enums/unions in ZIG which I didn’t know about as I didn’t find any examples actually talking about enum/union functions other than specific variant functions (pub fn foo(self: EnumVariant) ...) but nothing on “raw” functions on enums/unions.

I’m able to reproduce this in VS Code. I also tested whether completions are given in a few other cases:

                                                   // fields | decls
const example1: AnsiCode = .bold;                  // yes    | yes
const example2 = [_]AnsiCode{.bold};               // yes    | no
const example3: []const AnsiCode = &.{.bold};      // yes    | no
const example4: [1]AnsiCode = .{.bold};            // yes    | no
const example5 = @as(AnsiCode, .bold);             // no     | no
const example6 = @as([]const AnsiCode, &.{.bold}); // no     | no
const example7 = @as([1]AnsiCode, .{.bold});       // no     | no

zls tends to have issues when types are infered ie .{...}, it might work better if you specify the type &[_]AnsiCode{...}