Add language format and code action edits

This commit is contained in:
slhx agent
2026-06-21 14:21:09 +02:00
parent e4d6837f78
commit 616eea670b
4 changed files with 460 additions and 2 deletions
+2 -1
View File
@@ -392,7 +392,8 @@ Design-only and needing future implementation slices:
- parameter navigation in function calls; - parameter navigation in function calls;
- compact-first hover/signature cards with expandable scroll/search panels; - compact-first hover/signature cards with expandable scroll/search panels;
- full provider arbitration for multiple LSP/tool sources; - full provider arbitration for multiple LSP/tool sources;
- source-profile format-on-save policy and one-shot save-without-format; - source-profile-backed format, organize-imports, code-action edit application;
- format-on-save policy and one-shot save-without-format;
- contextual rails for all prefixes listed in this document; - contextual rails for all prefixes listed in this document;
- Insert pending-space rail, including `Space` pause then `n` to Normal; - Insert pending-space rail, including `Space` pause then `n` to Normal;
- counts/repetition grammar with direct digit and mobile repeat-rail paths; - counts/repetition grammar with direct digit and mobile repeat-rail paths;
+9 -1
View File
@@ -30,6 +30,10 @@ pub const Action = union(enum) {
hover, hover,
signature, signature,
expand_hover, expand_hover,
language_format,
language_format_policy,
language_organize_imports,
language_code_actions,
diagnostics_open, diagnostics_open,
diagnostics_next, diagnostics_next,
diagnostics_previous, diagnostics_previous,
@@ -220,7 +224,11 @@ pub const Leader = struct {
self.mode = .idle; self.mode = .idle;
if (std.mem.eql(u8, text, "h")) return .hover; if (std.mem.eql(u8, text, "h")) return .hover;
if (std.mem.eql(u8, text, "s")) return .signature; if (std.mem.eql(u8, text, "s")) return .signature;
if (std.mem.eql(u8, text, "o")) return .expand_hover; if (std.mem.eql(u8, text, "o")) return .language_organize_imports;
if (std.mem.eql(u8, text, "f")) return .language_format;
if (std.mem.eql(u8, text, "F")) return .language_format_policy;
if (std.mem.eql(u8, text, "w")) return .language_format_policy;
if (std.mem.eql(u8, text, "a")) return .language_code_actions;
self.message = "language action is not built in this profile yet"; self.message = "language action is not built in this profile yet";
return .{ .not_built = .lsp }; return .{ .not_built = .lsp };
}, },
+91
View File
@@ -23,6 +23,8 @@ pub const Error = error{
InvalidProvider, InvalidProvider,
MissingProvider, MissingProvider,
AmbiguousProvider, AmbiguousProvider,
StaleEdit,
OverlappingEdit,
}; };
pub const ProviderKind = enum { pub const ProviderKind = enum {
@@ -209,6 +211,95 @@ pub fn compactSignatureCardAlloc(allocator: std.mem.Allocator, row: []const u8)
return std.fmt.allocPrint(allocator, "[{s}] {s}", .{ provider, body }); return std.fmt.allocPrint(allocator, "[{s}] {s}", .{ provider, body });
} }
pub const ProviderEdit = struct {
provider: []const u8,
capability: ProviderCapability,
version: u64,
start: usize,
end: usize,
replacement: []const u8,
};
pub fn providerCapabilityFromName(name: []const u8) !ProviderCapability {
if (std.mem.eql(u8, name, "format")) return .format;
if (std.mem.eql(u8, name, "organize_imports")) return .organize_imports;
if (std.mem.eql(u8, name, "code_action")) return .code_action;
return Error.InvalidProvider;
}
pub fn editRowAlloc(
allocator: std.mem.Allocator,
provider: []const u8,
capability: ProviderCapability,
version: u64,
start: usize,
end: usize,
replacement: []const u8,
) ![]u8 {
if (provider.len == 0 or start > end) return Error.InvalidEdit;
return std.fmt.allocPrint(allocator, "provider:{s}:edit:{s}:{d}:{d}:{d}:{s}", .{ provider, @tagName(capability), version, start, end, replacement });
}
pub fn parseProviderEditRow(row: []const u8) !ProviderEdit {
const provider = try providerIdFromRow(row);
const payload = try providerPayloadFromRow(row);
if (!std.mem.startsWith(u8, payload, "edit:")) return Error.InvalidEdit;
var parts = std.mem.splitScalar(u8, payload, ':');
_ = parts.next() orelse return Error.InvalidEdit;
const capability_name = parts.next() orelse return Error.InvalidEdit;
const version_text = parts.next() orelse return Error.InvalidEdit;
const start_text = parts.next() orelse return Error.InvalidEdit;
const end_text = parts.next() orelse return Error.InvalidEdit;
const replacement = parts.rest();
const start = std.fmt.parseUnsigned(usize, start_text, 10) catch return Error.InvalidEdit;
const end = std.fmt.parseUnsigned(usize, end_text, 10) catch return Error.InvalidEdit;
if (start > end) return Error.InvalidEdit;
return .{
.provider = provider,
.capability = try providerCapabilityFromName(capability_name),
.version = std.fmt.parseUnsigned(u64, version_text, 10) catch return Error.InvalidEdit,
.start = start,
.end = end,
.replacement = replacement,
};
}
pub fn validateEditBatch(rows: []const []const u8, document_version: u64, document_len: usize) !void {
var previous_start: ?usize = null;
var previous_end: ?usize = null;
for (rows) |row| {
const edit = try parseProviderEditRow(row);
if (edit.version != document_version) return Error.StaleEdit;
if (edit.end > document_len) return Error.InvalidEdit;
if (previous_start) |start| {
if (!(edit.end <= start or edit.start >= previous_end.?)) return Error.OverlappingEdit;
}
previous_start = edit.start;
previous_end = edit.end;
}
}
pub fn actionPanelRowAlloc(allocator: std.mem.Allocator, row: []const u8) ![]const u8 {
const edit = try parseProviderEditRow(row);
return std.fmt.allocPrint(allocator, "action:{s}:provider:{s}:scope:{d}-{d}:apply", .{ @tagName(edit.capability), edit.provider, edit.start, edit.end });
}
pub fn providerFromActionPanelRow(row: []const u8) ![]const u8 {
const marker = ":provider:";
const start = std.mem.indexOf(u8, row, marker) orelse return Error.InvalidProvider;
const rest = row[start + marker.len ..];
const end = std.mem.indexOfScalar(u8, rest, ':') orelse return Error.InvalidProvider;
if (end == 0) return Error.InvalidProvider;
return rest[0..end];
}
pub fn capabilityFromActionPanelRow(row: []const u8) !ProviderCapability {
if (!std.mem.startsWith(u8, row, "action:")) return Error.InvalidProvider;
const rest = row[7..];
const end = std.mem.indexOfScalar(u8, rest, ':') orelse return Error.InvalidProvider;
return providerCapabilityFromName(rest[0..end]);
}
pub fn providerPickerRowsAlloc(allocator: std.mem.Allocator, capability: ProviderCapability, providers: []const Provider) ![][]const u8 { pub fn providerPickerRowsAlloc(allocator: std.mem.Allocator, capability: ProviderCapability, providers: []const Provider) ![][]const u8 {
var rows = std.ArrayList([]const u8).empty; var rows = std.ArrayList([]const u8).empty;
errdefer { errdefer {
+358
View File
@@ -44,6 +44,8 @@ const PanelContext = enum {
none, none,
file_picker, file_picker,
project_search, project_search,
language_provider_picker,
code_actions,
}; };
const EditSnapshot = struct { const EditSnapshot = struct {
@@ -155,6 +157,11 @@ pub const Client = struct {
hover_rows: std.ArrayList([]u8) = .empty, hover_rows: std.ArrayList([]u8) = .empty,
signature_rows: std.ArrayList([]u8) = .empty, signature_rows: std.ArrayList([]u8) = .empty,
diagnostic_rows: std.ArrayList([]u8) = .empty, diagnostic_rows: std.ArrayList([]u8) = .empty,
language_edit_rows: std.ArrayList([]u8) = .empty,
format_provider_default: ?[]u8 = null,
format_on_save: bool = true,
skip_next_save_format: bool = false,
pending_language_capability: ?lsp_mod.ProviderCapability = null,
diagnostic_filter: ?[]u8 = null, diagnostic_filter: ?[]u8 = null,
diagnostic_index: usize = 0, diagnostic_index: usize = 0,
diagnostic_selected: bool = false, diagnostic_selected: bool = false,
@@ -192,6 +199,8 @@ pub const Client = struct {
self.freeOwnedClientRows(&self.hover_rows); self.freeOwnedClientRows(&self.hover_rows);
self.freeOwnedClientRows(&self.signature_rows); self.freeOwnedClientRows(&self.signature_rows);
self.freeOwnedClientRows(&self.diagnostic_rows); self.freeOwnedClientRows(&self.diagnostic_rows);
self.freeOwnedClientRows(&self.language_edit_rows);
if (self.format_provider_default) |provider| self.allocator.free(provider);
if (self.diagnostic_filter) |filter| self.allocator.free(filter); if (self.diagnostic_filter) |filter| self.allocator.free(filter);
self.freeSnapshotStack(&self.undo_stack); self.freeSnapshotStack(&self.undo_stack);
self.freeSnapshotStack(&self.redo_stack); self.freeSnapshotStack(&self.redo_stack);
@@ -216,6 +225,10 @@ pub const Client = struct {
if (std.mem.startsWith(u8, line, "lsp_signature_fixture ")) return self.addSignatureFixture(line[22..]); if (std.mem.startsWith(u8, line, "lsp_signature_fixture ")) return self.addSignatureFixture(line[22..]);
if (std.mem.startsWith(u8, line, "diagnostic_fixture ")) return self.addDiagnosticFixture(line[19..]); if (std.mem.startsWith(u8, line, "diagnostic_fixture ")) return self.addDiagnosticFixture(line[19..]);
if (std.mem.startsWith(u8, line, "diagnostic_filter ")) return self.setDiagnosticFilter(line[18..]); if (std.mem.startsWith(u8, line, "diagnostic_filter ")) return self.setDiagnosticFilter(line[18..]);
if (std.mem.startsWith(u8, line, "language_edit ")) return self.addLanguageEditFixture(line[14..]);
if (std.mem.startsWith(u8, line, "language_default_format ")) return self.setFormatProviderDefault(line[24..]);
if (std.mem.startsWith(u8, line, "format_on_save ")) return self.setFormatOnSavePolicy(line[15..]);
if (std.mem.eql(u8, line, "save_without_format")) return self.saveWithoutFormat();
if (std.mem.startsWith(u8, line, "current_file ")) return self.setCurrentPath(line[13..]); if (std.mem.startsWith(u8, line, "current_file ")) return self.setCurrentPath(line[13..]);
if (std.mem.startsWith(u8, line, "job_profile ")) return self.openJobProfileByName(line[12..]); if (std.mem.startsWith(u8, line, "job_profile ")) return self.openJobProfileByName(line[12..]);
if (std.mem.startsWith(u8, line, "job_missing_tool ")) return self.openMissingToolJob(line[17..]); if (std.mem.startsWith(u8, line, "job_missing_tool ")) return self.openMissingToolJob(line[17..]);
@@ -1205,6 +1218,8 @@ pub const Client = struct {
switch (self.panel_context) { switch (self.panel_context) {
.file_picker => try self.openRepoPathFromPanel(owned), .file_picker => try self.openRepoPathFromPanel(owned),
.project_search => try self.openProjectSearchResult(owned), .project_search => try self.openProjectSearchResult(owned),
.language_provider_picker => try self.applyLanguageProviderChoice(owned),
.code_actions => try self.applyCodeActionRow(owned),
.none => self.message = "panel item has no action", .none => self.message = "panel item has no action",
} }
} }
@@ -1310,6 +1325,198 @@ pub const Client = struct {
try self.openListRows("hover", rows, .none); try self.openListRows("hover", rows, .none);
} }
fn addLanguageEditFixture(self: *Client, payload: []const u8) !void {
var parts = std.mem.splitScalar(u8, payload, '|');
const provider = parts.next() orelse return Error.ProtocolRejected;
const capability_name = parts.next() orelse return Error.ProtocolRejected;
const version_text = parts.next() orelse return Error.ProtocolRejected;
const start_text = parts.next() orelse return Error.ProtocolRejected;
const end_text = parts.next() orelse return Error.ProtocolRejected;
const replacement = parts.rest();
const capability = lsp_mod.providerCapabilityFromName(capability_name) catch return Error.ProtocolRejected;
const version = std.fmt.parseUnsigned(u64, version_text, 10) catch return Error.ProtocolRejected;
const start = std.fmt.parseUnsigned(usize, start_text, 10) catch return Error.ProtocolRejected;
const end = std.fmt.parseUnsigned(usize, end_text, 10) catch return Error.ProtocolRejected;
const row = lsp_mod.editRowAlloc(self.allocator, provider, capability, version, start, end, replacement) catch return Error.ProtocolRejected;
try self.language_edit_rows.append(self.allocator, row);
}
fn setFormatProviderDefault(self: *Client, provider: []const u8) !void {
if (self.format_provider_default) |old| self.allocator.free(old);
self.format_provider_default = if (std.mem.eql(u8, provider, "none")) null else try self.allocator.dupe(u8, provider);
const message = try std.fmt.allocPrint(self.allocator, "format default:{s}", .{self.format_provider_default orelse "none"});
self.setOwnedStatusMessage(message);
}
fn setFormatOnSavePolicy(self: *Client, policy: []const u8) !void {
if (std.mem.eql(u8, policy, "on")) self.format_on_save = true else if (std.mem.eql(u8, policy, "off")) self.format_on_save = false else return Error.ProtocolRejected;
try self.showFormatPolicy();
}
fn showFormatPolicy(self: *Client) !void {
const provider = self.format_provider_default orelse "select";
const message = try std.fmt.allocPrint(self.allocator, "format-on-save:{s} provider:{s} one-shot:save_without_format", .{ if (self.format_on_save) "on" else "off", provider });
self.setOwnedStatusMessage(message);
}
fn saveWithoutFormat(self: *Client) !void {
self.skip_next_save_format = true;
try self.save();
}
fn formatCurrentBuffer(self: *Client) !void {
try self.applyLanguageMutation(.format, null);
}
fn organizeImports(self: *Client) !void {
if (!self.hasLanguageProvider(.organize_imports) and self.hover_rows.items.len != 0) return self.openExpandedHover();
try self.applyLanguageMutation(.organize_imports, null);
}
fn hasLanguageProvider(self: *Client, capability: lsp_mod.ProviderCapability) bool {
for (self.language_edit_rows.items) |row| {
const edit = lsp_mod.parseProviderEditRow(row) catch continue;
if (edit.capability == capability) return true;
}
return false;
}
fn openCodeActions(self: *Client) !void {
const rows = try self.actionRowsForCapability(.code_action);
defer freeOwnedRows(self.allocator, rows);
if (rows.len == 0) {
self.message = "NoProvider:code_action";
return;
}
try self.openListRows("code-actions", rows, .code_actions);
}
fn actionRowsForCapability(self: *Client, capability: lsp_mod.ProviderCapability) ![][]const u8 {
var rows = std.ArrayList([]const u8).empty;
errdefer freeOwnedRows(self.allocator, rows.items);
for (self.language_edit_rows.items) |row| {
const edit = lsp_mod.parseProviderEditRow(row) catch continue;
if (edit.capability != capability) continue;
try rows.append(self.allocator, try lsp_mod.actionPanelRowAlloc(self.allocator, row));
}
return rows.toOwnedSlice(self.allocator);
}
fn applyCodeActionRow(self: *Client, row: []const u8) !void {
const provider = lsp_mod.providerFromActionPanelRow(row) catch return Error.ProtocolRejected;
const capability = lsp_mod.capabilityFromActionPanelRow(row) catch return Error.ProtocolRejected;
try self.applyLanguageMutation(capability, provider);
}
fn applyLanguageProviderChoice(self: *Client, row: []const u8) !void {
const provider = lsp_mod.providerFromActionPanelRow(row) catch return Error.ProtocolRejected;
const capability = self.pending_language_capability orelse return Error.ProtocolRejected;
self.pending_language_capability = null;
try self.applyLanguageMutation(capability, provider);
}
fn applyLanguageMutation(self: *Client, capability: lsp_mod.ProviderCapability, forced_provider: ?[]const u8) !void {
const provider = try self.chooseLanguageProvider(capability, forced_provider);
if (provider == null) return;
try self.applyProviderEdits(capability, provider.?);
}
fn chooseLanguageProvider(self: *Client, capability: lsp_mod.ProviderCapability, forced_provider: ?[]const u8) !?[]const u8 {
if (forced_provider) |provider| return provider;
var providers = std.ArrayList([]const u8).empty;
defer providers.deinit(self.allocator);
for (self.language_edit_rows.items) |row| {
const edit = lsp_mod.parseProviderEditRow(row) catch continue;
if (edit.capability != capability) continue;
var seen = false;
for (providers.items) |provider| {
if (std.mem.eql(u8, provider, edit.provider)) seen = true;
}
if (!seen) try providers.append(self.allocator, edit.provider);
}
if (providers.items.len == 0) {
const message = try std.fmt.allocPrint(self.allocator, "NoProvider:{s}", .{@tagName(capability)});
self.setOwnedStatusMessage(message);
return null;
}
if (capability == .format) {
if (self.format_provider_default) |preferred| {
for (providers.items) |provider| if (std.mem.eql(u8, provider, preferred)) return provider;
}
}
if (providers.items.len == 1) return providers.items[0];
var rows = std.ArrayList([]const u8).empty;
errdefer freeOwnedRows(self.allocator, rows.items);
for (providers.items) |provider| try rows.append(self.allocator, try std.fmt.allocPrint(self.allocator, "action:{s}:provider:{s}:scope:buffer:choose", .{ @tagName(capability), provider }));
const owned = try rows.toOwnedSlice(self.allocator);
defer freeOwnedRows(self.allocator, owned);
self.pending_language_capability = capability;
try self.openListRows("language-providers", owned, .language_provider_picker);
return null;
}
const EditOrder = struct { start: usize, row: []const u8 };
fn applyProviderEdits(self: *Client, capability: lsp_mod.ProviderCapability, provider: []const u8) !void {
const snap = try self.session.snapshot();
var matches = std.ArrayList([]const u8).empty;
defer matches.deinit(self.allocator);
for (self.language_edit_rows.items) |row| {
const edit = lsp_mod.parseProviderEditRow(row) catch {
self.message = "InvalidEdit";
return;
};
if (edit.capability == capability and std.mem.eql(u8, edit.provider, provider)) try matches.append(self.allocator, row);
}
if (matches.items.len == 0) {
const message = try std.fmt.allocPrint(self.allocator, "NoProvider:{s}", .{@tagName(capability)});
self.setOwnedStatusMessage(message);
return;
}
var ordered = std.ArrayList(EditOrder).empty;
defer ordered.deinit(self.allocator);
for (matches.items) |row| {
const edit = lsp_mod.parseProviderEditRow(row) catch {
self.message = "InvalidEdit";
return;
};
if (edit.version != self.document_version) {
self.message = "StaleEdit";
return;
}
if (edit.end > snap.bytes.len) {
self.message = "InvalidEdit";
return;
}
for (ordered.items) |existing| {
const prev = lsp_mod.parseProviderEditRow(existing.row) catch unreachable;
if (!(edit.end <= prev.start or edit.start >= prev.end)) {
self.message = "OverlappingEdit";
return;
}
}
try ordered.append(self.allocator, .{ .start = edit.start, .row = row });
}
std.mem.sort(EditOrder, ordered.items, {}, struct {
fn lessThan(_: void, lhs: EditOrder, rhs: EditOrder) bool {
return lhs.start > rhs.start;
}
}.lessThan);
try self.recordUndo();
self.clearRedo();
for (ordered.items) |item| {
const edit = lsp_mod.parseProviderEditRow(item.row) catch unreachable;
self.session.replaceRange(edit.start, edit.end, edit.replacement) catch |err| {
self.dropLastUndoSnapshot();
self.message = @errorName(err);
return;
};
}
self.noteDocumentChanged();
const message = try std.fmt.allocPrint(self.allocator, "{s}:{s}:applied", .{ @tagName(capability), provider });
self.setOwnedStatusMessage(message);
}
const DiagnosticDirection = enum { next, previous }; const DiagnosticDirection = enum { next, previous };
fn diagnosticMatchesFilter(self: *const Client, row: []const u8) bool { fn diagnosticMatchesFilter(self: *const Client, row: []const u8) bool {
@@ -1947,6 +2154,10 @@ pub const Client = struct {
.hover => try self.showCompactHover(), .hover => try self.showCompactHover(),
.signature => try self.showCompactSignature(), .signature => try self.showCompactSignature(),
.expand_hover => try self.openExpandedHover(), .expand_hover => try self.openExpandedHover(),
.language_format => try self.formatCurrentBuffer(),
.language_format_policy => try self.showFormatPolicy(),
.language_organize_imports => try self.organizeImports(),
.language_code_actions => try self.openCodeActions(),
.diagnostics_open => try self.openDiagnosticsPanel(), .diagnostics_open => try self.openDiagnosticsPanel(),
.diagnostics_next => try self.gotoDiagnostic(.next), .diagnostics_next => try self.gotoDiagnostic(.next),
.diagnostics_previous => try self.gotoDiagnostic(.previous), .diagnostics_previous => try self.gotoDiagnostic(.previous),
@@ -1999,6 +2210,12 @@ pub const Client = struct {
} }
fn save(self: *Client) !void { fn save(self: *Client) !void {
if (self.format_on_save and !self.skip_next_save_format and self.hasLanguageProvider(.format)) {
const before_format_version = self.document_version;
try self.applyLanguageMutation(.format, null);
if (self.document_version == before_format_version) return;
}
self.skip_next_save_format = false;
const snap = try self.session.snapshot(); const snap = try self.session.snapshot();
if (snap.bytes.len > diagnostics_mod.max_file_bytes) { if (snap.bytes.len > diagnostics_mod.max_file_bytes) {
self.message = "diagnostic:save_failed:file_too_large"; self.message = "diagnostic:save_failed:file_too_large";
@@ -4223,3 +4440,144 @@ test "regular: Space t jump opens selected diagnostic row and does not mix outpu
try std.testing.expectEqualStrings("one\nabcdTARGET\n", snap.bytes); try std.testing.expectEqualStrings("one\nabcdTARGET\n", snap.bytes);
try std.testing.expectEqual(@as(usize, 8), snap.cursor_byte); try std.testing.expectEqual(@as(usize, 8), snap.cursor_byte);
} }
test "regular: language format applies provider edit and undo restores" {
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
defer client.deinit();
try client.handleTraceLine("open dirty");
try client.handleTraceLine("language_edit zls|format|1|0|5|clean");
try client.handleInput(" ");
try client.handleInput("l");
try client.handleInput("f");
var snap = try client.session.snapshot();
try std.testing.expectEqualStrings("clean", snap.bytes);
try client.handleInput("u");
snap = try client.session.snapshot();
try std.testing.expectEqualStrings("dirty", snap.bytes);
}
test "regular: format on save and one-shot save without format" {
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
defer client.deinit();
try client.handleTraceLine("open abc");
try client.handleTraceLine("language_default_format zls");
try client.handleTraceLine("language_edit zls|format|1|0|3|ABC");
try client.handleTraceLine("save");
try std.testing.expectEqualStrings("ABC", try client.saved());
var ambiguous = try Client.init(std.testing.allocator, .{ .width = 80, .height = 6 });
defer ambiguous.deinit();
try ambiguous.handleTraceLine("open abc");
try ambiguous.handleTraceLine("language_edit zls|format|1|0|3|ZLS");
try ambiguous.handleTraceLine("language_edit prettier|format|1|0|3|PRETTY");
try ambiguous.handleTraceLine("save");
try std.testing.expectError(Error.NothingSaved, ambiguous.saved());
const ambiguous_frame = try ambiguous.render(std.testing.allocator);
defer std.testing.allocator.free(ambiguous_frame);
try std.testing.expect(std.mem.indexOf(u8, ambiguous_frame, "action:format:provider:zls") != null);
var skip = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
defer skip.deinit();
try skip.handleTraceLine("open abc");
try skip.handleTraceLine("language_default_format zls");
try skip.handleTraceLine("language_edit zls|format|1|0|3|ABC");
try skip.handleTraceLine("save_without_format");
try std.testing.expectEqualStrings("abc", try skip.saved());
}
test "regular: provider picker applies selected formatter" {
var client = try Client.init(std.testing.allocator, .{ .width = 80, .height = 7 });
defer client.deinit();
try client.handleTraceLine("open abc");
try client.handleTraceLine("language_edit zls|format|1|0|3|ZLS");
try client.handleTraceLine("language_edit prettier|format|1|0|3|PRETTY");
try client.handleInput(" ");
try client.handleInput("l");
try client.handleInput("f");
const picker_frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(picker_frame);
try std.testing.expect(std.mem.indexOf(u8, picker_frame, "action:format:provider:zls") != null);
try std.testing.expect(std.mem.indexOf(u8, picker_frame, "action:format:provider:prettier") != null);
try client.handleTraceLine("list_filter prettier");
try client.handleTraceLine("key enter");
const snap = try client.session.snapshot();
try std.testing.expectEqualStrings("PRETTY", snap.bytes);
}
test "regular: organize imports and code action show source before mutation" {
var client = try Client.init(std.testing.allocator, .{ .width = 88, .height = 7 });
defer client.deinit();
try client.handleTraceLine("open imports\nbody");
try client.handleTraceLine("language_edit zls|organize_imports|1|0|7|sorted");
try client.handleInput(" ");
try client.handleInput("l");
try client.handleInput("o");
var snap = try client.session.snapshot();
try std.testing.expectEqualStrings("sorted\nbody", snap.bytes);
var action = try Client.init(std.testing.allocator, .{ .width = 88, .height = 7 });
defer action.deinit();
try action.handleTraceLine("open abcdef");
try action.handleTraceLine("language_edit zls|code_action|1|1|4|XYZ");
try action.handleInput(" ");
try action.handleInput("l");
try action.handleInput("a");
const frame = try action.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "action:code_action:provider:zls:scope:1-4") != null);
try action.handleTraceLine("key enter");
snap = try action.session.snapshot();
try std.testing.expectEqualStrings("aXYZef", snap.bytes);
}
test "adversarial: language mutations report missing stale and overlapping providers" {
var missing = try Client.init(std.testing.allocator, .{ .width = 80, .height = 6 });
defer missing.deinit();
try missing.handleTraceLine("open abc");
try missing.handleInput(" ");
try missing.handleInput("l");
try missing.handleInput("f");
const missing_frame = try missing.render(std.testing.allocator);
defer std.testing.allocator.free(missing_frame);
try std.testing.expect(std.mem.indexOf(u8, missing_frame, "NoProvider:format") != null);
var stale = try Client.init(std.testing.allocator, .{ .width = 80, .height = 6 });
defer stale.deinit();
try stale.handleTraceLine("open abc");
try stale.handleTraceLine("language_edit zls|format|0|0|3|ABC");
try stale.handleInput(" ");
try stale.handleInput("l");
try stale.handleInput("f");
const stale_frame = try stale.render(std.testing.allocator);
defer std.testing.allocator.free(stale_frame);
try std.testing.expect(std.mem.indexOf(u8, stale_frame, "StaleEdit") != null);
var overlap = try Client.init(std.testing.allocator, .{ .width = 80, .height = 6 });
defer overlap.deinit();
try overlap.handleTraceLine("open abcdef");
try overlap.handleTraceLine("language_edit zls|format|1|0|3|AAA");
try overlap.handleTraceLine("language_edit zls|format|1|2|5|BBB");
try overlap.handleInput(" ");
try overlap.handleInput("l");
try overlap.handleInput("f");
const overlap_frame = try overlap.render(std.testing.allocator);
defer std.testing.allocator.free(overlap_frame);
try std.testing.expect(std.mem.indexOf(u8, overlap_frame, "OverlappingEdit") != null);
}
test "regular: provider edit rows parse source scope and replacement" {
const row = try lsp_mod.editRowAlloc(std.testing.allocator, "zls", .code_action, 7, 2, 5, "XYZ");
defer std.testing.allocator.free(row);
const edit = try lsp_mod.parseProviderEditRow(row);
try std.testing.expectEqualStrings("zls", edit.provider);
try std.testing.expectEqual(lsp_mod.ProviderCapability.code_action, edit.capability);
try std.testing.expectEqual(@as(u64, 7), edit.version);
try std.testing.expectEqualStrings("XYZ", edit.replacement);
const action_row = try lsp_mod.actionPanelRowAlloc(std.testing.allocator, row);
defer std.testing.allocator.free(action_row);
try std.testing.expectEqualStrings("zls", try lsp_mod.providerFromActionPanelRow(action_row));
}