diff --git a/help.md b/help.md index cee90df..ba97eff 100644 --- a/help.md +++ b/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 diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 602e7dc..dae1855 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -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| { - self.plane.cursor_move_yx(@intCast(pos.row), @intCast(pos.col)) catch return; - self.render_diagnostic_cell(theme); + 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(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 { diff --git a/src/tui/editor_gutter.zig b/src/tui/editor_gutter.zig index d79ca88..d4adf59 100644 --- a/src/tui/editor_gutter.zig +++ b/src/tui/editor_gutter.zig @@ -104,8 +104,8 @@ pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { 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); + const tmp = std.fmt.bufPrint(&buf, " {d} ", .{self.lines}) catch return; + 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); diff --git a/src/tui/mode/input/flow.zig b/src/tui/mode/input/flow.zig index 1caff3d..5970fe9 100644 --- a/src/tui/mode/input/flow.zig +++ b/src/tui/mode/input/flow.zig @@ -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", .{}), diff --git a/src/tui/status/diagstate.zig b/src/tui/status/diagstate.zig new file mode 100644 index 0000000..981a63a --- /dev/null +++ b/src/tui/status/diagstate.zig @@ -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; +} diff --git a/src/tui/status/statusbar.zig b/src/tui/status/statusbar.zig index da5fdbd..3e3e372 100644 --- a/src/tui/status/statusbar.zig +++ b/src/tui/status/statusbar.zig @@ -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));