Add language provider registry

This commit is contained in:
slhx agent
2026-06-21 13:20:09 +02:00
parent 1d1ecf29d5
commit 7e639dd87f
2 changed files with 307 additions and 0 deletions
+38
View File
@@ -1,4 +1,5 @@
const std = @import("std");
const lsp = @import("lsp.zig");
// Local v1 guardrails for SSH/iPhone use: visible rows instead of silent stalls.
// req: testing/001, testing/002, testing/003, testing/004
@@ -7,6 +8,27 @@ pub const max_search_rows: usize = 64;
pub const process_stdout_limit: usize = 256 * 1024;
pub const process_stderr_limit: usize = 256 * 1024;
pub const SourceLabel = struct {
provider: ?[]const u8,
row: []const u8,
};
pub fn sourceLabel(row: []const u8) SourceLabel {
if (std.mem.startsWith(u8, row, "provider:")) {
return .{
.provider = lsp.providerIdFromRow(row) catch null,
.row = lsp.providerPayloadFromRow(row) catch row,
};
}
return .{ .provider = null, .row = row };
}
pub fn displayRowAlloc(allocator: std.mem.Allocator, row: []const u8) ![]u8 {
const labelled = sourceLabel(row);
if (labelled.provider) |provider| return std.fmt.allocPrint(allocator, "[{s}] {s}", .{ provider, labelled.row });
return allocator.dupe(u8, labelled.row);
}
pub fn searchTruncatedRowAlloc(allocator: std.mem.Allocator, shown: usize, limit: usize) ![]u8 {
return std.fmt.allocPrint(allocator, "diagnostic:search_truncated:showing_{d}_of_limit_{d}", .{ shown, limit });
}
@@ -46,3 +68,19 @@ test "adversarial: unsafe diagnostic reasons are sanitized" {
try std.testing.expectEqualStrings("unknown", sanitizeReason(""));
try std.testing.expectEqualStrings("spawn_error", sanitizeReason("spawn_error"));
}
test "regular: provider-labelled diagnostic rows display source identity" {
const labelled = sourceLabel("provider:zls:diagnostic:error:boom");
try std.testing.expectEqualStrings("zls", labelled.provider.?);
try std.testing.expectEqualStrings("diagnostic:error:boom", labelled.row);
const display = try displayRowAlloc(std.testing.allocator, "provider:zls:diagnostic:error:boom");
defer std.testing.allocator.free(display);
try std.testing.expectEqualStrings("[zls] diagnostic:error:boom", display);
}
test "regular: unlabelled diagnostic rows stay compatible" {
const labelled = sourceLabel("diagnostic:error:boom");
try std.testing.expect(labelled.provider == null);
try std.testing.expectEqualStrings("diagnostic:error:boom", labelled.row);
}
+269
View File
@@ -19,8 +19,162 @@ pub const Error = error{
InvalidEdit,
InvalidEditRow,
InvalidHelp,
DuplicateProvider,
InvalidProvider,
MissingProvider,
AmbiguousProvider,
};
pub const ProviderKind = enum {
lsp,
linter,
formatter,
build,
test_runner,
checker,
};
pub const ProviderCapability = enum {
diagnostics,
hover,
signature,
navigation,
format,
code_action,
lint,
build,
tests,
check,
rename,
organize_imports,
};
pub const Provider = struct {
id: []const u8,
name: []const u8,
kind: ProviderKind,
capabilities: []const ProviderCapability,
preferred_for_mutation: bool = false,
document_version: u64 = 0,
alive: bool = true,
pub fn supports(self: Provider, capability: ProviderCapability) bool {
for (self.capabilities) |candidate| if (candidate == capability) return true;
return false;
}
};
pub const ProviderChoice = union(enum) {
missing,
selected: Provider,
ambiguous: []Provider,
};
pub const ProviderRegistry = struct {
allocator: std.mem.Allocator,
providers: std.ArrayList(Provider),
pub fn init(allocator: std.mem.Allocator) ProviderRegistry {
return .{ .allocator = allocator, .providers = .empty };
}
pub fn deinit(self: *ProviderRegistry) void {
self.providers.deinit(self.allocator);
self.* = undefined;
}
pub fn register(self: *ProviderRegistry, provider: Provider) !void {
if (provider.id.len == 0 or provider.name.len == 0) return Error.InvalidProvider;
for (self.providers.items) |existing| {
if (std.mem.eql(u8, existing.id, provider.id)) return Error.DuplicateProvider;
}
try self.providers.append(self.allocator, provider);
}
pub fn nonMutatingProvidersAlloc(self: *const ProviderRegistry, allocator: std.mem.Allocator, capability: ProviderCapability) ![]Provider {
var matches = std.ArrayList(Provider).empty;
errdefer matches.deinit(allocator);
for (self.providers.items) |provider| {
if (provider.alive and provider.supports(capability)) try matches.append(allocator, provider);
}
return matches.toOwnedSlice(allocator);
}
pub fn chooseMutatingProvider(self: *const ProviderRegistry, allocator: std.mem.Allocator, capability: ProviderCapability) !ProviderChoice {
const matches = try self.nonMutatingProvidersAlloc(allocator, capability);
if (matches.len == 0) {
allocator.free(matches);
return .missing;
}
var preferred: ?Provider = null;
for (matches) |provider| if (provider.preferred_for_mutation) {
if (preferred != null) return .{ .ambiguous = matches };
preferred = provider;
};
if (preferred) |provider| {
allocator.free(matches);
return .{ .selected = provider };
}
if (matches.len == 1) {
const provider = matches[0];
allocator.free(matches);
return .{ .selected = provider };
}
return .{ .ambiguous = matches };
}
pub fn isStale(self: *const ProviderRegistry, provider_id: []const u8, document_version: u64) !bool {
for (self.providers.items) |provider| {
if (std.mem.eql(u8, provider.id, provider_id)) return document_version < provider.document_version;
}
return Error.MissingProvider;
}
};
pub fn providerRowsAlloc(allocator: std.mem.Allocator, provider: Provider, rows: []const []const u8) ![][]const u8 {
var labelled = std.ArrayList([]const u8).empty;
errdefer {
for (labelled.items) |row| allocator.free(row);
labelled.deinit(allocator);
}
for (rows) |row| {
try labelled.append(allocator, try std.fmt.allocPrint(allocator, "provider:{s}:{s}", .{ provider.id, row }));
}
return labelled.toOwnedSlice(allocator);
}
pub fn providerIdFromRow(row: []const u8) ![]const u8 {
if (!std.mem.startsWith(u8, row, "provider:")) return Error.InvalidProvider;
const rest = row[9..];
const split = std.mem.indexOfScalar(u8, rest, ':') orelse return Error.InvalidProvider;
if (split == 0) return Error.InvalidProvider;
return rest[0..split];
}
pub fn providerPayloadFromRow(row: []const u8) ![]const u8 {
if (!std.mem.startsWith(u8, row, "provider:")) return Error.InvalidProvider;
const rest = row[9..];
const split = std.mem.indexOfScalar(u8, rest, ':') orelse return Error.InvalidProvider;
if (split == 0 or split + 1 > rest.len) return Error.InvalidProvider;
return rest[split + 1 ..];
}
pub fn missingProviderRowAlloc(allocator: std.mem.Allocator, capability: ProviderCapability) ![]const u8 {
return std.fmt.allocPrint(allocator, "provider:missing:{s}", .{@tagName(capability)});
}
pub fn providerPickerRowsAlloc(allocator: std.mem.Allocator, capability: ProviderCapability, providers: []const Provider) ![][]const u8 {
var rows = std.ArrayList([]const u8).empty;
errdefer {
for (rows.items) |row| allocator.free(row);
rows.deinit(allocator);
}
for (providers) |provider| {
try rows.append(allocator, try std.fmt.allocPrint(allocator, "provider:choose:{s}:{s}:{s}", .{ @tagName(capability), provider.id, provider.name }));
}
return rows.toOwnedSlice(allocator);
}
pub const Document = struct {
uri: []const u8,
language_id: []const u8,
@@ -985,3 +1139,118 @@ test "adversarial: help payloads and non-call parameter moves fail safely" {
try std.testing.expectEqualStrings("lsp:param:outside_call", moved.rows[0]);
try std.testing.expectEqual(@as(usize, 3), moved.cursor);
}
test "regular: provider registry preserves source identity for non-mutating rows" {
var registry = ProviderRegistry.init(std.testing.allocator);
defer registry.deinit();
const caps = [_]ProviderCapability{ .diagnostics, .hover };
try registry.register(.{ .id = "zls", .name = "ZLS", .kind = .lsp, .capabilities = &caps, .document_version = 4 });
try registry.register(.{ .id = "lint", .name = "Lint", .kind = .linter, .capabilities = &[_]ProviderCapability{.diagnostics}, .document_version = 4 });
const providers = try registry.nonMutatingProvidersAlloc(std.testing.allocator, .diagnostics);
defer std.testing.allocator.free(providers);
try std.testing.expectEqual(@as(usize, 2), providers.len);
const rows_src = [_][]const u8{"lsp:diag:error:1:1:file.zig:bad"};
const rows = try providerRowsAlloc(std.testing.allocator, providers[0], &rows_src);
defer {
for (rows) |row| std.testing.allocator.free(row);
std.testing.allocator.free(rows);
}
try std.testing.expectEqualStrings("provider:zls:lsp:diag:error:1:1:file.zig:bad", rows[0]);
try std.testing.expectEqualStrings("zls", try providerIdFromRow(rows[0]));
try std.testing.expectEqualStrings("lsp:diag:error:1:1:file.zig:bad", try providerPayloadFromRow(rows[0]));
}
test "regular: mutating provider choice defaults to preferred or reports ambiguity" {
var registry = ProviderRegistry.init(std.testing.allocator);
defer registry.deinit();
try registry.register(.{ .id = "fmt-a", .name = "Formatter A", .kind = .formatter, .capabilities = &[_]ProviderCapability{.format} });
try registry.register(.{ .id = "fmt-b", .name = "Formatter B", .kind = .formatter, .capabilities = &[_]ProviderCapability{.format} });
var choice = try registry.chooseMutatingProvider(std.testing.allocator, .format);
switch (choice) {
.ambiguous => |providers| {
defer std.testing.allocator.free(providers);
try std.testing.expectEqual(@as(usize, 2), providers.len);
const picker = try providerPickerRowsAlloc(std.testing.allocator, .format, providers);
defer {
for (picker) |row| std.testing.allocator.free(row);
std.testing.allocator.free(picker);
}
try std.testing.expectEqualStrings("provider:choose:format:fmt-a:Formatter A", picker[0]);
},
else => return error.ExpectedAmbiguousProvider,
}
var preferred = ProviderRegistry.init(std.testing.allocator);
defer preferred.deinit();
try preferred.register(.{ .id = "fmt-a", .name = "Formatter A", .kind = .formatter, .capabilities = &[_]ProviderCapability{.format}, .preferred_for_mutation = true });
try preferred.register(.{ .id = "fmt-b", .name = "Formatter B", .kind = .formatter, .capabilities = &[_]ProviderCapability{.format} });
choice = try preferred.chooseMutatingProvider(std.testing.allocator, .format);
switch (choice) {
.selected => |provider| try std.testing.expectEqualStrings("fmt-a", provider.id),
else => return error.ExpectedSelectedProvider,
}
}
test "adversarial: provider registry rejects duplicates reports missing and stale" {
var registry = ProviderRegistry.init(std.testing.allocator);
defer registry.deinit();
try registry.register(.{ .id = "zls", .name = "ZLS", .kind = .lsp, .capabilities = &[_]ProviderCapability{.diagnostics}, .document_version = 7 });
try std.testing.expectError(Error.DuplicateProvider, registry.register(.{ .id = "zls", .name = "Other", .kind = .lsp, .capabilities = &[_]ProviderCapability{.hover} }));
const missing_choice = try registry.chooseMutatingProvider(std.testing.allocator, .format);
switch (missing_choice) {
.missing => {},
else => return error.ExpectedMissingProvider,
}
const missing_row = try missingProviderRowAlloc(std.testing.allocator, .format);
defer std.testing.allocator.free(missing_row);
try std.testing.expectEqualStrings("provider:missing:format", missing_row);
try std.testing.expect(try registry.isStale("zls", 6));
try std.testing.expect(!(try registry.isStale("zls", 7)));
try std.testing.expectError(Error.MissingProvider, registry.isStale("none", 1));
}
test "regular: provider-labelled diagnostic rows retain original parser compatibility" {
const payload =
\\{"params":{"uri":"file:///a.zig","diagnostics":[{"range":{"start":{"line":0,"character":2}},"severity":1,"message":"boom"}]}}
;
const rows = try diagnosticRowsAlloc(std.testing.allocator, payload);
defer {
for (rows) |row| std.testing.allocator.free(row);
std.testing.allocator.free(rows);
}
const provider: Provider = .{ .id = "zls", .name = "ZLS", .kind = .lsp, .capabilities = &[_]ProviderCapability{.diagnostics} };
const labelled = try providerRowsAlloc(std.testing.allocator, provider, rows[1..]);
defer {
for (labelled) |row| std.testing.allocator.free(row);
std.testing.allocator.free(labelled);
}
const payload_row = try providerPayloadFromRow(labelled[0]);
const location = try diagnosticLocationFromRow(payload_row);
try std.testing.expectEqual(@as(usize, 1), location.line);
try std.testing.expectEqual(@as(usize, 3), location.character);
}
test "regular: provider rows label hover signature navigation actions and lint uniformly" {
const provider: Provider = .{ .id = "zls", .name = "ZLS", .kind = .lsp, .capabilities = &[_]ProviderCapability{ .hover, .signature, .navigation, .code_action, .lint } };
const raw = [_][]const u8{
"lsp:hover:headline:docs",
"lsp:signature:call(arg):active=1",
"lsp:definition:src/main.zig:10:4",
"lsp:code_action:fix:remove unused",
"lint:warning:src/main.zig:1:1:style",
};
const rows = try providerRowsAlloc(std.testing.allocator, provider, &raw);
defer {
for (rows) |row| std.testing.allocator.free(row);
std.testing.allocator.free(rows);
}
for (rows, raw) |row, expected_payload| {
try std.testing.expectEqualStrings("zls", try providerIdFromRow(row));
try std.testing.expectEqualStrings(expected_payload, try providerPayloadFromRow(row));
}
}