Add provider-aware diagnostics rail
This commit is contained in:
@@ -13,6 +13,99 @@ pub const SourceLabel = struct {
|
|||||||
row: []const u8,
|
row: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const Severity = enum {
|
||||||
|
err,
|
||||||
|
warning,
|
||||||
|
info,
|
||||||
|
hint,
|
||||||
|
|
||||||
|
pub fn parse(text: []const u8) !Severity {
|
||||||
|
if (std.mem.eql(u8, text, "error")) return .err;
|
||||||
|
if (std.mem.eql(u8, text, "err")) return .err;
|
||||||
|
if (std.mem.eql(u8, text, "warning")) return .warning;
|
||||||
|
if (std.mem.eql(u8, text, "info")) return .info;
|
||||||
|
if (std.mem.eql(u8, text, "hint")) return .hint;
|
||||||
|
return error.InvalidDiagnosticRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(self: Severity) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.err => "error",
|
||||||
|
.warning => "warning",
|
||||||
|
.info => "info",
|
||||||
|
.hint => "hint",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Diagnostic = struct {
|
||||||
|
provider: []const u8,
|
||||||
|
version: u64,
|
||||||
|
file: []const u8,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
severity: Severity,
|
||||||
|
message: []const u8,
|
||||||
|
|
||||||
|
pub fn stale(self: Diagnostic, document_version: u64) bool {
|
||||||
|
return self.version < document_version;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn rowAlloc(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
provider: []const u8,
|
||||||
|
version: u64,
|
||||||
|
file: []const u8,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
severity: Severity,
|
||||||
|
message: []const u8,
|
||||||
|
) ![]u8 {
|
||||||
|
if (provider.len == 0 or file.len == 0 or start > end) return error.InvalidDiagnosticRow;
|
||||||
|
return std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
"diag:{s}:{d}:{s}:{d}:{d}:{s}:{s}",
|
||||||
|
.{ sanitizeToken(provider), version, severity.label(), start, end, sanitizeToken(file), sanitizeToken(message) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parseRow(row: []const u8) !Diagnostic {
|
||||||
|
if (!std.mem.startsWith(u8, row, "diag:")) return error.InvalidDiagnosticRow;
|
||||||
|
var parts = std.mem.splitScalar(u8, row, ':');
|
||||||
|
_ = parts.next() orelse return error.InvalidDiagnosticRow;
|
||||||
|
const provider = parts.next() orelse return error.InvalidDiagnosticRow;
|
||||||
|
const version_text = parts.next() orelse return error.InvalidDiagnosticRow;
|
||||||
|
const severity_text = parts.next() orelse return error.InvalidDiagnosticRow;
|
||||||
|
const start_text = parts.next() orelse return error.InvalidDiagnosticRow;
|
||||||
|
const end_text = parts.next() orelse return error.InvalidDiagnosticRow;
|
||||||
|
const file = parts.next() orelse return error.InvalidDiagnosticRow;
|
||||||
|
const message = parts.rest();
|
||||||
|
const version = std.fmt.parseUnsigned(u64, version_text, 10) catch return error.InvalidDiagnosticRow;
|
||||||
|
const start = std.fmt.parseUnsigned(usize, start_text, 10) catch return error.InvalidDiagnosticRow;
|
||||||
|
const end = std.fmt.parseUnsigned(usize, end_text, 10) catch return error.InvalidDiagnosticRow;
|
||||||
|
if (provider.len == 0 or file.len == 0 or start > end) return error.InvalidDiagnosticRow;
|
||||||
|
return .{ .provider = provider, .version = version, .file = file, .start = start, .end = end, .severity = try Severity.parse(severity_text), .message = message };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn panelRowAlloc(allocator: std.mem.Allocator, row: []const u8, document_version: u64) ![]u8 {
|
||||||
|
const diagnostic = try parseRow(row);
|
||||||
|
const freshness = if (diagnostic.stale(document_version)) "stale" else "fresh";
|
||||||
|
return std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
"diag:{s}:{s}:{s}:{d}-{d}:{s}:{s}",
|
||||||
|
.{ freshness, diagnostic.provider, diagnostic.severity.label(), diagnostic.start, diagnostic.end, diagnostic.file, diagnostic.message },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sanitizeToken(text: []const u8) []const u8 {
|
||||||
|
if (text.len == 0) return "unknown";
|
||||||
|
for (text) |byte| {
|
||||||
|
if (byte <= 0x20 or byte == ':' or byte == '|') return "invalid";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sourceLabel(row: []const u8) SourceLabel {
|
pub fn sourceLabel(row: []const u8) SourceLabel {
|
||||||
if (std.mem.startsWith(u8, row, "provider:")) {
|
if (std.mem.startsWith(u8, row, "provider:")) {
|
||||||
return .{
|
return .{
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub const Feature = enum {
|
|||||||
search,
|
search,
|
||||||
panel_close,
|
panel_close,
|
||||||
lsp,
|
lsp,
|
||||||
|
diagnostics,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Action = union(enum) {
|
pub const Action = union(enum) {
|
||||||
@@ -28,6 +29,10 @@ pub const Action = union(enum) {
|
|||||||
hover,
|
hover,
|
||||||
signature,
|
signature,
|
||||||
expand_hover,
|
expand_hover,
|
||||||
|
diagnostics_open,
|
||||||
|
diagnostics_next,
|
||||||
|
diagnostics_previous,
|
||||||
|
diagnostics_filter,
|
||||||
repeat_rail,
|
repeat_rail,
|
||||||
restore_last,
|
restore_last,
|
||||||
not_built: Feature,
|
not_built: Feature,
|
||||||
@@ -46,6 +51,7 @@ const Mode = enum {
|
|||||||
symbol_rail,
|
symbol_rail,
|
||||||
search_rail,
|
search_rail,
|
||||||
language_rail,
|
language_rail,
|
||||||
|
diagnostic_rail,
|
||||||
open_prompt,
|
open_prompt,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,6 +77,7 @@ pub const Leader = struct {
|
|||||||
.symbol_rail => return self.handleSymbolRail(event),
|
.symbol_rail => return self.handleSymbolRail(event),
|
||||||
.search_rail => return self.handleSearchRail(event),
|
.search_rail => return self.handleSearchRail(event),
|
||||||
.language_rail => return self.handleLanguageRail(event),
|
.language_rail => return self.handleLanguageRail(event),
|
||||||
|
.diagnostic_rail => return self.handleDiagnosticRail(event),
|
||||||
.open_prompt => return self.handleOpenPrompt(event),
|
.open_prompt => return self.handleOpenPrompt(event),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +90,7 @@ pub const Leader = struct {
|
|||||||
.symbol_rail => symbol_mod.rail_status,
|
.symbol_rail => symbol_mod.rail_status,
|
||||||
.search_rail => "search: f current file p project s symbols",
|
.search_rail => "search: f current file p project s symbols",
|
||||||
.language_rail => "language: h hover s signature o open hover",
|
.language_rail => "language: h hover s signature o open hover",
|
||||||
|
.diagnostic_rail => "diagnostics: d/open n next p previous f filter-source",
|
||||||
.open_prompt => "open: type path, Enter opens, Esc cancels",
|
.open_prompt => "open: type path, Enter opens, Esc cancels",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -131,6 +139,10 @@ pub const Leader = struct {
|
|||||||
self.mode = .language_rail;
|
self.mode = .language_rail;
|
||||||
return .none;
|
return .none;
|
||||||
}
|
}
|
||||||
|
if (std.mem.eql(u8, text, "d")) {
|
||||||
|
self.mode = .diagnostic_rail;
|
||||||
|
return .diagnostics_open;
|
||||||
|
}
|
||||||
if (std.mem.eql(u8, text, "q")) {
|
if (std.mem.eql(u8, text, "q")) {
|
||||||
self.mode = .idle;
|
self.mode = .idle;
|
||||||
return .quit;
|
return .quit;
|
||||||
@@ -216,6 +228,50 @@ pub const Leader = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handleDiagnosticRail(self: *Leader, event: input.Event) Action {
|
||||||
|
self.message = null;
|
||||||
|
switch (event) {
|
||||||
|
.text => |text| {
|
||||||
|
if (std.mem.eql(u8, text, "d")) {
|
||||||
|
self.mode = .idle;
|
||||||
|
return .diagnostics_open;
|
||||||
|
}
|
||||||
|
if (std.mem.eql(u8, text, "n")) {
|
||||||
|
self.mode = .idle;
|
||||||
|
return .diagnostics_next;
|
||||||
|
}
|
||||||
|
if (std.mem.eql(u8, text, "p")) {
|
||||||
|
self.mode = .idle;
|
||||||
|
return .diagnostics_previous;
|
||||||
|
}
|
||||||
|
if (std.mem.eql(u8, text, "f")) {
|
||||||
|
self.mode = .idle;
|
||||||
|
return .diagnostics_filter;
|
||||||
|
}
|
||||||
|
self.mode = .idle;
|
||||||
|
self.message = "unknown diagnostics key";
|
||||||
|
return .none;
|
||||||
|
},
|
||||||
|
.key => |key| switch (key) {
|
||||||
|
.escape, .backspace => {
|
||||||
|
self.mode = .idle;
|
||||||
|
self.message = "diagnostics cancelled";
|
||||||
|
return .none;
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
self.mode = .idle;
|
||||||
|
self.message = "unknown diagnostics key";
|
||||||
|
return .none;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.unknown => {
|
||||||
|
self.mode = .idle;
|
||||||
|
self.message = "unknown diagnostics key";
|
||||||
|
return .none;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handleSearchRail(self: *Leader, event: input.Event) Action {
|
fn handleSearchRail(self: *Leader, event: input.Event) Action {
|
||||||
self.message = null;
|
self.message = null;
|
||||||
switch (event) {
|
switch (event) {
|
||||||
|
|||||||
+283
-1
@@ -152,6 +152,11 @@ pub const Client = struct {
|
|||||||
panel_context: PanelContext = .none,
|
panel_context: PanelContext = .none,
|
||||||
hover_rows: std.ArrayList([]u8) = .empty,
|
hover_rows: std.ArrayList([]u8) = .empty,
|
||||||
signature_rows: std.ArrayList([]u8) = .empty,
|
signature_rows: std.ArrayList([]u8) = .empty,
|
||||||
|
diagnostic_rows: std.ArrayList([]u8) = .empty,
|
||||||
|
diagnostic_filter: ?[]u8 = null,
|
||||||
|
diagnostic_index: usize = 0,
|
||||||
|
diagnostic_selected: bool = false,
|
||||||
|
document_version: u64 = 1,
|
||||||
undo_stack: std.ArrayList(EditSnapshot) = .empty,
|
undo_stack: std.ArrayList(EditSnapshot) = .empty,
|
||||||
redo_stack: std.ArrayList(EditSnapshot) = .empty,
|
redo_stack: std.ArrayList(EditSnapshot) = .empty,
|
||||||
yank_bytes: ?[]u8 = null,
|
yank_bytes: ?[]u8 = null,
|
||||||
@@ -182,6 +187,8 @@ pub const Client = struct {
|
|||||||
self.search_matches.deinit(self.allocator);
|
self.search_matches.deinit(self.allocator);
|
||||||
self.freeOwnedClientRows(&self.hover_rows);
|
self.freeOwnedClientRows(&self.hover_rows);
|
||||||
self.freeOwnedClientRows(&self.signature_rows);
|
self.freeOwnedClientRows(&self.signature_rows);
|
||||||
|
self.freeOwnedClientRows(&self.diagnostic_rows);
|
||||||
|
if (self.diagnostic_filter) |filter| self.allocator.free(filter);
|
||||||
self.freeSnapshotStack(&self.undo_stack);
|
self.freeSnapshotStack(&self.undo_stack);
|
||||||
self.freeSnapshotStack(&self.redo_stack);
|
self.freeSnapshotStack(&self.redo_stack);
|
||||||
self.repo.deinit();
|
self.repo.deinit();
|
||||||
@@ -203,6 +210,8 @@ pub const Client = struct {
|
|||||||
if (std.mem.startsWith(u8, line, "repo_file ")) return self.addRepoFile(line[10..]);
|
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_hover_fixture ")) return self.addHoverFixture(line[18..]);
|
||||||
if (std.mem.startsWith(u8, line, "lsp_signature_fixture ")) return self.addSignatureFixture(line[22..]);
|
if (std.mem.startsWith(u8, line, "lsp_signature_fixture ")) return self.addSignatureFixture(line[22..]);
|
||||||
|
if (std.mem.startsWith(u8, line, "diagnostic_fixture ")) return self.addDiagnosticFixture(line[19..]);
|
||||||
|
if (std.mem.startsWith(u8, line, "diagnostic_filter ")) return self.setDiagnosticFilter(line[18..]);
|
||||||
if (std.mem.eql(u8, line, "file_picker")) return self.openRepoList("files", false, false);
|
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_picker all")) return self.openRepoList("files", false, true);
|
||||||
if (std.mem.eql(u8, line, "file_tree")) return self.openRepoList("tree", true, false);
|
if (std.mem.eql(u8, line, "file_tree")) return self.openRepoList("tree", true, false);
|
||||||
@@ -514,6 +523,31 @@ pub const Client = struct {
|
|||||||
try self.signature_rows.append(self.allocator, row);
|
try self.signature_rows.append(self.allocator, row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn addDiagnosticFixture(self: *Client, payload: []const u8) !void {
|
||||||
|
var parts = std.mem.splitScalar(u8, payload, '|');
|
||||||
|
const provider = parts.next() orelse return Error.ProtocolRejected;
|
||||||
|
const version_text = parts.next() orelse return Error.ProtocolRejected;
|
||||||
|
const file = parts.next() orelse return Error.ProtocolRejected;
|
||||||
|
const start_text = parts.next() orelse return Error.ProtocolRejected;
|
||||||
|
const end_text = parts.next() orelse return Error.ProtocolRejected;
|
||||||
|
const severity_text = parts.next() orelse return Error.ProtocolRejected;
|
||||||
|
const message = parts.rest();
|
||||||
|
const version = std.fmt.parseUnsigned(u64, version_text, 10) catch return Error.ProtocolRejected;
|
||||||
|
const start = std.fmt.parseUnsigned(usize, start_text, 10) catch return Error.ProtocolRejected;
|
||||||
|
const end = std.fmt.parseUnsigned(usize, end_text, 10) catch return Error.ProtocolRejected;
|
||||||
|
const severity = diagnostics_mod.Severity.parse(severity_text) catch return Error.ProtocolRejected;
|
||||||
|
const row = diagnostics_mod.rowAlloc(self.allocator, provider, version, file, start, end, severity, message) catch return Error.ProtocolRejected;
|
||||||
|
try self.diagnostic_rows.append(self.allocator, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setDiagnosticFilter(self: *Client, provider: []const u8) !void {
|
||||||
|
if (self.diagnostic_filter) |old| self.allocator.free(old);
|
||||||
|
self.diagnostic_filter = if (std.mem.eql(u8, provider, "all")) null else try self.allocator.dupe(u8, diagnostics_mod.sanitizeToken(provider));
|
||||||
|
self.diagnostic_index = 0;
|
||||||
|
self.diagnostic_selected = false;
|
||||||
|
try self.openDiagnosticsPanel();
|
||||||
|
}
|
||||||
|
|
||||||
fn addRepoFile(self: *Client, payload: []const u8) !void {
|
fn addRepoFile(self: *Client, payload: []const u8) !void {
|
||||||
const separator = std.mem.indexOfScalar(u8, payload, '=') orelse {
|
const separator = std.mem.indexOfScalar(u8, payload, '=') orelse {
|
||||||
self.message = "diagnostic:unsupported_file:invalid_repo_file_payload";
|
self.message = "diagnostic:unsupported_file:invalid_repo_file_payload";
|
||||||
@@ -1176,6 +1210,138 @@ pub const Client = struct {
|
|||||||
try self.openListRows("hover", rows, .none);
|
try self.openListRows("hover", rows, .none);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DiagnosticDirection = enum { next, previous };
|
||||||
|
|
||||||
|
fn diagnosticMatchesFilter(self: *const Client, row: []const u8) bool {
|
||||||
|
const filter = self.diagnostic_filter orelse return true;
|
||||||
|
const diagnostic = diagnostics_mod.parseRow(row) catch return false;
|
||||||
|
return std.mem.eql(u8, diagnostic.provider, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnosticPanelRowsAlloc(self: *const Client) ![][]const u8 {
|
||||||
|
var rows = std.ArrayList([]const u8).empty;
|
||||||
|
errdefer freeOwnedRows(self.allocator, rows.items);
|
||||||
|
for (self.diagnostic_rows.items) |row| {
|
||||||
|
if (!self.diagnosticMatchesFilter(row)) continue;
|
||||||
|
const panel_row = diagnostics_mod.panelRowAlloc(self.allocator, row, self.document_version) catch try self.allocator.dupe(u8, "diag:invalid");
|
||||||
|
try rows.append(self.allocator, panel_row);
|
||||||
|
}
|
||||||
|
if (rows.items.len == 0) try rows.append(self.allocator, try self.allocator.dupe(u8, "diag:empty"));
|
||||||
|
return rows.toOwnedSlice(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openDiagnosticsPanel(self: *Client) !void {
|
||||||
|
const snap = try self.session.snapshot();
|
||||||
|
if (snap.active_panel_title) |title| {
|
||||||
|
if (std.mem.eql(u8, title, "diagnostics")) self.session.closePanel() catch {};
|
||||||
|
}
|
||||||
|
const rows = try self.diagnosticPanelRowsAlloc();
|
||||||
|
defer freeOwnedRows(self.allocator, rows);
|
||||||
|
try self.session.openListPanel("diagnostics", rows);
|
||||||
|
self.message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filteredDiagnosticCount(self: *const Client) usize {
|
||||||
|
var count: usize = 0;
|
||||||
|
for (self.diagnostic_rows.items) |row| {
|
||||||
|
if (self.diagnosticMatchesFilter(row)) count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filteredDiagnosticRow(self: *const Client, index: usize) ?[]const u8 {
|
||||||
|
var seen: usize = 0;
|
||||||
|
for (self.diagnostic_rows.items) |row| {
|
||||||
|
if (!self.diagnosticMatchesFilter(row)) continue;
|
||||||
|
if (seen == index) return row;
|
||||||
|
seen += 1;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gotoDiagnostic(self: *Client, direction: DiagnosticDirection) !void {
|
||||||
|
const count = self.filteredDiagnosticCount();
|
||||||
|
if (count == 0) {
|
||||||
|
self.message = "diag:empty";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.diagnostic_index = if (self.diagnostic_selected) switch (direction) {
|
||||||
|
.next => (self.diagnostic_index + 1) % count,
|
||||||
|
.previous => if (self.diagnostic_index == 0) count - 1 else self.diagnostic_index - 1,
|
||||||
|
} else switch (direction) {
|
||||||
|
.next => 0,
|
||||||
|
.previous => count - 1,
|
||||||
|
};
|
||||||
|
self.diagnostic_selected = true;
|
||||||
|
const row = self.filteredDiagnosticRow(self.diagnostic_index) orelse return;
|
||||||
|
const range = self.diagnosticRangeFromRow(row) catch |err| {
|
||||||
|
self.message = @errorName(err);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
try self.session.moveToByte(range.start);
|
||||||
|
try self.session.selectRange(range.start, range.end);
|
||||||
|
self.session.clearSelection();
|
||||||
|
try self.session.moveToByte(range.start);
|
||||||
|
self.session.closePanel() catch {};
|
||||||
|
self.message = "diag:jumped";
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filterDiagnosticsFromPanel(self: *Client) !void {
|
||||||
|
const selected = self.session.activeListItem() catch null;
|
||||||
|
if (selected) |row| {
|
||||||
|
if (diagnosticProviderFromPanelRow(row)) |provider| {
|
||||||
|
try self.setDiagnosticFilter(provider);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (self.diagnostic_filter) |old| {
|
||||||
|
self.allocator.free(old);
|
||||||
|
self.diagnostic_filter = null;
|
||||||
|
self.message = "diag:filter_all";
|
||||||
|
} else self.message = "diag:filter_select_source";
|
||||||
|
try self.openDiagnosticsPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnosticProviderFromPanelRow(row: []const u8) ?[]const u8 {
|
||||||
|
if (!std.mem.startsWith(u8, row, "diag:")) return null;
|
||||||
|
var parts = std.mem.splitScalar(u8, row, ':');
|
||||||
|
_ = parts.next() orelse return null;
|
||||||
|
const freshness_or_empty = parts.next() orelse return null;
|
||||||
|
if (std.mem.eql(u8, freshness_or_empty, "empty") or std.mem.eql(u8, freshness_or_empty, "invalid")) return null;
|
||||||
|
return parts.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnosticRangeFromRow(self: *Client, row: []const u8) !ObjectRange {
|
||||||
|
const diagnostic = try diagnostics_mod.parseRow(row);
|
||||||
|
const snap = try self.session.snapshot();
|
||||||
|
if (diagnostic.stale(self.document_version)) return error.StaleDiagnostic;
|
||||||
|
if (diagnostic.end > snap.bytes.len) return error.InvalidDiagnosticRange;
|
||||||
|
return .{ .start = diagnostic.start, .end = diagnostic.end };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn currentDiagnosticRange(self: *Client) !ObjectRange {
|
||||||
|
const count = self.filteredDiagnosticCount();
|
||||||
|
if (count == 0) return error.NoDiagnostics;
|
||||||
|
if (self.diagnostic_index >= count) self.diagnostic_index = 0;
|
||||||
|
const row = self.filteredDiagnosticRow(self.diagnostic_index) orelse return error.NoDiagnostics;
|
||||||
|
return self.diagnosticRangeFromRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selectCurrentDiagnosticRange(self: *Client) !void {
|
||||||
|
const range = self.currentDiagnosticRange() catch |err| {
|
||||||
|
self.message = @errorName(err);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
try self.session.selectRange(range.start, range.end);
|
||||||
|
self.message = "selected diagnostic";
|
||||||
|
}
|
||||||
|
|
||||||
|
fn noteDocumentChanged(self: *Client) void {
|
||||||
|
self.document_version += 1;
|
||||||
|
self.diagnostic_index = 0;
|
||||||
|
self.diagnostic_selected = false;
|
||||||
|
}
|
||||||
|
|
||||||
fn openSearchPrompt(self: *Client) void {
|
fn openSearchPrompt(self: *Client) void {
|
||||||
self.search_prompt_active = true;
|
self.search_prompt_active = true;
|
||||||
self.search_prompt.clearRetainingCapacity();
|
self.search_prompt.clearRetainingCapacity();
|
||||||
@@ -1257,6 +1423,7 @@ pub const Client = struct {
|
|||||||
const text = eventText(event) orelse return self.unknownPrefixOrInput("normal");
|
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(.next);
|
||||||
if (std.mem.eql(u8, text, "A")) return self.moveParameter(.previous);
|
if (std.mem.eql(u8, text, "A")) return self.moveParameter(.previous);
|
||||||
|
if (std.mem.eql(u8, text, "e")) return self.gotoDiagnostic(.next);
|
||||||
self.applyKnownRailOrMessage(event, "go rail ready");
|
self.applyKnownRailOrMessage(event, "go rail ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1414,6 +1581,7 @@ pub const Client = struct {
|
|||||||
self.dropLastUndoSnapshot();
|
self.dropLastUndoSnapshot();
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
self.noteDocumentChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn changeRange(self: *Client, range: ObjectRange) !void {
|
fn changeRange(self: *Client, range: ObjectRange) !void {
|
||||||
@@ -1424,6 +1592,7 @@ pub const Client = struct {
|
|||||||
self.dropLastUndoSnapshot();
|
self.dropLastUndoSnapshot();
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
self.noteDocumentChanged();
|
||||||
self.mode = .insert;
|
self.mode = .insert;
|
||||||
self.message = "insert";
|
self.message = "insert";
|
||||||
}
|
}
|
||||||
@@ -1447,6 +1616,7 @@ pub const Client = struct {
|
|||||||
self.dropLastUndoSnapshot();
|
self.dropLastUndoSnapshot();
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
self.noteDocumentChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn yankCurrentLine(self: *Client, include_newline: bool) !void {
|
fn yankCurrentLine(self: *Client, include_newline: bool) !void {
|
||||||
@@ -1497,7 +1667,7 @@ pub const Client = struct {
|
|||||||
'i' => try self.indentRange(),
|
'i' => try self.indentRange(),
|
||||||
'p' => try self.parameterRange(),
|
'p' => try self.parameterRange(),
|
||||||
'f' => try self.enclosingFormRange(),
|
'f' => try self.enclosingFormRange(),
|
||||||
'd' => try self.lineRange(false),
|
'd' => self.currentDiagnosticRange() catch try self.lineRange(false),
|
||||||
else => error.UnsupportedObject,
|
else => error.UnsupportedObject,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1599,6 +1769,7 @@ pub const Client = struct {
|
|||||||
self.dropLastUndoSnapshot();
|
self.dropLastUndoSnapshot();
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
self.noteDocumentChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn applyMutatingProtocol(self: *Client, line: []const u8) !void {
|
fn applyMutatingProtocol(self: *Client, line: []const u8) !void {
|
||||||
@@ -1608,6 +1779,7 @@ pub const Client = struct {
|
|||||||
self.dropLastUndoSnapshot();
|
self.dropLastUndoSnapshot();
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
self.noteDocumentChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repeatProtocol(self: *Client, line: []const u8, repeat: usize) !void {
|
fn repeatProtocol(self: *Client, line: []const u8, repeat: usize) !void {
|
||||||
@@ -1675,6 +1847,10 @@ pub const Client = struct {
|
|||||||
.hover => try self.showCompactHover(),
|
.hover => try self.showCompactHover(),
|
||||||
.signature => try self.showCompactSignature(),
|
.signature => try self.showCompactSignature(),
|
||||||
.expand_hover => try self.openExpandedHover(),
|
.expand_hover => try self.openExpandedHover(),
|
||||||
|
.diagnostics_open => try self.openDiagnosticsPanel(),
|
||||||
|
.diagnostics_next => try self.gotoDiagnostic(.next),
|
||||||
|
.diagnostics_previous => try self.gotoDiagnostic(.previous),
|
||||||
|
.diagnostics_filter => try self.filterDiagnosticsFromPanel(),
|
||||||
.repeat_rail => self.openPrefix(.repeat),
|
.repeat_rail => self.openPrefix(.repeat),
|
||||||
.restore_last => self.restoreLastRail(),
|
.restore_last => self.restoreLastRail(),
|
||||||
.not_built => {},
|
.not_built => {},
|
||||||
@@ -3720,3 +3896,109 @@ test "regular: go rail parameter movement uses keymap paths" {
|
|||||||
snap = try client.session.snapshot();
|
snap = try client.session.snapshot();
|
||||||
try std.testing.expectEqual(@as(usize, 5), snap.cursor_byte);
|
try std.testing.expectEqual(@as(usize, 5), snap.cursor_byte);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "regular: diagnostics panel preserves providers filters and navigates" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 72, .height = 8 });
|
||||||
|
defer client.deinit();
|
||||||
|
try client.handleTraceLine("open abcdef\nsecond");
|
||||||
|
try client.handleTraceLine("diagnostic_fixture zls|1|main.zig|1|3|error|bad_token");
|
||||||
|
try client.handleTraceLine("diagnostic_fixture lint|1|main.zig|1|3|warning|style_duplicate");
|
||||||
|
try client.handleTraceLine("diagnostic_fixture zls|1|main.zig|8|13|info|second_line");
|
||||||
|
|
||||||
|
try client.handleInput(" ");
|
||||||
|
try client.handleInput("d");
|
||||||
|
const panel_frame = try client.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(panel_frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, panel_frame, "diag:fresh:zls:error:1-3:main.zig:bad_token") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, panel_frame, "diag:fresh:lint:warning:1-3:main.zig:style_duplicate") != null);
|
||||||
|
|
||||||
|
try client.handleInput("f");
|
||||||
|
const filtered_frame = try client.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(filtered_frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, filtered_frame, "diag:fresh:zls:error:1-3") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, filtered_frame, "lint:warning") == null);
|
||||||
|
|
||||||
|
try client.handleInput(" ");
|
||||||
|
try client.handleInput("d");
|
||||||
|
try client.handleInput("n");
|
||||||
|
var snap = try client.session.snapshot();
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), snap.cursor_byte);
|
||||||
|
|
||||||
|
try client.handleInput(" ");
|
||||||
|
try client.handleInput("d");
|
||||||
|
try client.handleInput("n");
|
||||||
|
snap = try client.session.snapshot();
|
||||||
|
try std.testing.expectEqual(@as(usize, 8), snap.cursor_byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: go rail jumps to first diagnostic" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
|
||||||
|
defer client.deinit();
|
||||||
|
try client.handleTraceLine("open abcdef");
|
||||||
|
try client.handleTraceLine("diagnostic_fixture zls|1|main.zig|2|5|error|range");
|
||||||
|
|
||||||
|
try client.handleInput("g");
|
||||||
|
try client.handleInput("e");
|
||||||
|
const snap = try client.session.snapshot();
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), snap.cursor_byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: select diagnostic object range with s d" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
|
||||||
|
defer client.deinit();
|
||||||
|
try client.handleTraceLine("open abcdef");
|
||||||
|
try client.handleTraceLine("diagnostic_fixture zls|1|main.zig|2|5|error|range");
|
||||||
|
|
||||||
|
try client.handleInput("s");
|
||||||
|
try client.handleInput("d");
|
||||||
|
const snap = try client.session.snapshot();
|
||||||
|
try std.testing.expect(snap.selection != null);
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), snap.selection.?.anchor);
|
||||||
|
try std.testing.expectEqual(@as(usize, 5), snap.selection.?.cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: diagnostics stale empty duplicate and invalid ranges are safe" {
|
||||||
|
var empty_client = try Client.init(std.testing.allocator, .{ .width = 72, .height = 8 });
|
||||||
|
defer empty_client.deinit();
|
||||||
|
try empty_client.handleTraceLine("open abcdef");
|
||||||
|
try empty_client.handleInput(" ");
|
||||||
|
try empty_client.handleInput("d");
|
||||||
|
const empty_frame = try empty_client.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(empty_frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, empty_frame, "diag:empty") != null);
|
||||||
|
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 72, .height = 8 });
|
||||||
|
defer client.deinit();
|
||||||
|
try client.handleTraceLine("open abcdef");
|
||||||
|
try client.handleTraceLine("diagnostic_fixture zls|1|main.zig|1|3|error|first");
|
||||||
|
try client.handleTraceLine("diagnostic_fixture lint|1|main.zig|1|3|warning|same_range_other_provider");
|
||||||
|
try client.handleTraceLine("diagnostic_fixture zls|1|main.zig|20|25|hint|invalid_range");
|
||||||
|
try client.handleInput("i");
|
||||||
|
try client.handleInput("X");
|
||||||
|
try client.handleTraceLine("key escape");
|
||||||
|
|
||||||
|
try client.handleInput(" ");
|
||||||
|
try client.handleInput("d");
|
||||||
|
const stale_frame = try client.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(stale_frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, stale_frame, "diag:stale:zls:error:1-3") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, stale_frame, "diag:stale:lint:warning:1-3") != null);
|
||||||
|
|
||||||
|
try client.handleTraceLine("diagnostic_filter all");
|
||||||
|
try client.handleInput("n");
|
||||||
|
const invalid_nav_frame = try client.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(invalid_nav_frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, invalid_nav_frame, "StaleDiagnostic") != null or std.mem.indexOf(u8, invalid_nav_frame, "InvalidDiagnosticRange") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: diagnostic row model sanitizes and parses" {
|
||||||
|
const row = try diagnostics_mod.rowAlloc(std.testing.allocator, "zls", 3, "main.zig", 1, 4, .err, "bad: token");
|
||||||
|
defer std.testing.allocator.free(row);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, row, "invalid") != null);
|
||||||
|
const parsed = try diagnostics_mod.parseRow(row);
|
||||||
|
try std.testing.expectEqual(@as(u64, 3), parsed.version);
|
||||||
|
try std.testing.expectEqual(diagnostics_mod.Severity.err, parsed.severity);
|
||||||
|
const panel = try diagnostics_mod.panelRowAlloc(std.testing.allocator, row, 4);
|
||||||
|
defer std.testing.allocator.free(panel);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, panel, "diag:stale:zls:error:1-4") != null);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user