feat: render language server diagnostics and add next/previous diagnostic commands
Bound to Alt-n and Alt-p
This commit is contained in:
parent
b541fd42de
commit
e69bd6166a
6 changed files with 250 additions and 16 deletions
3
help.md
3
help.md
|
@ -246,6 +246,9 @@ cycle style of editing.
|
|||
|
||||
### Language Server Commands
|
||||
|
||||
- Alt-n, Alt-p
|
||||
Goto next/previous diagnostic
|
||||
|
||||
- F12 =>
|
||||
Goto definition of symbol at cursor
|
||||
|
||||
|
|
|
@ -149,7 +149,7 @@ pub const CurSel = struct {
|
|||
}
|
||||
};
|
||||
|
||||
const Diagnostic = struct {
|
||||
pub const Diagnostic = struct {
|
||||
source: []const u8,
|
||||
code: []const u8,
|
||||
message: []const u8,
|
||||
|
@ -161,6 +161,21 @@ const Diagnostic = struct {
|
|||
a.free(self.code);
|
||||
a.free(self.message);
|
||||
}
|
||||
|
||||
const Severity = enum { Error, Warning, Information, Hint };
|
||||
pub fn get_severity(self: Diagnostic) Severity {
|
||||
return to_severity(self.severity);
|
||||
}
|
||||
|
||||
pub fn to_severity(sev: i32) Severity {
|
||||
return switch (sev) {
|
||||
1 => .Error,
|
||||
2 => .Warning,
|
||||
3 => .Information,
|
||||
4 => .Hint,
|
||||
else => .Error,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Editor = struct {
|
||||
|
@ -237,6 +252,10 @@ pub const Editor = struct {
|
|||
style_cache_theme: []const u8 = "",
|
||||
|
||||
diagnostics: std.ArrayList(Diagnostic),
|
||||
diag_errors: usize = 0,
|
||||
diag_warnings: usize = 0,
|
||||
diag_info: usize = 0,
|
||||
diag_hints: usize = 0,
|
||||
|
||||
const StyleCache = std.AutoHashMap(u32, ?Widget.Theme.Token);
|
||||
|
||||
|
@ -707,7 +726,7 @@ pub const Editor = struct {
|
|||
_ = root.walk_from_line_begin_const(self.view.row, ctx.walker, &ctx_) catch {};
|
||||
}
|
||||
self.render_syntax(theme, cache, root) catch {};
|
||||
self.render_diagnostics(theme, root) catch {};
|
||||
self.render_diagnostics(theme, hl_row) catch {};
|
||||
self.render_cursors(theme) catch {};
|
||||
}
|
||||
|
||||
|
@ -786,21 +805,58 @@ pub const Editor = struct {
|
|||
};
|
||||
}
|
||||
|
||||
fn render_diagnostics(self: *const Self, theme: *const Widget.Theme, root: Buffer.Root) !void {
|
||||
for (self.diagnostics.items) |*diag| self.render_diagnostic(diag, theme, root);
|
||||
fn render_diagnostics(self: *const Self, theme: *const Widget.Theme, hl_row: ?usize) !void {
|
||||
for (self.diagnostics.items) |*diag| self.render_diagnostic(diag, theme, hl_row);
|
||||
}
|
||||
|
||||
fn render_diagnostic(self: *const Self, diag: *const Diagnostic, theme: *const Widget.Theme, _: Buffer.Root) void {
|
||||
if (self.screen_cursor(&diag.sel.begin)) |pos| {
|
||||
fn render_diagnostic(self: *const Self, diag: *const Diagnostic, theme: *const Widget.Theme, hl_row: ?usize) void {
|
||||
const screen_width = self.view.cols;
|
||||
const pos = self.screen_cursor(&diag.sel.begin) orelse return;
|
||||
var style = switch (diag.get_severity()) {
|
||||
.Error => theme.editor_error,
|
||||
.Warning => theme.editor_warning,
|
||||
.Information => theme.editor_information,
|
||||
.Hint => theme.editor_hint,
|
||||
};
|
||||
if (hl_row) |hlr| if (hlr == diag.sel.begin.row) {
|
||||
style = .{ .fg = style.fg, .bg = theme.editor_line_highlight.bg };
|
||||
};
|
||||
|
||||
self.plane.cursor_move_yx(@intCast(pos.row), @intCast(pos.col)) catch return;
|
||||
self.render_diagnostic_cell(theme);
|
||||
self.render_diagnostic_cell(style);
|
||||
if (diag.sel.begin.row == diag.sel.end.row) {
|
||||
var col = pos.col;
|
||||
while (col < diag.sel.end.col) : (col += 1) {
|
||||
self.plane.cursor_move_yx(@intCast(pos.row), @intCast(col)) catch return;
|
||||
self.render_diagnostic_cell(style);
|
||||
}
|
||||
}
|
||||
const space_begin = get_line_end_space_begin(self.plane, screen_width, pos.row);
|
||||
if (space_begin < screen_width) {
|
||||
self.render_diagnostic_message(diag.message, pos.row, screen_width - space_begin, style);
|
||||
}
|
||||
}
|
||||
|
||||
inline fn render_diagnostic_cell(self: *const Self, theme: *const Widget.Theme) void {
|
||||
fn get_line_end_space_begin(plane: nc.Plane, screen_width: usize, screen_row: usize) usize {
|
||||
var pos = screen_width;
|
||||
var cell = plane.cell_init();
|
||||
while (pos > 0) : (pos -= 1) {
|
||||
plane.cursor_move_yx(@intCast(screen_row), @intCast(pos - 1)) catch return pos;
|
||||
const cell_egc_bytes = plane.at_cursor_cell(&cell) catch return pos;
|
||||
if (cell_egc_bytes > 0) return pos;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
fn render_diagnostic_message(self: *const Self, message: []const u8, y: usize, max_space: usize, style: Widget.Theme.Style) void {
|
||||
tui.set_style(&self.plane, style);
|
||||
_ = self.plane.print_aligned(@intCast(y), nc.Align.right, "{s}", .{message[0..@min(max_space, message.len)]}) catch {};
|
||||
}
|
||||
|
||||
inline fn render_diagnostic_cell(self: *const Self, _: Widget.Theme.Style) void {
|
||||
var cell = self.plane.cell_init();
|
||||
_ = self.plane.at_cursor_cell(&cell) catch return;
|
||||
tui.set_cell_style(&cell, theme.editor_error);
|
||||
tui.set_cell_style(&cell, .{ .fs = .undercurl });
|
||||
_ = self.plane.putc(&cell) catch {};
|
||||
}
|
||||
|
||||
|
@ -1095,6 +1151,10 @@ pub const Editor = struct {
|
|||
_ = try self.handlers.msg(.{ "E", "view", root.lines(), self.view.rows, self.view.row });
|
||||
}
|
||||
|
||||
fn send_editor_diagnostics(self: *const Self) !void {
|
||||
_ = try self.handlers.msg(.{ "E", "diag", self.diag_errors, self.diag_warnings, self.diag_info, self.diag_hints });
|
||||
}
|
||||
|
||||
fn send_editor_modified(self: *Self) !void {
|
||||
try self.send_editor_cursel_msg("modified", self.get_primary());
|
||||
}
|
||||
|
@ -3206,6 +3266,52 @@ pub const Editor = struct {
|
|||
try self.send_editor_jump_destination();
|
||||
}
|
||||
|
||||
pub fn goto_next_diagnostic(self: *Self, _: command.Context) tp.result {
|
||||
if (self.diagnostics.items.len == 0) return;
|
||||
self.sort_diagnostics();
|
||||
const primary = self.get_primary();
|
||||
for (self.diagnostics.items) |*diag| {
|
||||
if ((diag.sel.begin.row == primary.cursor.row and diag.sel.begin.col > primary.cursor.col) or diag.sel.begin.row > primary.cursor.row)
|
||||
return self.goto_diagnostic(diag);
|
||||
}
|
||||
return self.goto_diagnostic(&self.diagnostics.items[0]);
|
||||
}
|
||||
|
||||
pub fn goto_prev_diagnostic(self: *Self, _: command.Context) tp.result {
|
||||
if (self.diagnostics.items.len == 0) return;
|
||||
self.sort_diagnostics();
|
||||
const primary = self.get_primary();
|
||||
var i = self.diagnostics.items.len - 1;
|
||||
while (true) : (i -= 1) {
|
||||
const diag = &self.diagnostics.items[i];
|
||||
if ((diag.sel.begin.row == primary.cursor.row and diag.sel.begin.col < primary.cursor.col) or diag.sel.begin.row < primary.cursor.row)
|
||||
return self.goto_diagnostic(diag);
|
||||
if (i == 0) return self.goto_diagnostic(&self.diagnostics.items[self.diagnostics.items.len - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
fn goto_diagnostic(self: *Self, diag: *const Diagnostic) tp.result {
|
||||
const root = self.buf_root() catch return;
|
||||
const primary = self.get_primary();
|
||||
try self.send_editor_jump_source();
|
||||
self.cancel_all_selections();
|
||||
primary.cursor.move_to(root, diag.sel.begin.row, diag.sel.begin.col) catch |e| return tp.exit_error(e);
|
||||
self.clamp();
|
||||
try self.send_editor_jump_destination();
|
||||
}
|
||||
|
||||
fn sort_diagnostics(self: *Self) void {
|
||||
const less_fn = struct {
|
||||
fn less_fn(_: void, lhs: Diagnostic, rhs: Diagnostic) bool {
|
||||
return if (lhs.sel.begin.row == rhs.sel.begin.row)
|
||||
lhs.sel.begin.col < rhs.sel.begin.col
|
||||
else
|
||||
lhs.sel.begin.row < rhs.sel.begin.row;
|
||||
}
|
||||
}.less_fn;
|
||||
std.mem.sort(Diagnostic, self.diagnostics.items, {}, less_fn);
|
||||
}
|
||||
|
||||
pub fn goto_line(self: *Self, ctx: command.Context) tp.result {
|
||||
try self.send_editor_jump_source();
|
||||
var line: usize = 0;
|
||||
|
@ -3276,9 +3382,15 @@ pub const Editor = struct {
|
|||
pub fn clear_diagnostics(self: *Self, ctx: command.Context) tp.result {
|
||||
var file_path: []const u8 = undefined;
|
||||
if (!try ctx.args.match(.{tp.extract(&file_path)})) return tp.exit_error(error.InvalidArgument);
|
||||
file_path = project_manager.normalize_file_path(file_path);
|
||||
if (!std.mem.eql(u8, file_path, self.file_path orelse return)) return;
|
||||
for (self.diagnostics.items) |*d| d.deinit(self.diagnostics.allocator);
|
||||
self.diagnostics.clearRetainingCapacity();
|
||||
self.diag_errors = 0;
|
||||
self.diag_warnings = 0;
|
||||
self.diag_info = 0;
|
||||
self.diag_hints = 0;
|
||||
self.send_editor_diagnostics() catch {};
|
||||
}
|
||||
|
||||
pub fn add_diagnostic(self: *Self, ctx: command.Context) tp.result {
|
||||
|
@ -3302,7 +3414,6 @@ pub const Editor = struct {
|
|||
file_path = project_manager.normalize_file_path(file_path);
|
||||
if (!std.mem.eql(u8, file_path, self.file_path orelse return)) return;
|
||||
|
||||
self.logger.print("diag: {d}:{d} {s}", .{ sel.begin.row, sel.begin.col, message });
|
||||
(self.diagnostics.addOne() catch |e| return tp.exit_error(e)).* = .{
|
||||
.source = self.diagnostics.allocator.dupe(u8, source) catch |e| return tp.exit_error(e),
|
||||
.code = self.diagnostics.allocator.dupe(u8, code) catch |e| return tp.exit_error(e),
|
||||
|
@ -3310,6 +3421,15 @@ pub const Editor = struct {
|
|||
.severity = severity,
|
||||
.sel = sel,
|
||||
};
|
||||
|
||||
switch (Diagnostic.to_severity(severity)) {
|
||||
.Error => self.diag_errors += 1,
|
||||
.Warning => self.diag_warnings += 1,
|
||||
.Information => self.diag_info += 1,
|
||||
.Hint => self.diag_hints += 1,
|
||||
}
|
||||
self.send_editor_diagnostics() catch {};
|
||||
// self.logger.print("diag: {d} {d} {d}:{d} {s}", .{ self.diagnostics.items.len, severity, sel.begin.row, sel.begin.col, message });
|
||||
}
|
||||
|
||||
pub fn select(self: *Self, ctx: command.Context) tp.result {
|
||||
|
|
|
@ -105,7 +105,7 @@ fn update_width(self: *Self) void {
|
|||
if (!self.linenum) return;
|
||||
var buf: [31]u8 = undefined;
|
||||
const tmp = std.fmt.bufPrint(&buf, " {d} ", .{self.lines}) catch return;
|
||||
self.width = if (self.relative and tmp.len > 6) 6 else @max(tmp.len, 4);
|
||||
self.width = if (self.relative and tmp.len > 7) 7 else @max(tmp.len, 5);
|
||||
}
|
||||
|
||||
pub fn layout(self: *Self) Widget.Layout {
|
||||
|
@ -113,7 +113,7 @@ pub fn layout(self: *Self) Widget.Layout {
|
|||
}
|
||||
|
||||
inline fn get_width(self: *Self) usize {
|
||||
return if (self.linenum) self.width else 1;
|
||||
return if (self.linenum) self.width else 3;
|
||||
}
|
||||
|
||||
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
|
||||
|
@ -127,10 +127,28 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool {
|
|||
self.render_relative(theme)
|
||||
else
|
||||
self.render_linear(theme);
|
||||
} else {
|
||||
self.render_none(theme);
|
||||
}
|
||||
self.render_diagnostics(theme);
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn render_none(self: *Self, theme: *const Widget.Theme) void {
|
||||
var pos: usize = 0;
|
||||
var linenum = self.row + 1;
|
||||
var rows = self.rows;
|
||||
var diff_symbols = self.diff_symbols.items;
|
||||
while (rows > 0) : (rows -= 1) {
|
||||
if (linenum > self.lines) return;
|
||||
if (self.highlight and linenum == self.line + 1)
|
||||
self.render_line_highlight(pos, theme);
|
||||
self.render_diff_symbols(&diff_symbols, pos, linenum, theme);
|
||||
pos += 1;
|
||||
linenum += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_linear(self: *Self, theme: *const Widget.Theme) void {
|
||||
var pos: usize = 0;
|
||||
var linenum = self.row + 1;
|
||||
|
@ -218,6 +236,34 @@ inline fn render_diff_symbols(self: *Self, diff_symbols: *[]Symbol, pos: usize,
|
|||
_ = self.plane.putc(&cell) catch {};
|
||||
}
|
||||
|
||||
fn render_diagnostics(self: *Self, theme: *const Widget.Theme) void {
|
||||
for (self.editor.diagnostics.items) |*diag| self.render_diagnostic(diag, theme);
|
||||
}
|
||||
|
||||
fn render_diagnostic(self: *Self, diag: *const ed.Diagnostic, theme: *const Widget.Theme) void {
|
||||
const row = diag.sel.begin.row;
|
||||
if (!(self.row < row and row < self.row + self.rows)) return;
|
||||
const style = switch (diag.get_severity()) {
|
||||
.Error => theme.editor_error,
|
||||
.Warning => theme.editor_warning,
|
||||
.Information => theme.editor_information,
|
||||
.Hint => theme.editor_hint,
|
||||
};
|
||||
const icon = switch (diag.get_severity()) {
|
||||
.Error => "",
|
||||
.Warning => "",
|
||||
.Information => "",
|
||||
.Hint => "",
|
||||
};
|
||||
const y = row - self.row;
|
||||
self.plane.cursor_move_yx(@intCast(y), 0) catch return;
|
||||
var cell = self.plane.cell_init();
|
||||
_ = self.plane.at_cursor_cell(&cell) catch return;
|
||||
tui.set_cell_style_fg(&cell, style);
|
||||
_ = self.plane.cell_load(&cell, icon) catch {};
|
||||
_ = self.plane.putc(&cell) catch {};
|
||||
}
|
||||
|
||||
fn primary_click(self: *const Self, y: i32) error{Exit}!bool {
|
||||
var line = self.row + 1;
|
||||
line += @intCast(y);
|
||||
|
|
|
@ -136,8 +136,8 @@ fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
|
|||
},
|
||||
mod.ALT => switch (keynormal) {
|
||||
'J' => self.cmd("join_next_line", .{}),
|
||||
'N' => self.cmd("goto_next_match", .{}),
|
||||
'P' => self.cmd("goto_prev_match", .{}),
|
||||
'N' => self.cmd("goto_next_diagnostic", .{}),
|
||||
'P' => self.cmd("goto_prev_diagnostic", .{}),
|
||||
'L' => self.cmd("toggle_logview", .{}),
|
||||
'I' => self.cmd("toggle_inputview", .{}),
|
||||
'B' => self.cmd("move_word_left", .{}),
|
||||
|
|
64
src/tui/status/diagstate.zig
Normal file
64
src/tui/status/diagstate.zig
Normal file
|
@ -0,0 +1,64 @@
|
|||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const nc = @import("notcurses");
|
||||
const tp = @import("thespian");
|
||||
const tracy = @import("tracy");
|
||||
|
||||
const Widget = @import("../Widget.zig");
|
||||
const Button = @import("../Button.zig");
|
||||
const tui = @import("../tui.zig");
|
||||
const command = @import("../command.zig");
|
||||
|
||||
errors: usize = 0,
|
||||
warnings: usize = 0,
|
||||
info: usize = 0,
|
||||
hints: usize = 0,
|
||||
buf: [256]u8 = undefined,
|
||||
rendered: [:0]const u8 = "",
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
|
||||
return Button.create_widget(Self, a, parent, .{
|
||||
.ctx = .{},
|
||||
.label = "",
|
||||
.on_click = on_click,
|
||||
.on_layout = layout,
|
||||
.on_render = render,
|
||||
.on_receive = receive,
|
||||
});
|
||||
}
|
||||
|
||||
fn on_click(_: *Self, _: *Button.State(Self)) void {
|
||||
command.executeName("goto_next_diagnostic", .{}) catch {};
|
||||
}
|
||||
|
||||
pub fn layout(self: *Self, _: *Button.State(Self)) Widget.Layout {
|
||||
return .{ .static = self.rendered.len };
|
||||
}
|
||||
|
||||
pub fn render(self: *Self, btn: *Button.State(Self), theme: *const Widget.Theme) bool {
|
||||
const bg_style = if (btn.active) theme.editor_cursor else if (btn.hover) theme.statusbar_hover else theme.statusbar;
|
||||
tui.set_base_style(&btn.plane, " ", bg_style);
|
||||
btn.plane.erase();
|
||||
btn.plane.home();
|
||||
_ = btn.plane.putstr(self.rendered) catch {};
|
||||
return false;
|
||||
}
|
||||
|
||||
fn format(self: *Self) void {
|
||||
var fbs = std.io.fixedBufferStream(&self.buf);
|
||||
const writer = fbs.writer();
|
||||
if (self.errors > 0) std.fmt.format(writer, " {d}", .{self.errors}) catch {};
|
||||
if (self.warnings > 0) std.fmt.format(writer, " {d}", .{self.warnings}) catch {};
|
||||
if (self.info > 0) std.fmt.format(writer, " {d}", .{self.info}) catch {};
|
||||
if (self.hints > 0) std.fmt.format(writer, " {d}", .{self.hints}) catch {};
|
||||
self.rendered = @ptrCast(fbs.getWritten());
|
||||
self.buf[self.rendered.len] = 0;
|
||||
}
|
||||
|
||||
pub fn receive(self: *Self, _: *Button.State(Self), _: tp.pid_ref, m: tp.message) error{Exit}!bool {
|
||||
if (try m.match(.{ "E", "diag", tp.extract(&self.errors), tp.extract(&self.warnings), tp.extract(&self.info), tp.extract(&self.hints) }))
|
||||
self.format();
|
||||
return false;
|
||||
}
|
|
@ -16,6 +16,7 @@ pub fn create(a: std.mem.Allocator, parent: Widget) !Widget {
|
|||
try w.add(try @import("filestate.zig").create(a, w.plane));
|
||||
try w.add(try @import("minilog.zig").create(a, w.plane));
|
||||
if (tui.current().config.selectionstate_show) try w.add(try @import("selectionstate.zig").create(a, w.plane));
|
||||
try w.add(try @import("diagstate.zig").create(a, w.plane));
|
||||
try w.add(try @import("linenumstate.zig").create(a, w.plane));
|
||||
if (tui.current().config.modstate_show) try w.add(try @import("modstate.zig").create(a, w.plane));
|
||||
if (tui.current().config.keystate_show) try w.add(try @import("keystate.zig").create(a, w.plane));
|
||||
|
|
Loading…
Add table
Reference in a new issue