Add compact language assistance rails
This commit is contained in:
@@ -13,6 +13,7 @@ test {
|
||||
pub const Feature = enum {
|
||||
search,
|
||||
panel_close,
|
||||
lsp,
|
||||
};
|
||||
|
||||
pub const Action = union(enum) {
|
||||
@@ -24,6 +25,9 @@ pub const Action = union(enum) {
|
||||
file_picker,
|
||||
search_file,
|
||||
search_project,
|
||||
hover,
|
||||
signature,
|
||||
expand_hover,
|
||||
repeat_rail,
|
||||
restore_last,
|
||||
not_built: Feature,
|
||||
@@ -41,6 +45,7 @@ const Mode = enum {
|
||||
rail,
|
||||
symbol_rail,
|
||||
search_rail,
|
||||
language_rail,
|
||||
open_prompt,
|
||||
};
|
||||
|
||||
@@ -65,6 +70,7 @@ pub const Leader = struct {
|
||||
.rail => return self.handleRail(event),
|
||||
.symbol_rail => return self.handleSymbolRail(event),
|
||||
.search_rail => return self.handleSearchRail(event),
|
||||
.language_rail => return self.handleLanguageRail(event),
|
||||
.open_prompt => return self.handleOpenPrompt(event),
|
||||
}
|
||||
}
|
||||
@@ -76,6 +82,7 @@ pub const Leader = struct {
|
||||
.rail => "leader: w save q quit o open p symbols s search r repeat x close",
|
||||
.symbol_rail => symbol_mod.rail_status,
|
||||
.search_rail => "search: f current file p project s symbols",
|
||||
.language_rail => "language: h hover s signature o open hover",
|
||||
.open_prompt => "open: type path, Enter opens, Esc cancels",
|
||||
};
|
||||
}
|
||||
@@ -120,6 +127,10 @@ pub const Leader = struct {
|
||||
self.mode = .search_rail;
|
||||
return .none;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "l")) {
|
||||
self.mode = .language_rail;
|
||||
return .none;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "q")) {
|
||||
self.mode = .idle;
|
||||
return .quit;
|
||||
@@ -174,6 +185,37 @@ pub const Leader = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn handleLanguageRail(self: *Leader, event: input.Event) Action {
|
||||
self.message = null;
|
||||
switch (event) {
|
||||
.text => |text| {
|
||||
self.mode = .idle;
|
||||
if (std.mem.eql(u8, text, "h")) return .hover;
|
||||
if (std.mem.eql(u8, text, "s")) return .signature;
|
||||
if (std.mem.eql(u8, text, "o")) return .expand_hover;
|
||||
self.message = "language action is not built in this profile yet";
|
||||
return .{ .not_built = .lsp };
|
||||
},
|
||||
.key => |key| switch (key) {
|
||||
.escape, .backspace => {
|
||||
self.mode = .idle;
|
||||
self.message = "language cancelled";
|
||||
return .none;
|
||||
},
|
||||
else => {
|
||||
self.mode = .idle;
|
||||
self.message = "unknown language key";
|
||||
return .none;
|
||||
},
|
||||
},
|
||||
.unknown => {
|
||||
self.mode = .idle;
|
||||
self.message = "unknown language key";
|
||||
return .none;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handleSearchRail(self: *Leader, event: input.Event) Action {
|
||||
self.message = null;
|
||||
switch (event) {
|
||||
|
||||
+46
@@ -163,6 +163,52 @@ pub fn missingProviderRowAlloc(allocator: std.mem.Allocator, capability: Provide
|
||||
return std.fmt.allocPrint(allocator, "provider:missing:{s}", .{@tagName(capability)});
|
||||
}
|
||||
|
||||
pub fn compactHoverCardAlloc(allocator: std.mem.Allocator, row: []const u8, width: usize) ![]u8 {
|
||||
const provider = providerIdFromRow(row) catch "unknown";
|
||||
const payload = providerPayloadFromRow(row) catch row;
|
||||
const prefix = "lsp:hover:";
|
||||
const body = if (std.mem.startsWith(u8, payload, prefix)) payload[prefix.len..] else payload;
|
||||
const split = std.mem.indexOfScalar(u8, body, '|') orelse body.len;
|
||||
const headline = body[0..split];
|
||||
const docs = if (split < body.len) body[split + 1 ..] else "";
|
||||
const budget = if (width > provider.len + headline.len + 8) width - provider.len - headline.len - 8 else 0;
|
||||
const excerpt_len = @min(docs.len, budget);
|
||||
return std.fmt.allocPrint(allocator, "[{s}] {s}{s}{s}", .{ provider, headline, if (excerpt_len > 0) " — " else "", docs[0..excerpt_len] });
|
||||
}
|
||||
|
||||
pub fn expandedHoverRowsAlloc(allocator: std.mem.Allocator, row: []const u8, width: usize) ![][]const u8 {
|
||||
const provider = providerIdFromRow(row) catch "unknown";
|
||||
const payload = providerPayloadFromRow(row) catch row;
|
||||
const prefix = "lsp:hover:";
|
||||
const body = if (std.mem.startsWith(u8, payload, prefix)) payload[prefix.len..] else payload;
|
||||
var rows = std.ArrayList([]const u8).empty;
|
||||
errdefer {
|
||||
for (rows.items) |item| allocator.free(item);
|
||||
rows.deinit(allocator);
|
||||
}
|
||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "hover_provider:{s}", .{provider}));
|
||||
var start: usize = 0;
|
||||
const wrap = @max(width, 8);
|
||||
while (start < body.len) {
|
||||
const end = @min(start + wrap, body.len);
|
||||
const wrapped = try allocator.dupe(u8, body[start..end]);
|
||||
for (wrapped) |*byte| {
|
||||
if (byte.* <= 0x20 or byte.* == '|') byte.* = '_';
|
||||
}
|
||||
try rows.append(allocator, wrapped);
|
||||
start = end;
|
||||
}
|
||||
return rows.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
pub fn compactSignatureCardAlloc(allocator: std.mem.Allocator, row: []const u8) ![]u8 {
|
||||
const provider = providerIdFromRow(row) catch "unknown";
|
||||
const payload = providerPayloadFromRow(row) catch row;
|
||||
const prefix = "lsp:signature:";
|
||||
const body = if (std.mem.startsWith(u8, payload, prefix)) payload[prefix.len..] else payload;
|
||||
return std.fmt.allocPrint(allocator, "[{s}] {s}", .{ provider, body });
|
||||
}
|
||||
|
||||
pub fn providerPickerRowsAlloc(allocator: std.mem.Allocator, capability: ProviderCapability, providers: []const Provider) ![][]const u8 {
|
||||
var rows = std.ArrayList([]const u8).empty;
|
||||
errdefer {
|
||||
|
||||
+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