Add language provider registry
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const lsp = @import("lsp.zig");
|
||||||
|
|
||||||
// Local v1 guardrails for SSH/iPhone use: visible rows instead of silent stalls.
|
// Local v1 guardrails for SSH/iPhone use: visible rows instead of silent stalls.
|
||||||
// req: testing/001, testing/002, testing/003, testing/004
|
// 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_stdout_limit: usize = 256 * 1024;
|
||||||
pub const process_stderr_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 {
|
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 });
|
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("unknown", sanitizeReason(""));
|
||||||
try std.testing.expectEqualStrings("spawn_error", sanitizeReason("spawn_error"));
|
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,
|
InvalidEdit,
|
||||||
InvalidEditRow,
|
InvalidEditRow,
|
||||||
InvalidHelp,
|
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 {
|
pub const Document = struct {
|
||||||
uri: []const u8,
|
uri: []const u8,
|
||||||
language_id: []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.expectEqualStrings("lsp:param:outside_call", moved.rows[0]);
|
||||||
try std.testing.expectEqual(@as(usize, 3), moved.cursor);
|
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