Add LSP navigation and symbol panels
This commit is contained in:
+178
@@ -13,6 +13,8 @@ pub const Error = error{
|
|||||||
InvalidDocument,
|
InvalidDocument,
|
||||||
InvalidDiagnostics,
|
InvalidDiagnostics,
|
||||||
InvalidDiagnosticRow,
|
InvalidDiagnosticRow,
|
||||||
|
InvalidNavigation,
|
||||||
|
InvalidNavigationRow,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Document = struct {
|
pub const Document = struct {
|
||||||
@@ -97,6 +99,74 @@ pub fn byteOffsetForLineColumn(content: []const u8, line: usize, column: usize)
|
|||||||
return Error.InvalidDiagnosticRow;
|
return Error.InvalidDiagnosticRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn locationRowsAlloc(allocator: std.mem.Allocator, kind: []const u8, payload: []const u8) ![][]const u8 {
|
||||||
|
try validateNavKind(kind);
|
||||||
|
var parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch return Error.InvalidNavigation;
|
||||||
|
defer parsed.deinit();
|
||||||
|
const root = parsed.value;
|
||||||
|
if (root != .object) return Error.InvalidNavigation;
|
||||||
|
const result = objectGet(root, "result") orelse return Error.InvalidNavigation;
|
||||||
|
|
||||||
|
var rows = std.ArrayList([]const u8).empty;
|
||||||
|
errdefer {
|
||||||
|
for (rows.items) |row| allocator.free(row);
|
||||||
|
rows.deinit(allocator);
|
||||||
|
}
|
||||||
|
if (result == .null) {
|
||||||
|
try rows.append(allocator, try std.fmt.allocPrint(allocator, "lsp:nav:{s}:none", .{kind}));
|
||||||
|
return rows.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
if (result == .array) {
|
||||||
|
for (result.array.items) |location| try appendLocationRow(allocator, &rows, kind, location, "location");
|
||||||
|
} else {
|
||||||
|
try appendLocationRow(allocator, &rows, kind, result, "location");
|
||||||
|
}
|
||||||
|
if (rows.items.len == 0) try rows.append(allocator, try std.fmt.allocPrint(allocator, "lsp:nav:{s}:none", .{kind}));
|
||||||
|
return rows.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symbolRowsAlloc(allocator: std.mem.Allocator, scope: []const u8, payload: []const u8) ![][]const u8 {
|
||||||
|
try validateSymbolScope(scope);
|
||||||
|
var parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch return Error.InvalidNavigation;
|
||||||
|
defer parsed.deinit();
|
||||||
|
const root = parsed.value;
|
||||||
|
if (root != .object) return Error.InvalidNavigation;
|
||||||
|
const result = objectGet(root, "result") orelse return Error.InvalidNavigation;
|
||||||
|
if (result != .array) return Error.InvalidNavigation;
|
||||||
|
|
||||||
|
var rows = std.ArrayList([]const u8).empty;
|
||||||
|
errdefer {
|
||||||
|
for (rows.items) |row| allocator.free(row);
|
||||||
|
rows.deinit(allocator);
|
||||||
|
}
|
||||||
|
for (result.array.items) |symbol_value| try appendSymbolRow(allocator, &rows, scope, symbol_value);
|
||||||
|
if (rows.items.len == 0) try rows.append(allocator, try std.fmt.allocPrint(allocator, "lsp:symbol:{s}:none", .{scope}));
|
||||||
|
return rows.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn navigationLocationFromRow(row: []const u8) !DiagnosticLocation {
|
||||||
|
var parts = std.mem.splitScalar(u8, row, ':');
|
||||||
|
const lsp = parts.next() orelse return Error.InvalidNavigationRow;
|
||||||
|
if (!std.mem.eql(u8, lsp, "lsp")) return Error.InvalidNavigationRow;
|
||||||
|
const family = parts.next() orelse return Error.InvalidNavigationRow;
|
||||||
|
if (std.mem.eql(u8, family, "nav")) {
|
||||||
|
_ = parts.next() orelse return Error.InvalidNavigationRow; // kind
|
||||||
|
const line_token = parts.next() orelse return Error.InvalidNavigationRow;
|
||||||
|
const character_token = parts.next() orelse return Error.InvalidNavigationRow;
|
||||||
|
const uri = parts.next() orelse return Error.InvalidNavigationRow;
|
||||||
|
return parseLocationTokens(uri, line_token, character_token);
|
||||||
|
}
|
||||||
|
if (std.mem.eql(u8, family, "symbol")) {
|
||||||
|
_ = parts.next() orelse return Error.InvalidNavigationRow; // scope
|
||||||
|
_ = parts.next() orelse return Error.InvalidNavigationRow; // symbol kind
|
||||||
|
const line_token = parts.next() orelse return Error.InvalidNavigationRow;
|
||||||
|
const character_token = parts.next() orelse return Error.InvalidNavigationRow;
|
||||||
|
const uri = parts.next() orelse return Error.InvalidNavigationRow;
|
||||||
|
return parseLocationTokens(uri, line_token, character_token);
|
||||||
|
}
|
||||||
|
return Error.InvalidNavigationRow;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn runDocumentSyncRowsAlloc(
|
pub fn runDocumentSyncRowsAlloc(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
io: std.Io,
|
io: std.Io,
|
||||||
@@ -187,11 +257,73 @@ pub fn transcriptAlloc(allocator: std.mem.Allocator, document: Document) ![]u8 {
|
|||||||
return out.toOwnedSlice(allocator);
|
return out.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validateNavKind(kind: []const u8) !void {
|
||||||
|
if (std.mem.eql(u8, kind, "definition") or std.mem.eql(u8, kind, "references")) return;
|
||||||
|
return Error.InvalidNavigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validateSymbolScope(scope: []const u8) !void {
|
||||||
|
if (std.mem.eql(u8, scope, "document") or std.mem.eql(u8, scope, "workspace")) return;
|
||||||
|
return Error.InvalidNavigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn appendLocationRow(allocator: std.mem.Allocator, rows: *std.ArrayList([]const u8), kind: []const u8, location: std.json.Value, fallback_label: []const u8) !void {
|
||||||
|
const uri = stringGetValue(location, "uri") orelse return Error.InvalidNavigation;
|
||||||
|
const range = objectGet(location, "range") orelse return Error.InvalidNavigation;
|
||||||
|
const start = objectGet(range, "start") orelse return Error.InvalidNavigation;
|
||||||
|
const line = unsignedGet(start, "line") orelse return Error.InvalidNavigation;
|
||||||
|
const character = unsignedGet(start, "character") orelse return Error.InvalidNavigation;
|
||||||
|
const uri_preview = try previewAlloc(allocator, uri);
|
||||||
|
defer allocator.free(uri_preview);
|
||||||
|
const label_preview = try previewAlloc(allocator, fallback_label);
|
||||||
|
defer allocator.free(label_preview);
|
||||||
|
try rows.append(allocator, try std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
"lsp:nav:{s}:{d}:{d}:{s}:{s}",
|
||||||
|
.{ kind, line + 1, character + 1, uri_preview, label_preview },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn appendSymbolRow(allocator: std.mem.Allocator, rows: *std.ArrayList([]const u8), scope: []const u8, symbol_value: std.json.Value) !void {
|
||||||
|
const name = stringGetValue(symbol_value, "name") orelse return Error.InvalidNavigation;
|
||||||
|
const kind = unsignedGet(symbol_value, "kind") orelse 0;
|
||||||
|
const location = if (std.mem.eql(u8, scope, "workspace")) objectGet(symbol_value, "location") orelse return Error.InvalidNavigation else symbol_value;
|
||||||
|
const uri = stringGetValue(location, "uri") orelse "current";
|
||||||
|
const range = if (objectGet(symbol_value, "selectionRange")) |selection_range| selection_range else objectGet(location, "range") orelse return Error.InvalidNavigation;
|
||||||
|
const start = objectGet(range, "start") orelse return Error.InvalidNavigation;
|
||||||
|
const line = unsignedGet(start, "line") orelse return Error.InvalidNavigation;
|
||||||
|
const character = unsignedGet(start, "character") orelse return Error.InvalidNavigation;
|
||||||
|
const uri_preview = try previewAlloc(allocator, uri);
|
||||||
|
defer allocator.free(uri_preview);
|
||||||
|
const name_preview = try previewAlloc(allocator, name);
|
||||||
|
defer allocator.free(name_preview);
|
||||||
|
try rows.append(allocator, try std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
"lsp:symbol:{s}:kind_{d}:{d}:{d}:{s}:{s}",
|
||||||
|
.{ scope, kind, line + 1, character + 1, uri_preview, name_preview },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseLocationTokens(uri: []const u8, line_token: []const u8, character_token: []const u8) !DiagnosticLocation {
|
||||||
|
const line = std.fmt.parseUnsigned(usize, line_token, 10) catch return Error.InvalidNavigationRow;
|
||||||
|
const character = std.fmt.parseUnsigned(usize, character_token, 10) catch return Error.InvalidNavigationRow;
|
||||||
|
if (line == 0 or character == 0 or uri.len == 0) return Error.InvalidNavigationRow;
|
||||||
|
return .{ .uri = uri, .line = line, .character = character };
|
||||||
|
}
|
||||||
|
|
||||||
fn objectGet(value: std.json.Value, key: []const u8) ?std.json.Value {
|
fn objectGet(value: std.json.Value, key: []const u8) ?std.json.Value {
|
||||||
if (value != .object) return null;
|
if (value != .object) return null;
|
||||||
return value.object.get(key);
|
return value.object.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stringGetValue(value: std.json.Value, key: []const u8) ?[]const u8 {
|
||||||
|
const child = objectGet(value, key) orelse return null;
|
||||||
|
return switch (child) {
|
||||||
|
.string => |text| text,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn stringGet(value: std.json.Value, key: []const u8) ?[]const u8 {
|
fn stringGet(value: std.json.Value, key: []const u8) ?[]const u8 {
|
||||||
const child = objectGet(value, key) orelse return null;
|
const child = objectGet(value, key) orelse return null;
|
||||||
return switch (child) {
|
return switch (child) {
|
||||||
@@ -375,3 +507,49 @@ test "adversarial: bad diagnostics and rows are rejected" {
|
|||||||
try std.testing.expectError(Error.InvalidDiagnosticRow, diagnosticLocationFromRow("lsp:diagnostics:count_1"));
|
try std.testing.expectError(Error.InvalidDiagnosticRow, diagnosticLocationFromRow("lsp:diagnostics:count_1"));
|
||||||
try std.testing.expectError(Error.InvalidDiagnosticRow, byteOffsetForLineColumn("short", 9, 1));
|
try std.testing.expectError(Error.InvalidDiagnosticRow, byteOffsetForLineColumn("short", 9, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "regular: definition and reference locations become navigation rows" {
|
||||||
|
const definition_payload =
|
||||||
|
\\{"jsonrpc":"2.0","id":2,"result":{"uri":"file:///repo/src/main.zig","range":{"start":{"line":1,"character":4},"end":{"line":1,"character":8}}}}
|
||||||
|
;
|
||||||
|
const definition_rows = try locationRowsAlloc(std.testing.allocator, "definition", definition_payload);
|
||||||
|
defer freeRows(std.testing.allocator, definition_rows);
|
||||||
|
try std.testing.expectEqualStrings("lsp:nav:definition:2:5:file_///repo/src/main.zig:location", definition_rows[0]);
|
||||||
|
const definition_location = try navigationLocationFromRow(definition_rows[0]);
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), definition_location.line);
|
||||||
|
try std.testing.expectEqual(@as(usize, 5), definition_location.character);
|
||||||
|
|
||||||
|
const references_payload =
|
||||||
|
\\{"jsonrpc":"2.0","id":3,"result":[{"uri":"file:///repo/src/main.zig","range":{"start":{"line":0,"character":0},"end":{"line":0,"character":3}}},{"uri":"file:///repo/src/lib.zig","range":{"start":{"line":2,"character":1},"end":{"line":2,"character":4}}}]}
|
||||||
|
;
|
||||||
|
const reference_rows = try locationRowsAlloc(std.testing.allocator, "references", references_payload);
|
||||||
|
defer freeRows(std.testing.allocator, reference_rows);
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), reference_rows.len);
|
||||||
|
try std.testing.expectEqualStrings("lsp:nav:references:1:1:file_///repo/src/main.zig:location", reference_rows[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: document and workspace symbols become jumpable rows" {
|
||||||
|
const document_payload =
|
||||||
|
\\{"jsonrpc":"2.0","id":4,"result":[{"name":"main","kind":12,"range":{"start":{"line":0,"character":0},"end":{"line":3,"character":1}},"selectionRange":{"start":{"line":1,"character":4},"end":{"line":1,"character":8}}}]}
|
||||||
|
;
|
||||||
|
const document_rows = try symbolRowsAlloc(std.testing.allocator, "document", document_payload);
|
||||||
|
defer freeRows(std.testing.allocator, document_rows);
|
||||||
|
try std.testing.expectEqualStrings("lsp:symbol:document:kind_12:2:5:current:main", document_rows[0]);
|
||||||
|
const document_location = try navigationLocationFromRow(document_rows[0]);
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), document_location.line);
|
||||||
|
|
||||||
|
const workspace_payload =
|
||||||
|
\\{"jsonrpc":"2.0","id":5,"result":[{"name":"helper","kind":12,"location":{"uri":"file:///repo/src/lib.zig","range":{"start":{"line":4,"character":2},"end":{"line":4,"character":8}}}}]}
|
||||||
|
;
|
||||||
|
const workspace_rows = try symbolRowsAlloc(std.testing.allocator, "workspace", workspace_payload);
|
||||||
|
defer freeRows(std.testing.allocator, workspace_rows);
|
||||||
|
try std.testing.expectEqualStrings("lsp:symbol:workspace:kind_12:5:3:file_///repo/src/lib.zig:helper", workspace_rows[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: invalid navigation payloads and rows are rejected" {
|
||||||
|
try std.testing.expectError(Error.InvalidNavigation, locationRowsAlloc(std.testing.allocator, "hover", "{}"));
|
||||||
|
try std.testing.expectError(Error.InvalidNavigation, locationRowsAlloc(std.testing.allocator, "definition", "{}"));
|
||||||
|
try std.testing.expectError(Error.InvalidNavigation, symbolRowsAlloc(std.testing.allocator, "project", "{}"));
|
||||||
|
try std.testing.expectError(Error.InvalidNavigationRow, navigationLocationFromRow("lsp:nav:definition:none"));
|
||||||
|
try std.testing.expectError(Error.InvalidNavigationRow, navigationLocationFromRow("lsp:symbol:document:none"));
|
||||||
|
}
|
||||||
|
|||||||
+116
@@ -152,6 +152,11 @@ pub const Client = struct {
|
|||||||
if (std.mem.startsWith(u8, line, "lsp_sync ")) return self.openLspSync(line[9..]);
|
if (std.mem.startsWith(u8, line, "lsp_sync ")) return self.openLspSync(line[9..]);
|
||||||
if (std.mem.startsWith(u8, line, "lsp_diagnostics ")) return self.openLspDiagnostics(line[16..]);
|
if (std.mem.startsWith(u8, line, "lsp_diagnostics ")) return self.openLspDiagnostics(line[16..]);
|
||||||
if (std.mem.eql(u8, line, "lsp_diagnostics_open_selected")) return self.openSelectedLspDiagnostic();
|
if (std.mem.eql(u8, line, "lsp_diagnostics_open_selected")) return self.openSelectedLspDiagnostic();
|
||||||
|
if (std.mem.startsWith(u8, line, "lsp_definition ")) return self.openLspLocations("definition", line[15..]);
|
||||||
|
if (std.mem.startsWith(u8, line, "lsp_references ")) return self.openLspLocations("references", line[15..]);
|
||||||
|
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, "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);
|
||||||
@@ -451,6 +456,32 @@ pub const Client = struct {
|
|||||||
fn openSelectedLspDiagnostic(self: *Client) !void {
|
fn openSelectedLspDiagnostic(self: *Client) !void {
|
||||||
const selected = self.session.selectListPanel() catch return Error.ProtocolRejected;
|
const selected = self.session.selectListPanel() catch return Error.ProtocolRejected;
|
||||||
const location = lsp_mod.diagnosticLocationFromRow(selected) catch return Error.ProtocolRejected;
|
const location = lsp_mod.diagnosticLocationFromRow(selected) catch return Error.ProtocolRejected;
|
||||||
|
try self.jumpCurrentBufferToLspLocation(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openLspLocations(self: *Client, kind: []const u8, payload: []const u8) !void {
|
||||||
|
const rows = lsp_mod.locationRowsAlloc(self.allocator, kind, payload) catch return Error.ProtocolRejected;
|
||||||
|
defer freeOwnedRows(self.allocator, rows);
|
||||||
|
const title = if (std.mem.eql(u8, kind, "definition")) "lsp-definition" else "lsp-references";
|
||||||
|
try self.session.openListPanel(title, rows);
|
||||||
|
self.message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openLspSymbols(self: *Client, scope: []const u8, payload: []const u8) !void {
|
||||||
|
const rows = lsp_mod.symbolRowsAlloc(self.allocator, scope, payload) catch return Error.ProtocolRejected;
|
||||||
|
defer freeOwnedRows(self.allocator, rows);
|
||||||
|
const title = if (std.mem.eql(u8, scope, "document")) "lsp-document-symbols" else "lsp-workspace-symbols";
|
||||||
|
try self.session.openListPanel(title, rows);
|
||||||
|
self.message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openSelectedLspNavigation(self: *Client) !void {
|
||||||
|
const selected = self.session.selectListPanel() catch return Error.ProtocolRejected;
|
||||||
|
const location = lsp_mod.navigationLocationFromRow(selected) catch return Error.ProtocolRejected;
|
||||||
|
try self.jumpCurrentBufferToLspLocation(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
defer self.allocator.free(bytes);
|
defer self.allocator.free(bytes);
|
||||||
@@ -1653,3 +1684,88 @@ test "adversarial: malformed diagnostics and unselected summary row do not corru
|
|||||||
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 definition panel selects and jumps current buffer" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 96, .height = 6 });
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
try client.handleTraceLine("open one abcTARGET");
|
||||||
|
const payload =
|
||||||
|
\\{"jsonrpc":"2.0","id":2,"result":{"uri":"file:///tmp/main.zig","range":{"start":{"line":0,"character":4},"end":{"line":0,"character":7}}}}
|
||||||
|
;
|
||||||
|
const command = try std.fmt.allocPrint(std.testing.allocator, "lsp_definition {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-definition") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:nav:definition:1:5:file_///tmp/main.zig:location") != null);
|
||||||
|
}
|
||||||
|
try client.handleTraceLine("lsp_navigation_open_selected");
|
||||||
|
const snap = try client.session.snapshot();
|
||||||
|
try std.testing.expectEqual(@as(usize, 4), snap.cursor_byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: lsp references document symbols and workspace symbols render shared panels" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 112, .height = 7 });
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
try client.handleTraceLine("open symbol body");
|
||||||
|
const references_payload =
|
||||||
|
\\{"jsonrpc":"2.0","id":3,"result":[{"uri":"file:///tmp/main.zig","range":{"start":{"line":0,"character":0},"end":{"line":0,"character":3}}},{"uri":"file:///tmp/lib.zig","range":{"start":{"line":0,"character":7},"end":{"line":0,"character":11}}}]}
|
||||||
|
;
|
||||||
|
const references_command = try std.fmt.allocPrint(std.testing.allocator, "lsp_references {s}", .{references_payload});
|
||||||
|
defer std.testing.allocator.free(references_command);
|
||||||
|
try client.handleTraceLine(references_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-references") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:nav:references:1:1:file_///tmp/main.zig:location") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:nav:references:1:8:file_///tmp/lib.zig:location") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const document_payload =
|
||||||
|
\\{"jsonrpc":"2.0","id":4,"result":[{"name":"main","kind":12,"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":6}},"selectionRange":{"start":{"line":0,"character":0},"end":{"line":0,"character":4}}}]}
|
||||||
|
;
|
||||||
|
const document_command = try std.fmt.allocPrint(std.testing.allocator, "lsp_document_symbols {s}", .{document_payload});
|
||||||
|
defer std.testing.allocator.free(document_command);
|
||||||
|
try client.handleTraceLine(document_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-document-symbols") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:symbol:document:kind_12:1:1:current:main") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace_payload =
|
||||||
|
\\{"jsonrpc":"2.0","id":5,"result":[{"name":"helper","kind":12,"location":{"uri":"file:///tmp/lib.zig","range":{"start":{"line":0,"character":7},"end":{"line":0,"character":11}}}}]}
|
||||||
|
;
|
||||||
|
const workspace_command = try std.fmt.allocPrint(std.testing.allocator, "lsp_workspace_symbols {s}", .{workspace_payload});
|
||||||
|
defer std.testing.allocator.free(workspace_command);
|
||||||
|
try client.handleTraceLine(workspace_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-workspace-symbols") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:symbol:workspace:kind_12:1:8:file_///tmp/lib.zig:helper") != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: lsp navigation failures do not corrupt 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_definition not-json"));
|
||||||
|
const none_payload =
|
||||||
|
\\{"jsonrpc":"2.0","id":2,"result":null}
|
||||||
|
;
|
||||||
|
const command = try std.fmt.allocPrint(std.testing.allocator, "lsp_definition {s}", .{none_payload});
|
||||||
|
defer std.testing.allocator.free(command);
|
||||||
|
try client.handleTraceLine(command);
|
||||||
|
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("lsp_navigation_open_selected"));
|
||||||
|
const snap = try client.session.snapshot();
|
||||||
|
try std.testing.expectEqualStrings("safe", snap.bytes);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user