Add LSP document sync transport
This commit is contained in:
+85
@@ -2,6 +2,7 @@ const std = @import("std");
|
||||
const input = @import("input.zig");
|
||||
const job_mod = @import("job.zig");
|
||||
const leader_mod = @import("leader.zig");
|
||||
const lsp_mod = @import("lsp.zig");
|
||||
const protocol = @import("protocol.zig");
|
||||
const replay = @import("replay.zig");
|
||||
const repo_mod = @import("repo.zig");
|
||||
@@ -148,6 +149,7 @@ pub const Client = struct {
|
||||
if (std.mem.eql(u8, line, "terminal_cancel")) return self.openStaticJobRow("terminal-status", "terminal_cancel:no_running_terminal");
|
||||
if (std.mem.eql(u8, line, "terminal_exit")) return self.closeTerminalPanel();
|
||||
if (std.mem.startsWith(u8, line, "syntax_spans ")) return self.openSyntaxSpans(line[13..]);
|
||||
if (std.mem.startsWith(u8, line, "lsp_sync ")) return self.openLspSync(line[9..]);
|
||||
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);
|
||||
@@ -416,6 +418,27 @@ pub const Client = struct {
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openLspSync(self: *Client, args: []const u8) !void {
|
||||
const io = self.io orelse return Error.ProtocolRejected;
|
||||
const parsed = parseLspSyncArgs(args) orelse return Error.ProtocolRejected;
|
||||
const snap = try self.session.snapshot();
|
||||
var argv = std.ArrayList([]const u8).empty;
|
||||
defer argv.deinit(self.allocator);
|
||||
var parts = std.mem.splitScalar(u8, parsed.command, ' ');
|
||||
while (parts.next()) |part| {
|
||||
if (part.len == 0) continue;
|
||||
try argv.append(self.allocator, part);
|
||||
}
|
||||
const rows = lsp_mod.runDocumentSyncRowsAlloc(self.allocator, io, parsed.cwd, argv.items, .{
|
||||
.uri = parsed.uri,
|
||||
.language_id = parsed.language_id,
|
||||
.text = snap.bytes,
|
||||
}) catch return Error.ProtocolRejected;
|
||||
defer freeOwnedRows(self.allocator, rows);
|
||||
try self.session.openListPanel("lsp-sync", rows);
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openStaticJobRow(self: *Client, title: []const u8, row: []const u8) !void {
|
||||
try self.session.openListPanel(title, &.{row});
|
||||
self.message = null;
|
||||
@@ -669,6 +692,20 @@ fn splitCwdAndCommand(cwd_and_command: []const u8) ?struct { cwd: []const u8, co
|
||||
return .{ .cwd = cwd, .command = command };
|
||||
}
|
||||
|
||||
fn parseLspSyncArgs(args: []const u8) ?struct { cwd: []const u8, uri: []const u8, language_id: []const u8, command: []const u8 } {
|
||||
const first = std.mem.indexOfScalar(u8, args, ' ') orelse return null;
|
||||
const cwd = args[0..first];
|
||||
const rest = std.mem.trim(u8, args[first + 1 ..], " ");
|
||||
const second = std.mem.indexOfScalar(u8, rest, ' ') orelse return null;
|
||||
const uri = rest[0..second];
|
||||
const rest2 = std.mem.trim(u8, rest[second + 1 ..], " ");
|
||||
const third = std.mem.indexOfScalar(u8, rest2, ' ') orelse return null;
|
||||
const language_id = rest2[0..third];
|
||||
const command = std.mem.trim(u8, rest2[third + 1 ..], " ");
|
||||
if (cwd.len == 0 or uri.len == 0 or language_id.len == 0 or command.len == 0) return null;
|
||||
return .{ .cwd = cwd, .uri = uri, .language_id = language_id, .command = command };
|
||||
}
|
||||
|
||||
test "regular: scripted narrow terminal trace edits saves exits and replays saved bytes" {
|
||||
const trace =
|
||||
\\open abc
|
||||
@@ -1505,3 +1542,51 @@ test "adversarial: unsupported syntax language is rejected without changing buff
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("safe", snap.bytes);
|
||||
}
|
||||
|
||||
fn makeTuiLspFixture(allocator: std.mem.Allocator) !struct { tmp: std.testing.TmpDir, cwd: []u8 } {
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
errdefer tmp.cleanup();
|
||||
const cwd = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}", .{&tmp.sub_path});
|
||||
errdefer allocator.free(cwd);
|
||||
return .{ .tmp = tmp, .cwd = cwd };
|
||||
}
|
||||
|
||||
test "regular: lsp sync panel sends current buffer to fake server" {
|
||||
var fixture = try makeTuiLspFixture(std.testing.allocator);
|
||||
defer {
|
||||
std.testing.allocator.free(fixture.cwd);
|
||||
fixture.tmp.cleanup();
|
||||
}
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 72, .height = 9 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open pub const x = 1;");
|
||||
const sync = try std.fmt.allocPrint(std.testing.allocator, "lsp_sync {s} file:///tmp/main.zig zig sh -c cat>observed.lsp", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(sync);
|
||||
try client.handleTraceLine(sync);
|
||||
{
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp-sync") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:sent:initialize") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:sent:textDocument/didOpen") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:sent:textDocument/didChange") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:sent:textDocument/didSave") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:status:exit_0") != null);
|
||||
}
|
||||
const observed = try fixture.tmp.dir.readFileAlloc(std.testing.io, "observed.lsp", std.testing.allocator, .limited(64 * 1024));
|
||||
defer std.testing.allocator.free(observed);
|
||||
try std.testing.expect(std.mem.indexOf(u8, observed, "pub const x = 1;") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, observed, "textDocument/didOpen") != null);
|
||||
}
|
||||
|
||||
test "adversarial: lsp sync rejection does not corrupt current buffer" {
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 72, .height = 5 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open safe");
|
||||
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("lsp_sync ."));
|
||||
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("lsp_sync . bad uri zig sh -c cat"));
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("safe", snap.bytes);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user