Add explicit LSP edit previews
This commit is contained in:
+189
@@ -15,6 +15,8 @@ pub const Error = error{
|
|||||||
InvalidDiagnosticRow,
|
InvalidDiagnosticRow,
|
||||||
InvalidNavigation,
|
InvalidNavigation,
|
||||||
InvalidNavigationRow,
|
InvalidNavigationRow,
|
||||||
|
InvalidEdit,
|
||||||
|
InvalidEditRow,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Document = struct {
|
pub const Document = struct {
|
||||||
@@ -167,6 +169,57 @@ pub fn navigationLocationFromRow(row: []const u8) !DiagnosticLocation {
|
|||||||
return Error.InvalidNavigationRow;
|
return Error.InvalidNavigationRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn workspaceEditRowsAlloc(allocator: std.mem.Allocator, kind: []const u8, payload: []const u8) ![][]const u8 {
|
||||||
|
try validateEditKind(kind);
|
||||||
|
var parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch return Error.InvalidEdit;
|
||||||
|
defer parsed.deinit();
|
||||||
|
const root = parsed.value;
|
||||||
|
if (root != .object) return Error.InvalidEdit;
|
||||||
|
const result = objectGet(root, "result") orelse return Error.InvalidEdit;
|
||||||
|
if (result == .null) return singleEditNoneRow(allocator, kind);
|
||||||
|
return editRowsFromWorkspaceEditValue(allocator, kind, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codeActionRowsAlloc(allocator: std.mem.Allocator, payload: []const u8) ![][]const u8 {
|
||||||
|
var parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch return Error.InvalidEdit;
|
||||||
|
defer parsed.deinit();
|
||||||
|
const root = parsed.value;
|
||||||
|
if (root != .object) return Error.InvalidEdit;
|
||||||
|
const result = objectGet(root, "result") orelse return Error.InvalidEdit;
|
||||||
|
if (result != .array) return Error.InvalidEdit;
|
||||||
|
var rows = std.ArrayList([]const u8).empty;
|
||||||
|
errdefer {
|
||||||
|
for (rows.items) |row| allocator.free(row);
|
||||||
|
rows.deinit(allocator);
|
||||||
|
}
|
||||||
|
for (result.array.items) |action| {
|
||||||
|
const title = stringGetValue(action, "title") orelse return Error.InvalidEdit;
|
||||||
|
const edit = objectGet(action, "edit") orelse {
|
||||||
|
const title_preview = try previewAlloc(allocator, title);
|
||||||
|
defer allocator.free(title_preview);
|
||||||
|
try rows.append(allocator, try std.fmt.allocPrint(allocator, "lsp:action:none:{s}", .{title_preview}));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
try appendRowsFromWorkspaceEditValue(allocator, &rows, "action", edit, title);
|
||||||
|
}
|
||||||
|
if (rows.items.len == 0) try rows.append(allocator, try allocator.dupe(u8, "lsp:action:none"));
|
||||||
|
return rows.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn applyEditRowAlloc(allocator: std.mem.Allocator, content: []const u8, row: []const u8) !struct { bytes: []u8, cursor: usize } {
|
||||||
|
const parsed = try parseEditRow(allocator, row);
|
||||||
|
defer allocator.free(parsed.new_text);
|
||||||
|
const start = byteOffsetForLineColumn(content, parsed.start_line, parsed.start_character) catch return Error.InvalidEditRow;
|
||||||
|
const end = byteOffsetForLineColumn(content, parsed.end_line, parsed.end_character) catch return Error.InvalidEditRow;
|
||||||
|
if (end < start) return Error.InvalidEditRow;
|
||||||
|
var out = std.ArrayList(u8).empty;
|
||||||
|
errdefer out.deinit(allocator);
|
||||||
|
try out.appendSlice(allocator, content[0..start]);
|
||||||
|
try out.appendSlice(allocator, parsed.new_text);
|
||||||
|
try out.appendSlice(allocator, content[end..]);
|
||||||
|
return .{ .bytes = try out.toOwnedSlice(allocator), .cursor = start + parsed.new_text.len };
|
||||||
|
}
|
||||||
|
|
||||||
pub fn runDocumentSyncRowsAlloc(
|
pub fn runDocumentSyncRowsAlloc(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
io: std.Io,
|
io: std.Io,
|
||||||
@@ -257,6 +310,108 @@ pub fn transcriptAlloc(allocator: std.mem.Allocator, document: Document) ![]u8 {
|
|||||||
return out.toOwnedSlice(allocator);
|
return out.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validateEditKind(kind: []const u8) !void {
|
||||||
|
if (std.mem.eql(u8, kind, "rename") or std.mem.eql(u8, kind, "edit")) return;
|
||||||
|
return Error.InvalidEdit;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editRowsFromWorkspaceEditValue(allocator: std.mem.Allocator, kind: []const u8, edit: std.json.Value) ![][]const u8 {
|
||||||
|
var rows = std.ArrayList([]const u8).empty;
|
||||||
|
errdefer {
|
||||||
|
for (rows.items) |row| allocator.free(row);
|
||||||
|
rows.deinit(allocator);
|
||||||
|
}
|
||||||
|
try appendRowsFromWorkspaceEditValue(allocator, &rows, kind, edit, kind);
|
||||||
|
if (rows.items.len == 0) try rows.append(allocator, try std.fmt.allocPrint(allocator, "lsp:edit:{s}:none", .{kind}));
|
||||||
|
return rows.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn appendRowsFromWorkspaceEditValue(allocator: std.mem.Allocator, rows: *std.ArrayList([]const u8), kind: []const u8, edit: std.json.Value, label: []const u8) !void {
|
||||||
|
const changes = objectGet(edit, "changes") orelse return Error.InvalidEdit;
|
||||||
|
if (changes != .object) return Error.InvalidEdit;
|
||||||
|
var it = changes.object.iterator();
|
||||||
|
while (it.next()) |entry| {
|
||||||
|
const uri = entry.key_ptr.*;
|
||||||
|
const edits = entry.value_ptr.*;
|
||||||
|
if (edits != .array) return Error.InvalidEdit;
|
||||||
|
for (edits.array.items) |text_edit| try appendEditRow(allocator, rows, kind, uri, text_edit, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn appendEditRow(allocator: std.mem.Allocator, rows: *std.ArrayList([]const u8), kind: []const u8, uri: []const u8, text_edit: std.json.Value, label: []const u8) !void {
|
||||||
|
const range = objectGet(text_edit, "range") orelse return Error.InvalidEdit;
|
||||||
|
const start_value = objectGet(range, "start") orelse return Error.InvalidEdit;
|
||||||
|
const end_value = objectGet(range, "end") orelse return Error.InvalidEdit;
|
||||||
|
const start_line = unsignedGet(start_value, "line") orelse return Error.InvalidEdit;
|
||||||
|
const start_character = unsignedGet(start_value, "character") orelse return Error.InvalidEdit;
|
||||||
|
const end_line = unsignedGet(end_value, "line") orelse return Error.InvalidEdit;
|
||||||
|
const end_character = unsignedGet(end_value, "character") orelse return Error.InvalidEdit;
|
||||||
|
const new_text = stringGetValue(text_edit, "newText") orelse return Error.InvalidEdit;
|
||||||
|
const encoded = try hexEncodeAlloc(allocator, new_text);
|
||||||
|
defer allocator.free(encoded);
|
||||||
|
const uri_preview = try previewAlloc(allocator, uri);
|
||||||
|
defer allocator.free(uri_preview);
|
||||||
|
const label_preview = try previewAlloc(allocator, label);
|
||||||
|
defer allocator.free(label_preview);
|
||||||
|
try rows.append(allocator, try std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
"lsp:edit:{s}:{d}:{d}:{d}:{d}:{s}:{s}:{s}",
|
||||||
|
.{ kind, start_line + 1, start_character + 1, end_line + 1, end_character + 1, encoded, uri_preview, label_preview },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn singleEditNoneRow(allocator: std.mem.Allocator, kind: []const u8) ![][]const u8 {
|
||||||
|
const row = try std.fmt.allocPrint(allocator, "lsp:edit:{s}:none", .{kind});
|
||||||
|
errdefer allocator.free(row);
|
||||||
|
const rows = try allocator.alloc([]const u8, 1);
|
||||||
|
rows[0] = row;
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseEditRow(allocator: std.mem.Allocator, row: []const u8) !struct { start_line: usize, start_character: usize, end_line: usize, end_character: usize, new_text: []u8 } {
|
||||||
|
var parts = std.mem.splitScalar(u8, row, ':');
|
||||||
|
if (!std.mem.eql(u8, parts.next() orelse return Error.InvalidEditRow, "lsp")) return Error.InvalidEditRow;
|
||||||
|
if (!std.mem.eql(u8, parts.next() orelse return Error.InvalidEditRow, "edit")) return Error.InvalidEditRow;
|
||||||
|
_ = parts.next() orelse return Error.InvalidEditRow; // kind
|
||||||
|
const start_line = std.fmt.parseUnsigned(usize, parts.next() orelse return Error.InvalidEditRow, 10) catch return Error.InvalidEditRow;
|
||||||
|
const start_character = std.fmt.parseUnsigned(usize, parts.next() orelse return Error.InvalidEditRow, 10) catch return Error.InvalidEditRow;
|
||||||
|
const end_line = std.fmt.parseUnsigned(usize, parts.next() orelse return Error.InvalidEditRow, 10) catch return Error.InvalidEditRow;
|
||||||
|
const end_character = std.fmt.parseUnsigned(usize, parts.next() orelse return Error.InvalidEditRow, 10) catch return Error.InvalidEditRow;
|
||||||
|
const encoded = parts.next() orelse return Error.InvalidEditRow;
|
||||||
|
if (start_line == 0 or start_character == 0 or end_line == 0 or end_character == 0) return Error.InvalidEditRow;
|
||||||
|
return .{ .start_line = start_line, .start_character = start_character, .end_line = end_line, .end_character = end_character, .new_text = try hexDecodeAlloc(allocator, encoded) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hexEncodeAlloc(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
|
||||||
|
const encoded = try allocator.alloc(u8, bytes.len * 2);
|
||||||
|
const alphabet = "0123456789abcdef";
|
||||||
|
for (bytes, 0..) |byte, i| {
|
||||||
|
encoded[i * 2] = alphabet[byte >> 4];
|
||||||
|
encoded[i * 2 + 1] = alphabet[byte & 0x0f];
|
||||||
|
}
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hexDecodeAlloc(allocator: std.mem.Allocator, encoded: []const u8) ![]u8 {
|
||||||
|
if (encoded.len % 2 != 0) return Error.InvalidEditRow;
|
||||||
|
const bytes = try allocator.alloc(u8, encoded.len / 2);
|
||||||
|
errdefer allocator.free(bytes);
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < bytes.len) : (i += 1) {
|
||||||
|
const hi = hexValue(encoded[i * 2]) orelse return Error.InvalidEditRow;
|
||||||
|
const lo = hexValue(encoded[i * 2 + 1]) orelse return Error.InvalidEditRow;
|
||||||
|
bytes[i] = (hi << 4) | lo;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hexValue(byte: u8) ?u8 {
|
||||||
|
if (byte >= '0' and byte <= '9') return byte - '0';
|
||||||
|
if (byte >= 'a' and byte <= 'f') return byte - 'a' + 10;
|
||||||
|
if (byte >= 'A' and byte <= 'F') return byte - 'A' + 10;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
fn validateNavKind(kind: []const u8) !void {
|
fn validateNavKind(kind: []const u8) !void {
|
||||||
if (std.mem.eql(u8, kind, "definition") or std.mem.eql(u8, kind, "references")) return;
|
if (std.mem.eql(u8, kind, "definition") or std.mem.eql(u8, kind, "references")) return;
|
||||||
return Error.InvalidNavigation;
|
return Error.InvalidNavigation;
|
||||||
@@ -553,3 +708,37 @@ test "adversarial: invalid navigation payloads and rows are rejected" {
|
|||||||
try std.testing.expectError(Error.InvalidNavigationRow, navigationLocationFromRow("lsp:nav:definition:none"));
|
try std.testing.expectError(Error.InvalidNavigationRow, navigationLocationFromRow("lsp:nav:definition:none"));
|
||||||
try std.testing.expectError(Error.InvalidNavigationRow, navigationLocationFromRow("lsp:symbol:document:none"));
|
try std.testing.expectError(Error.InvalidNavigationRow, navigationLocationFromRow("lsp:symbol:document:none"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "regular: rename workspace edit row applies to current buffer" {
|
||||||
|
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 rows = try workspaceEditRowsAlloc(std.testing.allocator, "rename", payload);
|
||||||
|
defer freeRows(std.testing.allocator, rows);
|
||||||
|
try std.testing.expectEqualStrings("lsp:edit:rename:1:5:1:8:72656e616d6564:file_///tmp/main.zig:rename", rows[0]);
|
||||||
|
const applied = try applyEditRowAlloc(std.testing.allocator, "pub old = 1;", rows[0]);
|
||||||
|
defer std.testing.allocator.free(applied.bytes);
|
||||||
|
try std.testing.expectEqualStrings("pub renamed = 1;", applied.bytes);
|
||||||
|
try std.testing.expectEqual(@as(usize, 11), applied.cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: code action edit rows preserve title and apply replacement" {
|
||||||
|
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"}]}}},{"title":"command only","command":{"title":"noop","command":"noop"}}]}
|
||||||
|
;
|
||||||
|
const rows = try codeActionRowsAlloc(std.testing.allocator, payload);
|
||||||
|
defer freeRows(std.testing.allocator, rows);
|
||||||
|
try std.testing.expectEqualStrings("lsp:edit:action:1:1:1:4:636f6e7374:file_///tmp/main.zig:replace_with_const", rows[0]);
|
||||||
|
try std.testing.expectEqualStrings("lsp:action:none:command_only", rows[1]);
|
||||||
|
const applied = try applyEditRowAlloc(std.testing.allocator, "var x = 1;", rows[0]);
|
||||||
|
defer std.testing.allocator.free(applied.bytes);
|
||||||
|
try std.testing.expectEqualStrings("const x = 1;", applied.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: invalid edits and non-edit rows are rejected" {
|
||||||
|
try std.testing.expectError(Error.InvalidEdit, workspaceEditRowsAlloc(std.testing.allocator, "hover", "{}"));
|
||||||
|
try std.testing.expectError(Error.InvalidEdit, workspaceEditRowsAlloc(std.testing.allocator, "rename", "{}"));
|
||||||
|
try std.testing.expectError(Error.InvalidEdit, codeActionRowsAlloc(std.testing.allocator, "{}"));
|
||||||
|
try std.testing.expectError(Error.InvalidEditRow, applyEditRowAlloc(std.testing.allocator, "safe", "lsp:action:none:title"));
|
||||||
|
try std.testing.expectError(Error.InvalidEditRow, applyEditRowAlloc(std.testing.allocator, "safe", "lsp:edit:rename:1:1:9:1:78:file:title"));
|
||||||
|
}
|
||||||
|
|||||||
+89
@@ -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_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.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.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, "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_open ")) return self.applyProtocolCommand(line);
|
||||||
if (std.mem.startsWith(u8, line, "list_filter ")) 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);
|
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 {
|
fn jumpCurrentBufferToLspLocation(self: *Client, location: lsp_mod.DiagnosticLocation) !void {
|
||||||
const snap = try self.session.snapshot();
|
const snap = try self.session.snapshot();
|
||||||
const bytes = try self.allocator.dupe(u8, snap.bytes);
|
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();
|
const snap = try client.session.snapshot();
|
||||||
try std.testing.expectEqualStrings("safe", snap.bytes);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user