Add language provider registry
This commit is contained in:
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user