Add explicit LSP edit previews

This commit is contained in:
slhx agent
2026-06-21 04:43:22 +02:00
parent 9ed3f8453c
commit 970d1bb4ec
2 changed files with 278 additions and 0 deletions
+89
View File
@@ -157,6 +157,9 @@ pub const Client = struct {
if (std.mem.startsWith(u8, line, "lsp_document_symbols ")) return self.openLspSymbols("document", line[21..]);
if (std.mem.startsWith(u8, line, "lsp_workspace_symbols ")) return self.openLspSymbols("workspace", line[22..]);
if (std.mem.eql(u8, line, "lsp_navigation_open_selected")) return self.openSelectedLspNavigation();
if (std.mem.startsWith(u8, line, "lsp_rename ")) return self.openLspWorkspaceEdit("rename", line[11..]);
if (std.mem.startsWith(u8, line, "lsp_code_actions ")) return self.openLspCodeActions(line[17..]);
if (std.mem.eql(u8, line, "lsp_edit_apply_selected")) return self.applySelectedLspEdit();
if (std.mem.startsWith(u8, line, "panel_open ")) return self.applyProtocolCommand(line);
if (std.mem.startsWith(u8, line, "list_open ")) return self.applyProtocolCommand(line);
if (std.mem.startsWith(u8, line, "list_filter ")) return self.applyProtocolCommand(line);
@@ -481,6 +484,30 @@ pub const Client = struct {
try self.jumpCurrentBufferToLspLocation(location);
}
fn openLspWorkspaceEdit(self: *Client, kind: []const u8, payload: []const u8) !void {
const rows = lsp_mod.workspaceEditRowsAlloc(self.allocator, kind, payload) catch return Error.ProtocolRejected;
defer freeOwnedRows(self.allocator, rows);
try self.session.openListPanel("lsp-edit-preview", rows);
self.message = null;
}
fn openLspCodeActions(self: *Client, payload: []const u8) !void {
const rows = lsp_mod.codeActionRowsAlloc(self.allocator, payload) catch return Error.ProtocolRejected;
defer freeOwnedRows(self.allocator, rows);
try self.session.openListPanel("lsp-code-actions", rows);
self.message = null;
}
fn applySelectedLspEdit(self: *Client) !void {
const selected = self.session.selectListPanel() catch return Error.ProtocolRejected;
const snap = try self.session.snapshot();
const applied = lsp_mod.applyEditRowAlloc(self.allocator, snap.bytes, selected) catch return Error.ProtocolRejected;
defer self.allocator.free(applied.bytes);
try self.session.openFixtureAt(applied.bytes, applied.cursor);
try self.session.closePanel();
self.message = null;
}
fn jumpCurrentBufferToLspLocation(self: *Client, location: lsp_mod.DiagnosticLocation) !void {
const snap = try self.session.snapshot();
const bytes = try self.allocator.dupe(u8, snap.bytes);
@@ -1769,3 +1796,65 @@ test "adversarial: lsp navigation failures do not corrupt buffer" {
const snap = try client.session.snapshot();
try std.testing.expectEqualStrings("safe", snap.bytes);
}
test "regular: lsp rename preview applies selected edit after confirmation" {
var client = try Client.init(std.testing.allocator, .{ .width = 112, .height = 6 });
defer client.deinit();
try client.handleTraceLine("open pub old = old;");
const payload =
\\{"jsonrpc":"2.0","id":6,"result":{"changes":{"file:///tmp/main.zig":[{"range":{"start":{"line":0,"character":4},"end":{"line":0,"character":7}},"newText":"renamed"}]}}}
;
const command = try std.fmt.allocPrint(std.testing.allocator, "lsp_rename {s}", .{payload});
defer std.testing.allocator.free(command);
try client.handleTraceLine(command);
{
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp-edit-preview") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:edit:rename:1:5:1:8:72656e616d6564") != null);
}
try client.handleTraceLine("lsp_edit_apply_selected");
const snap = try client.session.snapshot();
try std.testing.expectEqualStrings("pub renamed = old;", snap.bytes);
try std.testing.expectEqual(@as(usize, 11), snap.cursor_byte);
}
test "regular: lsp code action preview applies selected workspace edit" {
var client = try Client.init(std.testing.allocator, .{ .width = 112, .height = 6 });
defer client.deinit();
try client.handleTraceLine("open var x = 1;");
const payload =
\\{"jsonrpc":"2.0","id":7,"result":[{"title":"replace with const","kind":"quickfix","edit":{"changes":{"file:///tmp/main.zig":[{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":3}},"newText":"const"}]}}}]}
;
const command = try std.fmt.allocPrint(std.testing.allocator, "lsp_code_actions {s}", .{payload});
defer std.testing.allocator.free(command);
try client.handleTraceLine(command);
{
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp-code-actions") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "replace_with_const") != null);
}
try client.handleTraceLine("lsp_edit_apply_selected");
const snap = try client.session.snapshot();
try std.testing.expectEqualStrings("const x = 1;", snap.bytes);
}
test "adversarial: lsp edit failures do not corrupt current buffer" {
var client = try Client.init(std.testing.allocator, .{ .width = 80, .height = 5 });
defer client.deinit();
try client.handleTraceLine("open safe");
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("lsp_rename not-json"));
const command_only_payload =
\\{"jsonrpc":"2.0","id":7,"result":[{"title":"command only","command":{"title":"noop","command":"noop"}}]}
;
const command = try std.fmt.allocPrint(std.testing.allocator, "lsp_code_actions {s}", .{command_only_payload});
defer std.testing.allocator.free(command);
try client.handleTraceLine(command);
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("lsp_edit_apply_selected"));
const snap = try client.session.snapshot();
try std.testing.expectEqualStrings("safe", snap.bytes);
}