Add compact language assistance rails
This commit is contained in:
+192
-11
@@ -136,6 +136,7 @@ pub const Client = struct {
|
||||
viewport: Viewport,
|
||||
saved_bytes: ?[]u8 = null,
|
||||
message: ?[]const u8 = null,
|
||||
owned_message: ?[]u8 = null,
|
||||
quit: bool = false,
|
||||
mode: EditorMode = .normal,
|
||||
prefix: PrefixRail = .none,
|
||||
@@ -149,6 +150,8 @@ pub const Client = struct {
|
||||
project_search_prompt_active: bool = false,
|
||||
project_search_prompt: std.ArrayList(u8) = .empty,
|
||||
panel_context: PanelContext = .none,
|
||||
hover_rows: std.ArrayList([]u8) = .empty,
|
||||
signature_rows: std.ArrayList([]u8) = .empty,
|
||||
undo_stack: std.ArrayList(EditSnapshot) = .empty,
|
||||
redo_stack: std.ArrayList(EditSnapshot) = .empty,
|
||||
yank_bytes: ?[]u8 = null,
|
||||
@@ -171,11 +174,14 @@ pub const Client = struct {
|
||||
|
||||
pub fn deinit(self: *Client) void {
|
||||
if (self.saved_bytes) |bytes| self.allocator.free(bytes);
|
||||
if (self.owned_message) |bytes| self.allocator.free(bytes);
|
||||
if (self.yank_bytes) |bytes| self.allocator.free(bytes);
|
||||
self.search_prompt.deinit(self.allocator);
|
||||
self.project_search_prompt.deinit(self.allocator);
|
||||
self.allocator.free(self.search_query);
|
||||
self.search_matches.deinit(self.allocator);
|
||||
self.freeOwnedClientRows(&self.hover_rows);
|
||||
self.freeOwnedClientRows(&self.signature_rows);
|
||||
self.freeSnapshotStack(&self.undo_stack);
|
||||
self.freeSnapshotStack(&self.redo_stack);
|
||||
self.repo.deinit();
|
||||
@@ -195,6 +201,8 @@ pub const Client = struct {
|
||||
if (std.mem.startsWith(u8, line, "type ")) return self.handleInput(line[5..]);
|
||||
if (std.mem.startsWith(u8, line, "repo_gitignore ")) return self.repo.addIgnorePattern(line[15..]);
|
||||
if (std.mem.startsWith(u8, line, "repo_file ")) return self.addRepoFile(line[10..]);
|
||||
if (std.mem.startsWith(u8, line, "lsp_hover_fixture ")) return self.addHoverFixture(line[18..]);
|
||||
if (std.mem.startsWith(u8, line, "lsp_signature_fixture ")) return self.addSignatureFixture(line[22..]);
|
||||
if (std.mem.eql(u8, line, "file_picker")) return self.openRepoList("files", false, false);
|
||||
if (std.mem.eql(u8, line, "file_picker all")) return self.openRepoList("files", false, true);
|
||||
if (std.mem.eql(u8, line, "file_tree")) return self.openRepoList("tree", true, false);
|
||||
@@ -432,6 +440,14 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
pub fn setStatusMessage(self: *Client, message: []const u8) void {
|
||||
if (self.owned_message) |old| self.allocator.free(old);
|
||||
self.owned_message = null;
|
||||
self.message = message;
|
||||
}
|
||||
|
||||
fn setOwnedStatusMessage(self: *Client, message: []u8) void {
|
||||
if (self.owned_message) |old| self.allocator.free(old);
|
||||
self.owned_message = message;
|
||||
self.message = message;
|
||||
}
|
||||
|
||||
@@ -463,6 +479,12 @@ pub const Client = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn freeOwnedClientRows(self: *Client, rows: *std.ArrayList([]u8)) void {
|
||||
for (rows.items) |row| self.allocator.free(row);
|
||||
rows.deinit(self.allocator);
|
||||
rows.* = .empty;
|
||||
}
|
||||
|
||||
pub fn addRepoFileContent(self: *Client, path: []const u8, file_bytes: []const u8) !void {
|
||||
if (file_bytes.len > diagnostics_mod.max_file_bytes) {
|
||||
self.message = "diagnostic:file_too_large";
|
||||
@@ -475,6 +497,23 @@ pub const Client = struct {
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn addHoverFixture(self: *Client, payload: []const u8) !void {
|
||||
var parts = std.mem.splitScalar(u8, payload, '|');
|
||||
const provider = parts.next() orelse "fixture";
|
||||
const headline = parts.next() orelse "hover";
|
||||
const docs = parts.rest();
|
||||
const row = try std.fmt.allocPrint(self.allocator, "provider:{s}:lsp:hover:{s}|{s}", .{ provider, headline, docs });
|
||||
try self.hover_rows.append(self.allocator, row);
|
||||
}
|
||||
|
||||
fn addSignatureFixture(self: *Client, payload: []const u8) !void {
|
||||
var parts = std.mem.splitScalar(u8, payload, '|');
|
||||
const provider = parts.next() orelse "fixture";
|
||||
const signature = parts.rest();
|
||||
const row = try std.fmt.allocPrint(self.allocator, "provider:{s}:lsp:signature:{s}", .{ provider, signature });
|
||||
try self.signature_rows.append(self.allocator, row);
|
||||
}
|
||||
|
||||
fn addRepoFile(self: *Client, payload: []const u8) !void {
|
||||
const separator = std.mem.indexOfScalar(u8, payload, '=') orelse {
|
||||
self.message = "diagnostic:unsupported_file:invalid_repo_file_payload";
|
||||
@@ -943,7 +982,7 @@ pub const Client = struct {
|
||||
.match => self.applyMatchRail(event) catch |err| {
|
||||
self.message = @errorName(err);
|
||||
},
|
||||
.go => self.applyKnownRailOrMessage(event, "go rail ready"),
|
||||
.go => try self.applyGoRail(event),
|
||||
.repeat => self.applyKnownRailOrMessage(event, "repeat rail ready"),
|
||||
.delete => try self.applyDeleteRail(event),
|
||||
.change => try self.applyChangeRail(event),
|
||||
@@ -1019,16 +1058,8 @@ pub const Client = struct {
|
||||
self.message = "empty list";
|
||||
return;
|
||||
}
|
||||
var joined = std.ArrayList(u8).empty;
|
||||
defer joined.deinit(self.allocator);
|
||||
try joined.appendSlice(self.allocator, "list_open ");
|
||||
try joined.appendSlice(self.allocator, title);
|
||||
try joined.append(self.allocator, ' ');
|
||||
for (rows, 0..) |row, i| {
|
||||
if (i != 0) try joined.append(self.allocator, '|');
|
||||
try joined.appendSlice(self.allocator, row);
|
||||
}
|
||||
try self.applyProtocol(joined.items);
|
||||
try self.session.openListPanel(title, rows);
|
||||
self.message = null;
|
||||
self.panel_context = context;
|
||||
}
|
||||
|
||||
@@ -1114,6 +1145,37 @@ pub const Client = struct {
|
||||
self.panel_context = .project_search;
|
||||
}
|
||||
|
||||
fn showCompactHover(self: *Client) !void {
|
||||
if (self.hover_rows.items.len == 0) {
|
||||
self.message = "NoProvider:hover";
|
||||
return;
|
||||
}
|
||||
const card = try lsp_mod.compactHoverCardAlloc(self.allocator, self.hover_rows.items[0], self.viewport.width);
|
||||
self.setOwnedStatusMessage(card);
|
||||
}
|
||||
|
||||
fn showCompactSignature(self: *Client) !void {
|
||||
if (self.signature_rows.items.len == 0) {
|
||||
self.message = "NoProvider:signature";
|
||||
return;
|
||||
}
|
||||
const card = try lsp_mod.compactSignatureCardAlloc(self.allocator, self.signature_rows.items[0]);
|
||||
self.setOwnedStatusMessage(card);
|
||||
}
|
||||
|
||||
fn openExpandedHover(self: *Client) !void {
|
||||
if (self.hover_rows.items.len == 0) {
|
||||
self.message = "NoProvider:hover";
|
||||
return;
|
||||
}
|
||||
const rows = try lsp_mod.expandedHoverRowsAlloc(self.allocator, self.hover_rows.items[0], self.viewport.width);
|
||||
defer {
|
||||
for (rows) |row| self.allocator.free(row);
|
||||
self.allocator.free(rows);
|
||||
}
|
||||
try self.openListRows("hover", rows, .none);
|
||||
}
|
||||
|
||||
fn openSearchPrompt(self: *Client) void {
|
||||
self.search_prompt_active = true;
|
||||
self.search_prompt.clearRetainingCapacity();
|
||||
@@ -1191,6 +1253,52 @@ pub const Client = struct {
|
||||
try self.gotoSearchMatch();
|
||||
}
|
||||
|
||||
fn applyGoRail(self: *Client, event: input.Event) !void {
|
||||
const text = eventText(event) orelse return self.unknownPrefixOrInput("normal");
|
||||
if (std.mem.eql(u8, text, "a")) return self.moveParameter(.next);
|
||||
if (std.mem.eql(u8, text, "A")) return self.moveParameter(.previous);
|
||||
self.applyKnownRailOrMessage(event, "go rail ready");
|
||||
}
|
||||
|
||||
const ParameterDirection = enum { next, previous };
|
||||
|
||||
fn moveParameter(self: *Client, direction: ParameterDirection) !void {
|
||||
const snap = try self.session.snapshot();
|
||||
const pair = self.matchRange('(', false) catch {
|
||||
self.message = "NoParameterContext";
|
||||
return;
|
||||
};
|
||||
var target: ?usize = null;
|
||||
switch (direction) {
|
||||
.next => {
|
||||
var at = snap.cursor_byte;
|
||||
while (at < pair.end) : (at += 1) {
|
||||
if (snap.bytes[at] == ',') {
|
||||
target = at + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
.previous => {
|
||||
var at = @min(snap.cursor_byte, pair.end);
|
||||
while (at > pair.start) {
|
||||
at -= 1;
|
||||
if (snap.bytes[at] == ',') {
|
||||
var prev = at;
|
||||
while (prev > pair.start and snap.bytes[prev - 1] != ',') prev -= 1;
|
||||
target = prev;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
if (target) |byte| {
|
||||
var trimmed = byte;
|
||||
while (trimmed < pair.end and std.ascii.isWhitespace(snap.bytes[trimmed])) trimmed += 1;
|
||||
try self.session.moveToByte(trimmed);
|
||||
} else self.message = "NoParameterTarget";
|
||||
}
|
||||
|
||||
fn applyMatchRail(self: *Client, event: input.Event) !void {
|
||||
const text = eventText(event) orelse return self.unknownPrefixOrInput("normal");
|
||||
if (std.mem.eql(u8, text, "m")) return self.jumpToMatch(null);
|
||||
@@ -1564,6 +1672,9 @@ pub const Client = struct {
|
||||
.file_picker => try self.openFilePickerPanel(),
|
||||
.search_file => self.openSearchPrompt(),
|
||||
.search_project => self.openProjectSearchPrompt(),
|
||||
.hover => try self.showCompactHover(),
|
||||
.signature => try self.showCompactSignature(),
|
||||
.expand_hover => try self.openExpandedHover(),
|
||||
.repeat_rail => self.openPrefix(.repeat),
|
||||
.restore_last => self.restoreLastRail(),
|
||||
.not_built => {},
|
||||
@@ -3539,3 +3650,73 @@ test "adversarial: file and project panels handle ignored empty missing and clos
|
||||
defer std.testing.allocator.free(no_match_frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, no_match_frame, "no project matches") != null);
|
||||
}
|
||||
|
||||
test "regular: language rail shows compact hover signature and expandable hover" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 8 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("lsp_hover_fixture zls|add(lhs, rhs)|Very long documentation that should be compact first and expanded only on request.");
|
||||
try client.handleTraceLine("lsp_signature_fixture zls|add(lhs: i32, rhs: i32) active=rhs");
|
||||
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("l");
|
||||
try client.handleInput("h");
|
||||
const hover_frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(hover_frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, hover_frame, "[zls] add(lhs, rhs)") != null);
|
||||
try assertLinesFit(hover_frame, 48);
|
||||
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("l");
|
||||
try client.handleInput("s");
|
||||
const signature_frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(signature_frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, signature_frame, "[zls] add(lhs: i32, rhs: i32) active=rhs") != null);
|
||||
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("l");
|
||||
try client.handleInput("o");
|
||||
const expanded_frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(expanded_frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, expanded_frame, "hover_provider:zls") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, expanded_frame, "Very_long_documentation") != null);
|
||||
}
|
||||
|
||||
test "adversarial: missing hover and signature providers are visible" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 6 });
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("l");
|
||||
try client.handleInput("h");
|
||||
const hover_frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(hover_frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, hover_frame, "NoProvider:hover") != null);
|
||||
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("l");
|
||||
try client.handleInput("s");
|
||||
const signature_frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(signature_frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, signature_frame, "NoProvider:signature") != null);
|
||||
}
|
||||
|
||||
test "regular: go rail parameter movement uses keymap paths" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open call(alpha, beta, gamma)");
|
||||
try client.handleTraceLine("right");
|
||||
try client.handleTraceLine("right");
|
||||
try client.handleTraceLine("right");
|
||||
try client.handleTraceLine("right");
|
||||
try client.handleTraceLine("right");
|
||||
|
||||
try client.handleInput("g");
|
||||
try client.handleInput("a");
|
||||
var snap = try client.session.snapshot();
|
||||
try std.testing.expectEqual(@as(usize, 12), snap.cursor_byte);
|
||||
|
||||
try client.handleInput("g");
|
||||
try client.handleInput("A");
|
||||
snap = try client.session.snapshot();
|
||||
try std.testing.expectEqual(@as(usize, 5), snap.cursor_byte);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user