diff --git a/KEYMAP.md b/KEYMAP.md index a17e469..79a1f53 100644 --- a/KEYMAP.md +++ b/KEYMAP.md @@ -392,7 +392,8 @@ Design-only and needing future implementation slices: - parameter navigation in function calls; - compact-first hover/signature cards with expandable scroll/search panels; - 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; - Insert pending-space rail, including `Space` pause then `n` to Normal; - counts/repetition grammar with direct digit and mobile repeat-rail paths; diff --git a/src/leader.zig b/src/leader.zig index 2f56732..764ddb1 100644 --- a/src/leader.zig +++ b/src/leader.zig @@ -30,6 +30,10 @@ pub const Action = union(enum) { hover, signature, expand_hover, + language_format, + language_format_policy, + language_organize_imports, + language_code_actions, diagnostics_open, diagnostics_next, diagnostics_previous, @@ -220,7 +224,11 @@ pub const Leader = struct { self.mode = .idle; if (std.mem.eql(u8, text, "h")) return .hover; 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"; return .{ .not_built = .lsp }; }, diff --git a/src/lsp.zig b/src/lsp.zig index f141799..35fc5dd 100644 --- a/src/lsp.zig +++ b/src/lsp.zig @@ -23,6 +23,8 @@ pub const Error = error{ InvalidProvider, MissingProvider, AmbiguousProvider, + StaleEdit, + OverlappingEdit, }; 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 }); } +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 { var rows = std.ArrayList([]const u8).empty; errdefer { diff --git a/src/tui.zig b/src/tui.zig index c69c908..c61d35e 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -44,6 +44,8 @@ const PanelContext = enum { none, file_picker, project_search, + language_provider_picker, + code_actions, }; const EditSnapshot = struct { @@ -155,6 +157,11 @@ pub const Client = struct { hover_rows: std.ArrayList([]u8) = .empty, signature_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_index: usize = 0, diagnostic_selected: bool = false, @@ -192,6 +199,8 @@ pub const Client = struct { self.freeOwnedClientRows(&self.hover_rows); self.freeOwnedClientRows(&self.signature_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); self.freeSnapshotStack(&self.undo_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, "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, "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, "job_profile ")) return self.openJobProfileByName(line[12..]); 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) { .file_picker => try self.openRepoPathFromPanel(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", } } @@ -1310,6 +1325,198 @@ pub const Client = struct { 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 }; fn diagnosticMatchesFilter(self: *const Client, row: []const u8) bool { @@ -1947,6 +2154,10 @@ pub const Client = struct { .hover => try self.showCompactHover(), .signature => try self.showCompactSignature(), .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_next => try self.gotoDiagnostic(.next), .diagnostics_previous => try self.gotoDiagnostic(.previous), @@ -1999,6 +2210,12 @@ pub const Client = struct { } 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(); if (snap.bytes.len > diagnostics_mod.max_file_bytes) { 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.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)); +}