const std = @import("std"); const builtin = @import("builtin"); const tp = @import("thespian"); const cbor = @import("cbor"); const log = @import("log"); const Buffer = @import("Buffer"); const ripgrep = @import("ripgrep"); const tracy = @import("tracy"); const text_manip = @import("text_manip"); const syntax = @import("syntax"); const project_manager = @import("project_manager"); const CaseData = @import("CaseData"); const root_mod = @import("root"); const Plane = @import("renderer").Plane; const Cell = @import("renderer").Cell; const key = @import("renderer").input.key; const event_type = @import("renderer").input.event_type; const scrollbar_v = @import("scrollbar_v.zig"); const editor_gutter = @import("editor_gutter.zig"); const EventHandler = @import("EventHandler.zig"); const Widget = @import("Widget.zig"); const WidgetList = @import("WidgetList.zig"); const command = @import("command.zig"); const tui = @import("tui.zig"); const module = @This(); pub const Cursor = Buffer.Cursor; pub const View = Buffer.View; pub const Selection = Buffer.Selection; const Allocator = std.mem.Allocator; const copy = std.mem.copy; const fmt = std.fmt; const time = std.time; const scroll_step_small = 3; const scroll_page_ratio = 3; const scroll_cursor_min_border_distance = 5; const scroll_cursor_min_border_distance_mouse = 1; const double_click_time_ms = 350; pub const max_matches = if (builtin.mode == std.builtin.OptimizeMode.Debug) 10_000 else 100_000; pub const max_match_lines = 15; pub const max_match_batch = if (builtin.mode == std.builtin.OptimizeMode.Debug) 100 else 1000; pub const Match = struct { begin: Cursor = Cursor{}, end: Cursor = Cursor{}, has_selection: bool = false, style: ?Widget.Theme.Style = null, const List = std.ArrayList(?Self); const Self = @This(); pub fn from_selection(sel: Selection) Self { return .{ .begin = sel.begin, .end = sel.end }; } pub fn to_selection(self: *const Self) Selection { return .{ .begin = self.begin, .end = self.end }; } fn nudge_insert(self: *Self, nudge: Selection) void { self.begin.nudge_insert(nudge); self.end.nudge_insert(nudge); } fn nudge_delete(self: *Self, nudge: Selection) bool { if (!self.begin.nudge_delete(nudge)) return false; return self.end.nudge_delete(nudge); } }; pub const CurSel = struct { cursor: Cursor = Cursor{}, selection: ?Selection = null, const List = std.ArrayList(?Self); const Self = @This(); pub inline fn invalid() Self { return .{ .cursor = Cursor.invalid() }; } inline fn reset(self: *Self) void { self.* = .{}; } fn enable_selection(self: *Self) *Selection { return if (self.selection) |*sel| sel else cod: { self.selection = Selection.from_cursor(&self.cursor); break :cod &self.selection.?; }; } fn check_selection(self: *Self) void { if (self.selection) |sel| if (sel.empty()) { self.selection = null; }; } fn expand_selection_to_line(self: *Self, root: Buffer.Root, plane: Plane) *Selection { const sel = self.enable_selection(); sel.normalize(); sel.begin.move_begin(); if (!(sel.end.row > sel.begin.row and sel.end.col == 0)) { sel.end.move_end(root, plane.metrics()); sel.end.move_right(root, plane.metrics()) catch {}; } return sel; } fn write(self: *const Self, writer: Buffer.MetaWriter) !void { try self.cursor.write(writer); if (self.selection) |sel| { try sel.write(writer); } else { try cbor.writeValue(writer, null); } } fn extract(self: *Self, iter: *[]const u8) !bool { if (!try self.cursor.extract(iter)) return false; var iter2 = iter.*; if (try cbor.matchValue(&iter2, cbor.null_)) { iter.* = iter2; } else { var sel: Selection = .{}; if (!try sel.extract(iter)) return false; self.selection = sel; } return true; } fn nudge_insert(self: *Self, nudge: Selection) void { if (self.selection) |*sel_| sel_.nudge_insert(nudge); self.cursor.nudge_insert(nudge); } fn nudge_delete(self: *Self, nudge: Selection) bool { if (self.selection) |*sel_| if (!sel_.nudge_delete(nudge)) return false; return self.cursor.nudge_delete(nudge); } }; pub const Diagnostic = struct { source: []const u8, code: []const u8, message: []const u8, severity: i32, sel: Selection, fn deinit(self: *Diagnostic, allocator: std.mem.Allocator) void { allocator.free(self.source); allocator.free(self.code); allocator.free(self.message); } pub 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 { const SelectMode = enum { char, word, line, }; const Self = @This(); pub const Target = Self; allocator: Allocator, plane: Plane, logger: log.Logger, file_path: ?[]const u8, buffer: ?*Buffer, lsp_version: usize = 1, cursels: CurSel.List, cursels_saved: CurSel.List, selection_mode: SelectMode = .char, clipboard: ?[]const u8 = null, target_column: ?Cursor = null, filter: ?struct { before_root: Buffer.Root, work_root: Buffer.Root, begin: Cursor, pos: CurSel, old_primary: CurSel, old_primary_reversed: bool, whole_file: ?std.ArrayList(u8), bytes: usize = 0, chunks: usize = 0, } = null, matches: Match.List, match_token: usize = 0, match_done_token: usize = 0, last_find_query: ?[]const u8 = null, find_history: ?std.ArrayList([]const u8) = null, find_operation: ?enum { goto_next_match, goto_prev_match } = null, prefix_buf: [8]u8 = undefined, prefix: []const u8 = &[_]u8{}, view: View = View{}, handlers: EventHandler.List, scroll_dest: usize = 0, fast_scroll: bool = false, jump_mode: bool = false, animation_step: usize = 0, animation_frame_rate: i64, animation_lag: f64, animation_last_time: i64, enable_terminal_cursor: bool, show_whitespace: bool, last: struct { root: ?Buffer.Root = null, primary: CurSel = CurSel.invalid(), view: View = View.invalid(), matches: usize = 0, cursels: usize = 0, dirty: bool = false, } = .{}, syntax: ?*syntax = null, syntax_refresh_full: bool = false, syntax_token: usize = 0, style_cache: ?StyleCache = null, 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, need_save_after_filter: bool = false, const StyleCache = std.AutoHashMap(u32, ?Widget.Theme.Token); const Context = command.Context; const Result = command.Result; pub fn write_state(self: *const Self, writer: Buffer.MetaWriter) !void { try cbor.writeArrayHeader(writer, 6); try cbor.writeValue(writer, self.file_path orelse ""); try cbor.writeValue(writer, self.clipboard orelse ""); try cbor.writeValue(writer, self.last_find_query orelse ""); if (self.find_history) |history| { try cbor.writeArrayHeader(writer, history.items.len); for (history.items) |item| try cbor.writeValue(writer, item); } else { try cbor.writeArrayHeader(writer, 0); } try self.view.write(writer); try self.get_primary().cursor.write(writer); } pub fn extract_state(self: *Self, buf: []const u8) !void { var file_path: []const u8 = undefined; var view_cbor: []const u8 = undefined; var primary_cbor: []const u8 = undefined; var clipboard: []const u8 = undefined; var query: []const u8 = undefined; var find_history: []const u8 = undefined; if (!try cbor.match(buf, .{ tp.extract(&file_path), tp.extract(&clipboard), tp.extract(&query), tp.extract_cbor(&find_history), tp.extract_cbor(&view_cbor), tp.extract_cbor(&primary_cbor), })) return error.RestoreStateMatch; try self.open(file_path); self.clipboard = if (clipboard.len > 0) try self.allocator.dupe(u8, clipboard) else null; self.last_find_query = if (query.len > 0) try self.allocator.dupe(u8, clipboard) else null; if (!try self.view.extract(&view_cbor)) return error.RestoreView; self.scroll_dest = self.view.row; if (!try self.get_primary().cursor.extract(&primary_cbor)) return error.RestoreCursor; var len = cbor.decodeArrayHeader(&find_history) catch return error.RestoryFindHistory; while (len > 0) : (len -= 1) { var value: []const u8 = undefined; if (!(cbor.matchValue(&find_history, cbor.extract(&value)) catch return error.RestoryFindHistory)) return error.RestoryFindHistory; self.push_find_history(value); } } fn init(self: *Self, allocator: Allocator, n: Plane) void { const logger = log.logger("editor"); var frame_rate = tp.env.get().num("frame-rate"); if (frame_rate == 0) frame_rate = 60; self.* = Self{ .allocator = allocator, .plane = n, .logger = logger, .file_path = null, .buffer = null, .handlers = EventHandler.List.init(allocator), .animation_lag = get_animation_max_lag(), .animation_frame_rate = frame_rate, .animation_last_time = time.microTimestamp(), .cursels = CurSel.List.init(allocator), .cursels_saved = CurSel.List.init(allocator), .matches = Match.List.init(allocator), .enable_terminal_cursor = tui.current().config.enable_terminal_cursor, .show_whitespace = tui.current().config.show_whitespace, .diagnostics = std.ArrayList(Diagnostic).init(allocator), }; } fn deinit(self: *Self) void { for (self.diagnostics.items) |*d| d.deinit(self.diagnostics.allocator); self.diagnostics.deinit(); if (self.syntax) |syn| syn.destroy(); self.cursels.deinit(); self.matches.deinit(); self.handlers.deinit(); self.logger.deinit(); if (self.buffer) |p| p.deinit(); } fn need_render(_: *Self) void { Widget.need_render(); } fn buf_for_update(self: *Self) !*const Buffer { self.cursels_saved.clearAndFree(); self.cursels_saved = try self.cursels.clone(); return self.buffer orelse error.Stop; } fn buf_root(self: *const Self) !Buffer.Root { return if (self.buffer) |p| p.root else error.Stop; } fn buf_a(self: *const Self) !Allocator { return if (self.buffer) |p| p.allocator else error.Stop; } pub fn get_current_root(self: *const Self) ?Buffer.Root { return if (self.buffer) |p| p.root else null; } pub fn handle_resize(self: *Self, pos: Widget.Box) void { self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return; self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return; self.view.rows = pos.h; self.view.cols = pos.w; } pub fn is_dirty(self: *Self) bool { const b = self.buffer orelse return false; return b.is_dirty(); } fn open(self: *Self, file_path: []const u8) !void { var new_buf = try Buffer.create(self.allocator); errdefer new_buf.deinit(); try new_buf.load_from_file_and_update(file_path); return self.open_buffer(file_path, new_buf); } fn open_scratch(self: *Self, file_path: []const u8, content: []const u8) !void { var new_buf = try Buffer.create(self.allocator); errdefer new_buf.deinit(); try new_buf.load_from_string_and_update(file_path, content); new_buf.file_exists = true; return self.open_buffer(file_path, new_buf); } fn open_buffer(self: *Self, file_path: []const u8, new_buf: *Buffer) !void { errdefer new_buf.deinit(); self.cancel_all_selections(); self.get_primary().reset(); self.file_path = try self.allocator.dupe(u8, file_path); if (self.buffer) |_| try self.close(); self.buffer = new_buf; self.syntax = if (tp.env.get().is("no-syntax")) null else syntax: { if (new_buf.root.lines() > root_mod.max_syntax_lines) break :syntax null; const lang_override = tp.env.get().str("language"); var content = std.ArrayList(u8).init(self.allocator); defer content.deinit(); try new_buf.root.store(content.writer()); const syn = if (lang_override.len > 0) syntax.create_file_type(self.allocator, content.items, lang_override) catch null else syntax.create_guess_file_type(self.allocator, content.items, self.file_path) catch null; if (syn) |syn_| project_manager.did_open(file_path, syn_.file_type, self.lsp_version, try content.toOwnedSlice()) catch {}; break :syntax syn; }; const ftn = if (self.syntax) |syn| syn.file_type.name else "text"; const fti = if (self.syntax) |syn| syn.file_type.icon else "🖹"; const ftc = if (self.syntax) |syn| syn.file_type.color else 0x000000; try self.send_editor_open(file_path, new_buf.file_exists, ftn, fti, ftc); } fn close(self: *Self) !void { return self.close_internal(false); } fn close_dirty(self: *Self) !void { return self.close_internal(true); } fn close_internal(self: *Self, allow_dirty_close: bool) !void { const b = self.buffer orelse return error.Stop; if (!allow_dirty_close and b.is_dirty()) return tp.exit("unsaved changes"); if (self.buffer) |b_mut| b_mut.deinit(); self.buffer = null; self.plane.erase(); self.plane.home(); tui.current().rdr.cursor_disable(); _ = try self.handlers.msg(.{ "E", "close" }); if (self.syntax) |_| if (self.file_path) |file_path| project_manager.did_close(file_path) catch {}; } fn save(self: *Self) !void { const b = self.buffer orelse return error.Stop; if (!b.is_dirty()) return self.logger.print("no changes to save", .{}); if (self.file_path) |file_path| { if (self.buffer) |b_mut| try b_mut.store_to_file_and_clean(file_path); } else return error.SaveNoFileName; try self.send_editor_save(self.file_path.?); self.last.dirty = false; } fn save_as(self: *Self, file_path: []const u8) !void { if (self.buffer) |b_mut| try b_mut.store_to_file_and_clean(file_path); if (self.file_path) |old_file_path| self.allocator.free(old_file_path); self.file_path = try self.allocator.dupe(u8, file_path); try self.send_editor_save(self.file_path.?); self.last.dirty = false; } pub fn push_cursor(self: *Self) !void { const primary = self.cursels.getLastOrNull() orelse CurSel{} orelse CurSel{}; (try self.cursels.addOne()).* = primary; } pub fn pop_cursor(self: *Self, _: Context) Result { if (self.cursels.items.len > 1) { const cursel = self.cursels.popOrNull() orelse return orelse return; if (cursel.selection) |sel| if (self.find_selection_match(sel)) |match| { match.has_selection = false; }; } self.clamp(); } pub fn get_primary(self: *const Self) *CurSel { var idx = self.cursels.items.len; while (idx > 0) : (idx -= 1) if (self.cursels.items[idx - 1]) |*primary| return primary; if (idx == 0) { self.logger.print("ERROR: no more cursors", .{}); (@constCast(self).cursels.addOne() catch |e| switch (e) { error.OutOfMemory => @panic("get_primary error.OutOfMemory"), }).* = CurSel{}; } return self.get_primary(); } fn store_undo_meta(self: *Self, allocator: Allocator) ![]u8 { var meta = std.ArrayList(u8).init(allocator); const writer = meta.writer(); for (self.cursels_saved.items) |*cursel_| if (cursel_.*) |*cursel| try cursel.write(writer); return meta.toOwnedSlice(); } fn store_current_undo_meta(self: *Self, allocator: Allocator) ![]u8 { var meta = std.ArrayList(u8).init(allocator); const writer = meta.writer(); for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| try cursel.write(writer); return meta.toOwnedSlice(); } fn update_buf(self: *Self, root: Buffer.Root) !void { const b = self.buffer orelse return error.Stop; var sfa = std.heap.stackFallback(512, self.allocator); const allocator = sfa.get(); const meta = try self.store_undo_meta(allocator); defer allocator.free(meta); try b.store_undo(meta); b.update(root); try self.send_editor_modified(); } fn restore_undo_redo_meta(self: *Self, meta: []const u8) !void { if (meta.len > 0) self.clear_all_cursors(); var iter = meta; while (iter.len > 0) { var cursel: CurSel = .{}; if (!try cursel.extract(&iter)) return error.SyntaxError; (try self.cursels.addOne()).* = cursel; } } fn restore_undo(self: *Self) !void { if (self.buffer) |b_mut| { try self.send_editor_jump_source(); self.cancel_all_matches(); var sfa = std.heap.stackFallback(512, self.allocator); const allocator = sfa.get(); const redo_meta = try self.store_current_undo_meta(allocator); defer allocator.free(redo_meta); const meta = b_mut.undo(redo_meta) catch |e| switch (e) { error.Stop => { self.logger.print("nothing to undo", .{}); return; }, else => return e, }; try self.restore_undo_redo_meta(meta); try self.send_editor_jump_destination(); self.reset_syntax(); } } fn restore_redo(self: *Self) !void { if (self.buffer) |b_mut| { try self.send_editor_jump_source(); self.cancel_all_matches(); const meta = b_mut.redo() catch |e| switch (e) { error.Stop => { self.logger.print("nothing to redo", .{}); return; }, else => return e, }; try self.restore_undo_redo_meta(meta); try self.send_editor_jump_destination(); self.reset_syntax(); } } fn find_first_non_ws(root: Buffer.Root, row: usize, plane: Plane) usize { const Ctx = struct { col: usize = 0, fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize, _: Buffer.Metrics) Buffer.Walker { const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); if (egc[0] == ' ' or egc[0] == '\t') { ctx.col += wcwidth; return Buffer.Walker.keep_walking; } return Buffer.Walker.stop; } }; var ctx: Ctx = .{}; root.walk_egc_forward(row, Ctx.walker, &ctx, plane.metrics()) catch return 0; return ctx.col; } fn write_range( self: *const Self, root: Buffer.Root, sel: Selection, writer: anytype, map_error: fn (e: anyerror, stack_trace: ?*std.builtin.StackTrace) @TypeOf(writer).Error, wcwidth_: ?*usize, plane_: Plane, ) @TypeOf(writer).Error!void { _ = self; const Writer = @TypeOf(writer); const Ctx = struct { col: usize = 0, sel: Selection, writer: Writer, wcwidth: usize = 0, fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize, _: Buffer.Metrics) Buffer.Walker { const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); if (ctx.col < ctx.sel.begin.col) { ctx.col += wcwidth; return Buffer.Walker.keep_walking; } _ = ctx.writer.write(egc) catch |e| return Buffer.Walker{ .err = e }; ctx.wcwidth += wcwidth; if (egc[0] == '\n') { ctx.col = 0; ctx.sel.begin.col = 0; ctx.sel.begin.row += 1; } else { ctx.col += wcwidth; ctx.sel.begin.col += wcwidth; } return if (ctx.sel.begin.eql(ctx.sel.end)) Buffer.Walker.stop else Buffer.Walker.keep_walking; } }; var ctx: Ctx = .{ .sel = sel, .writer = writer }; ctx.sel.normalize(); if (sel.begin.eql(sel.end)) return; root.walk_egc_forward(sel.begin.row, Ctx.walker, &ctx, plane_.metrics()) catch |e| return map_error(e, @errorReturnTrace()); if (wcwidth_) |p| p.* = ctx.wcwidth; } pub fn update(self: *Self) void { self.update_scroll(); self.update_event() catch {}; } pub fn render(self: *Self, theme: *const Widget.Theme) bool { const frame = tracy.initZone(@src(), .{ .name = "editor render" }); defer frame.deinit(); self.update_syntax() catch |e| switch (e) { error.Stop => {}, else => self.logger.err("update_syntax", e), }; if (self.style_cache) |*cache| { if (!std.mem.eql(u8, self.style_cache_theme, theme.name)) { cache.deinit(); self.style_cache = StyleCache.init(self.allocator); // self.logger.print("style_cache reset {s} -> {s}", .{ self.style_cache_theme, theme.name }); } } else { self.style_cache = StyleCache.init(self.allocator); } self.style_cache_theme = theme.name; const cache: *StyleCache = &self.style_cache.?; self.render_screen(theme, cache); return self.scroll_dest != self.view.row; } fn render_screen(self: *Self, theme: *const Widget.Theme, cache: *StyleCache) void { const ctx = struct { self: *Self, buf_row: usize, buf_col: usize = 0, match_idx: usize = 0, theme: *const Widget.Theme, hl_row: ?usize, fn walker(ctx_: *anyopaque, leaf: *const Buffer.Leaf, _: Buffer.Metrics) Buffer.Walker { const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); const self_ = ctx.self; const view = self_.view; const n = &self_.plane; if (ctx.buf_row > view.row + view.rows) return Buffer.Walker.stop; const bufsize = 4095; var bufstatic: [bufsize:0]u8 = undefined; const len = leaf.buf.len; var chunk_alloc: ?[:0]u8 = null; var chunk: [:0]u8 = if (len > bufsize) ret: { const ptr = self_.allocator.allocSentinel(u8, len, 0) catch |e| return Buffer.Walker{ .err = e }; chunk_alloc = ptr; break :ret ptr; } else &bufstatic; defer if (chunk_alloc) |p| self_.allocator.free(p); @memcpy(chunk[0..leaf.buf.len], leaf.buf); chunk[leaf.buf.len] = 0; chunk.len = leaf.buf.len; while (chunk.len > 0) { if (ctx.buf_col >= view.col + view.cols) break; var cell = n.cell_init(); const c = &cell; const bytes, const colcount = switch (chunk[0]) { 0...8, 10...31 => |code| ctx.self.render_control_code(c, n, code, ctx.theme), 32 => ctx.self.render_space(c, n, ctx.theme), 9 => ctx.self.render_tab(c, n, ctx.buf_col, ctx.theme), else => render_egc(c, n, chunk), }; if (ctx.hl_row) |hl_row| if (hl_row == ctx.buf_row) self_.render_line_highlight_cell(ctx.theme, c); self_.render_matches(&ctx.match_idx, ctx.theme, c); self_.render_selections(ctx.theme, c); const advanced = if (ctx.buf_col >= view.col) n.putc(c) catch break else colcount; const new_col = ctx.buf_col + colcount - advanced; if (ctx.buf_col < view.col and ctx.buf_col + advanced > view.col) n.cursor_move_rel(0, @intCast(ctx.buf_col + advanced - view.col)) catch {}; ctx.buf_col += advanced; while (ctx.buf_col < new_col) { if (ctx.buf_col >= view.col + view.cols) break; var cell_ = n.cell_init(); const c_ = &cell_; if (ctx.hl_row) |hl_row| if (hl_row == ctx.buf_row) self_.render_line_highlight_cell(ctx.theme, c_); self_.render_matches(&ctx.match_idx, ctx.theme, c_); self_.render_selections(ctx.theme, c_); const advanced_ = n.putc(c_) catch break; ctx.buf_col += advanced_; } chunk = chunk[bytes..]; } if (leaf.eol) { var c = ctx.self.render_eol(n, ctx.theme); if (ctx.hl_row) |hl_row| if (hl_row == ctx.buf_row) self_.render_line_highlight_cell(ctx.theme, &c); self_.render_matches(&ctx.match_idx, ctx.theme, &c); self_.render_selections(ctx.theme, &c); _ = n.putc(&c) catch {}; var term_cell = render_terminator(n, ctx.theme); if (ctx.hl_row) |hl_row| if (hl_row == ctx.buf_row) self_.render_line_highlight_cell(ctx.theme, &term_cell); _ = n.putc(&term_cell) catch {}; n.cursor_move_yx(-1, 0) catch |e| return Buffer.Walker{ .err = e }; n.cursor_move_rel(1, 0) catch |e| return Buffer.Walker{ .err = e }; ctx.buf_row += 1; ctx.buf_col = 0; } return Buffer.Walker.keep_walking; } }; const hl_row: ?usize = if (tui.current().config.highlight_current_line) self.get_primary().cursor.row else null; var ctx_: ctx = .{ .self = self, .buf_row = self.view.row, .theme = theme, .hl_row = hl_row }; const root = self.buf_root() catch return; { const frame = tracy.initZone(@src(), .{ .name = "editor render screen" }); defer frame.deinit(); self.plane.set_base_style(" ", theme.editor); self.plane.erase(); if (hl_row) |_| self.render_line_highlight(&self.get_primary().cursor, theme) catch {}; self.plane.home(); _ = root.walk_from_line_begin_const(self.view.row, ctx.walker, &ctx_, self.plane.metrics()) catch {}; } self.render_syntax(theme, cache, root) catch {}; self.render_diagnostics(theme, hl_row) catch {}; self.render_cursors(theme) catch {}; } fn render_terminal_cursor(self: *const Self, cursor_: *const Cursor) !void { if (self.screen_cursor(cursor_)) |cursor| { const y, const x = self.plane.rel_yx_to_abs(@intCast(cursor.row), @intCast(cursor.col)); const shape = if (tui.current().input_mode) |mode| mode.cursor_shape else .block; tui.current().rdr.cursor_enable(y, x, shape) catch {}; } else { tui.current().rdr.cursor_disable(); } } fn render_cursors(self: *Self, theme: *const Widget.Theme) !void { const frame = tracy.initZone(@src(), .{ .name = "editor render cursors" }); defer frame.deinit(); if (self.cursels.items.len == 1 and self.enable_terminal_cursor) return self.render_terminal_cursor(&self.get_primary().cursor); for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| try self.render_cursor(&cursel.cursor, theme); if (self.enable_terminal_cursor) try self.render_terminal_cursor(&self.get_primary().cursor); } fn render_cursor(self: *Self, cursor: *const Cursor, theme: *const Widget.Theme) !void { if (self.screen_cursor(cursor)) |pos| { self.plane.cursor_move_yx(@intCast(pos.row), @intCast(pos.col)) catch return; self.render_cursor_cell(theme); } } fn render_line_highlight(self: *Self, cursor: *const Cursor, theme: *const Widget.Theme) !void { const row_min = self.view.row; const row_max = row_min + self.view.rows; if (cursor.row < row_min or row_max < cursor.row) return; const row = cursor.row - self.view.row; for (0..self.view.cols) |i| { self.plane.cursor_move_yx(@intCast(row), @intCast(i)) catch return; var cell = self.plane.cell_init(); _ = self.plane.at_cursor_cell(&cell) catch return; self.render_line_highlight_cell(theme, &cell); _ = self.plane.putc(&cell) catch {}; } } fn render_matches(self: *const Self, last_idx: *usize, theme: *const Widget.Theme, cell: *Cell) void { var y: c_uint = undefined; var x: c_uint = undefined; self.plane.cursor_yx(&y, &x); while (true) { if (last_idx.* >= self.matches.items.len) return; const sel = if (self.matches.items[last_idx.*]) |sel_| sel_ else { last_idx.* += 1; continue; }; if (self.is_point_before_selection(sel, y, x)) return; if (self.is_point_in_selection(sel, y, x)) return self.render_match_cell(theme, cell, sel); last_idx.* += 1; } } fn render_selections(self: *const Self, theme: *const Widget.Theme, cell: *Cell) void { var y: c_uint = undefined; var x: c_uint = undefined; self.plane.cursor_yx(&y, &x); for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel_| { var sel = sel_; sel.normalize(); if (self.is_point_in_selection(sel, y, x)) return self.render_selection_cell(theme, cell); }; } fn render_diagnostics(self: *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: *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); } } fn get_line_end_space_begin(plane: *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: *Self, message: []const u8, y: usize, max_space: usize, style: Widget.Theme.Style) void { self.plane.set_style(style); _ = self.plane.print_aligned_right(@intCast(y), "{s}", .{message[0..@min(max_space, message.len)]}) catch {}; } inline fn render_diagnostic_cell(self: *Self, style: Widget.Theme.Style) void { var cell = self.plane.cell_init(); _ = self.plane.at_cursor_cell(&cell) catch return; cell.set_style(.{ .fs = .undercurl }); if (style.fg) |ul_col| cell.set_under_color(ul_col); _ = self.plane.putc(&cell) catch {}; } inline fn render_cursor_cell(self: *Self, theme: *const Widget.Theme) void { var cell = self.plane.cell_init(); _ = self.plane.at_cursor_cell(&cell) catch return; cell.set_style(theme.editor_cursor); _ = self.plane.putc(&cell) catch {}; } inline fn render_selection_cell(_: *const Self, theme: *const Widget.Theme, cell: *Cell) void { cell.set_style_bg(theme.editor_selection); } inline fn render_match_cell(_: *const Self, theme: *const Widget.Theme, cell: *Cell, match: Match) void { cell.set_style_bg(if (match.style) |style| style else theme.editor_match); } inline fn render_line_highlight_cell(_: *const Self, theme: *const Widget.Theme, cell: *Cell) void { cell.set_style_bg(theme.editor_line_highlight); } inline fn render_control_code(self: *const Self, c: *Cell, n: *Plane, code: u8, theme: *const Widget.Theme) struct { usize, usize } { const val = Buffer.unicode.control_code_to_unicode(code); if (self.show_whitespace) c.set_style(theme.editor_whitespace); _ = n.cell_load(c, val) catch {}; return .{ 1, 1 }; } inline fn render_eol(self: *const Self, n: *Plane, theme: *const Widget.Theme) Cell { var cell = n.cell_init(); const c = &cell; if (self.show_whitespace) { c.set_style(theme.editor_whitespace); //_ = n.cell_load(c, "$") catch {}; //_ = n.cell_load(c, " ") catch {}; //_ = n.cell_load(c, "⏎") catch {}; // _ = n.cell_load(c, "󰌑") catch {}; _ = n.cell_load(c, "↩") catch {}; //_ = n.cell_load(c, "↲") catch {}; //_ = n.cell_load(c, "⤶") catch {}; //_ = n.cell_load(c, "󱞱") catch {}; //_ = n.cell_load(c, "󱞲") catch {}; //_ = n.cell_load(c, "⤦") catch {}; //_ = n.cell_load(c, "¬") catch {}; //_ = n.cell_load(c, "␤") catch {}; //_ = n.cell_load(c, "❯") catch {}; //_ = n.cell_load(c, "❮") catch {}; } else { _ = n.cell_load(c, " ") catch {}; } return cell; } inline fn render_terminator(n: *Plane, theme: *const Widget.Theme) Cell { var cell = n.cell_init(); cell.set_style(theme.editor); _ = n.cell_load(&cell, "\u{2003}") catch unreachable; return cell; } inline fn render_space(self: *const Self, c: *Cell, n: *Plane, theme: *const Widget.Theme) struct { usize, usize } { if (self.show_whitespace) { c.set_style(theme.editor_whitespace); _ = n.cell_load(c, "·") catch {}; //_ = n.cell_load(c, "•") catch {}; //_ = n.cell_load(c, "⁃") catch {}; //_ = n.cell_load(c, " ") catch {}; //_ = n.cell_load(c, "_") catch {}; //_ = n.cell_load(c, "󱁐") catch {}; //_ = n.cell_load(c, "⎵") catch {}; //_ = n.cell_load(c, "‿") catch {}; //_ = n.cell_load(c, "_") catch {}; //_ = n.cell_load(c, " ") catch {}; //_ = n.cell_load(c, "〿") catch {}; //_ = n.cell_load(c, "␠") catch {}; } else { _ = n.cell_load(c, " ") catch {}; } return .{ 1, 1 }; } inline fn render_tab(self: *const Self, c: *Cell, n: *Plane, abs_col: usize, theme: *const Widget.Theme) struct { usize, usize } { if (self.show_whitespace) { c.set_style(theme.editor_whitespace); _ = n.cell_load(c, "→") catch {}; //_ = n.cell_load(c, "⭲") catch {}; } else { _ = n.cell_load(c, " ") catch {}; } return .{ 1, 9 - (abs_col % 8) }; } inline fn render_egc(c: *Cell, n: *Plane, egc: [:0]const u8) struct { usize, usize } { const bytes = n.cell_load(c, egc) catch return .{ 1, 1 }; const colcount = c.columns(); return .{ bytes, colcount }; } fn render_syntax(self: *Self, theme: *const Widget.Theme, cache: *StyleCache, root: Buffer.Root) !void { const frame = tracy.initZone(@src(), .{ .name = "editor render syntax" }); defer frame.deinit(); const syn = self.syntax orelse return; const Ctx = struct { self: *Self, theme: *const Widget.Theme, cache: *StyleCache, last_row: usize = std.math.maxInt(usize), last_col: usize = std.math.maxInt(usize), root: Buffer.Root, pos_cache: PosToWidthCache, fn cb(ctx: *@This(), range: syntax.Range, scope: []const u8, id: u32, _: usize, _: *const syntax.Node) error{Stop}!void { const sel_ = ctx.pos_cache.range_to_selection(range, ctx.root, ctx.self.plane) orelse return; defer { ctx.last_row = sel_.begin.row; ctx.last_col = sel_.begin.col; } if (ctx.last_row == sel_.begin.row and sel_.begin.col <= ctx.last_col) return; const style_ = style_cache_lookup(ctx.theme, ctx.cache, scope, id); const style = if (style_) |sty| sty.style else return; var sel = sel_; if (sel.end.row < ctx.self.view.row) return; if (sel.begin.row > ctx.self.view.row + ctx.self.view.rows) return; if (sel.begin.row < ctx.self.view.row) sel.begin.row = ctx.self.view.row; if (sel.end.row > ctx.self.view.row + ctx.self.view.rows) sel.end.row = ctx.self.view.row + ctx.self.view.rows; if (sel.end.col < ctx.self.view.col) return; if (sel.begin.col > ctx.self.view.col + ctx.self.view.cols) return; if (sel.begin.col < ctx.self.view.col) sel.begin.col = ctx.self.view.col; if (sel.end.col > ctx.self.view.col + ctx.self.view.cols) sel.end.col = ctx.self.view.col + ctx.self.view.cols; for (sel.begin.row..sel.end.row + 1) |row| { const begin_col = if (row == sel.begin.row) sel.begin.col else 0; const end_col = if (row == sel.end.row) sel.end.col else ctx.self.view.col + ctx.self.view.cols; const y = @max(ctx.self.view.row, row) - ctx.self.view.row; const x = @max(ctx.self.view.col, begin_col) - ctx.self.view.col; const end_x = @max(ctx.self.view.col, end_col) - ctx.self.view.col; if (x >= end_x) return; for (x..end_x) |x_| try ctx.render_cell(y, x_, style); } } fn render_cell(ctx: *@This(), y: usize, x: usize, style: Widget.Theme.Style) !void { ctx.self.plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return; var cell = ctx.self.plane.cell_init(); _ = ctx.self.plane.at_cursor_cell(&cell) catch return; cell.set_style(style); _ = ctx.self.plane.putc(&cell) catch {}; } }; var ctx: Ctx = .{ .self = self, .theme = theme, .cache = cache, .root = root, .pos_cache = try PosToWidthCache.init(self.allocator), }; defer ctx.pos_cache.deinit(); const range: syntax.Range = .{ .start_point = .{ .row = @intCast(self.view.row), .column = 0 }, .end_point = .{ .row = @intCast(self.view.row + self.view.rows), .column = 0 }, .start_byte = 0, .end_byte = 0, }; return syn.render(&ctx, Ctx.cb, range); } fn style_cache_lookup(theme: *const Widget.Theme, cache: *StyleCache, scope: []const u8, id: u32) ?Widget.Theme.Token { return if (cache.get(id)) |sty| ret: { break :ret sty; } else ret: { const sty = tui.find_scope_style(theme, scope) orelse null; cache.put(id, sty) catch {}; break :ret sty; }; } pub fn style_lookup(self: *Self, theme_: ?*const Widget.Theme, scope: []const u8, id: u32) ?Widget.Theme.Token { const theme = theme_ orelse return null; const cache = &(self.style_cache orelse return null); return style_cache_lookup(theme, cache, scope, id); } inline fn is_point_in_selection(self: *const Self, sel_: anytype, y: c_uint, x: c_uint) bool { const sel = sel_; const row = self.view.row + y; const col = self.view.col + x; const b_col: usize = if (sel.begin.row < row) 0 else sel.begin.col; const e_col: usize = if (row < sel.end.row) std.math.maxInt(u32) else sel.end.col; return sel.begin.row <= row and row <= sel.end.row and b_col <= col and col < e_col; } inline fn is_point_before_selection(self: *const Self, sel_: anytype, y: c_uint, x: c_uint) bool { const sel = sel_; const row = self.view.row + y; const col = self.view.col + x; return row < sel.begin.row or (row == sel.begin.row and col < sel.begin.col); } inline fn screen_cursor(self: *const Self, cursor: *const Cursor) ?Cursor { return if (self.view.is_visible(cursor)) .{ .row = cursor.row - self.view.row, .col = cursor.col - self.view.col, } else null; } inline fn screen_pos_y(self: *Self) usize { return self.primary.row - self.view.row; } inline fn screen_pos_x(self: *Self) usize { return self.primary.col - self.view.col; } fn update_event(self: *Self) !void { const primary = self.get_primary(); const dirty = if (self.buffer) |buf| buf.is_dirty() else false; const root: ?Buffer.Root = self.buf_root() catch null; if (token_from(self.last.root) != token_from(root)) { try self.send_editor_update(self.last.root, root); self.lsp_version += 1; } if (self.last.dirty != dirty) try self.send_editor_dirty(dirty); if (self.matches.items.len != self.last.matches and self.match_token == self.match_done_token) { try self.send_editor_match(self.matches.items.len); self.last.matches = self.matches.items.len; } if (self.cursels.items.len != self.last.cursels) { try self.send_editor_cursels(self.cursels.items.len); self.last.cursels = self.cursels.items.len; } if (!primary.cursor.eql(self.last.primary.cursor)) try self.send_editor_pos(&primary.cursor); if (primary.selection) |primary_selection_| { var primary_selection = primary_selection_; primary_selection.normalize(); if (self.last.primary.selection) |last_selection_| { var last_selection = last_selection_; last_selection.normalize(); if (!primary_selection.eql(last_selection)) try self.send_editor_selection_changed(primary_selection); } else try self.send_editor_selection_added(primary_selection); } else if (self.last.primary.selection) |_| try self.send_editor_selection_removed(); if (!self.view.eql(self.last.view)) try self.send_editor_view(); self.last.view = self.view; self.last.primary = primary.*; self.last.dirty = dirty; self.last.root = root; } fn send_editor_pos(self: *const Self, cursor: *const Cursor) !void { const root = self.buf_root() catch return error.Stop; _ = try self.handlers.msg(.{ "E", "pos", root.lines(), cursor.row, cursor.col }); } fn send_editor_match(self: *const Self, matches: usize) !void { _ = try self.handlers.msg(.{ "E", "match", matches }); } fn send_editor_cursels(self: *const Self, cursels: usize) !void { _ = try self.handlers.msg(.{ "E", "cursels", cursels }); } fn send_editor_selection_added(self: *const Self, sel: Selection) !void { return self.send_editor_selection_changed(sel); } fn send_editor_selection_changed(self: *const Self, sel: Selection) !void { _ = try self.handlers.msg(.{ "E", "sel", sel.begin.row, sel.begin.col, sel.end.row, sel.end.col }); } fn send_editor_selection_removed(self: *const Self) !void { _ = try self.handlers.msg(.{ "E", "sel", "none" }); } fn send_editor_view(self: *const Self) !void { const root = self.buf_root() catch return error.Stop; _ = 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()); } pub fn send_editor_jump_source(self: *Self) !void { try self.send_editor_cursel_msg("jump_source", self.get_primary()); } fn send_editor_jump_destination(self: *Self) !void { try self.send_editor_cursel_msg("jump_destination", self.get_primary()); } fn send_editor_cursel_msg(self: *Self, tag: []const u8, cursel: *CurSel) !void { const c = cursel.cursor; _ = try if (cursel.selection) |s| self.handlers.msg(.{ "E", "location", tag, c.row, c.col, s.begin.row, s.begin.col, s.end.row, s.end.col }) else self.handlers.msg(.{ "E", "location", tag, c.row, c.col }); } fn send_editor_open(self: *const Self, file_path: []const u8, file_exists: bool, file_type: []const u8, file_icon: []const u8, file_color: u24) !void { _ = try self.handlers.msg(.{ "E", "open", file_path, file_exists, file_type, file_icon, file_color }); } fn send_editor_save(self: *const Self, file_path: []const u8) !void { _ = try self.handlers.msg(.{ "E", "save", file_path }); if (self.syntax) |_| project_manager.did_save(file_path) catch {}; } fn send_editor_dirty(self: *const Self, file_dirty: bool) !void { _ = try self.handlers.msg(.{ "E", "dirty", file_dirty }); } fn token_from(p: ?*const anyopaque) usize { return if (p) |p_| @intFromPtr(p_) else 0; } fn send_editor_update(self: *const Self, old_root: ?Buffer.Root, new_root: ?Buffer.Root) !void { _ = try self.handlers.msg(.{ "E", "update", token_from(new_root), token_from(old_root) }); if (self.syntax) |_| if (self.file_path) |file_path| if (old_root != null and new_root != null) project_manager.did_change(file_path, self.lsp_version, token_from(new_root), token_from(old_root)) catch {}; } fn clamp_abs(self: *Self, abs: bool) void { var dest: View = self.view; dest.clamp(&self.get_primary().cursor, abs); self.update_scroll_dest_abs(dest.row); self.view.col = dest.col; } inline fn clamp(self: *Self) void { self.clamp_abs(false); } fn clamp_mouse(self: *Self) void { self.clamp_abs(true); } fn clear_all_cursors(self: *Self) void { self.cursels.clearRetainingCapacity(); } fn collapse_cursors(self: *Self) void { const frame = tracy.initZone(@src(), .{ .name = "collapse cursors" }); defer frame.deinit(); var old = self.cursels; defer old.deinit(); self.cursels = CurSel.List.initCapacity(self.allocator, old.items.len) catch return; for (old.items[0 .. old.items.len - 1], 0..) |*a_, i| if (a_.*) |*a| { for (old.items[i + 1 ..], i + 1..) |*b_, j| if (b_.*) |*b| { if (a.cursor.eql(b.cursor)) old.items[j] = null; }; }; for (old.items) |*item_| if (item_.*) |*item| { (self.cursels.addOne() catch return).* = item.*; }; } fn cancel_all_selections(self: *Self) void { var primary = self.cursels.getLast() orelse CurSel{}; primary.selection = null; self.cursels.clearRetainingCapacity(); self.cursels.addOneAssumeCapacity().* = primary; for (self.matches.items) |*match_| if (match_.*) |*match| { match.has_selection = false; }; } fn cancel_all_matches(self: *Self) void { self.matches.clearAndFree(); } pub fn clear_matches(self: *Self) void { self.cancel_all_matches(); self.match_token += 1; self.match_done_token = self.match_token; } pub fn init_matches_update(self: *Self) void { self.cancel_all_matches(); self.match_token += 1; } fn with_cursor_const(root: Buffer.Root, move: cursor_operator_const, cursel: *CurSel, plane: Plane) error{Stop}!void { try move(root, &cursel.cursor, plane); } fn with_cursors_const(self: *Self, root: Buffer.Root, move: cursor_operator_const) error{Stop}!void { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { cursel.selection = null; try with_cursor_const(root, move, cursel, self.plane); }; self.collapse_cursors(); } fn with_cursor_const_arg(root: Buffer.Root, move: cursor_operator_const_arg, cursel: *CurSel, ctx: Context, plane: Plane) error{Stop}!void { try move(root, &cursel.cursor, ctx, plane); } fn with_cursors_const_arg(self: *Self, root: Buffer.Root, move: cursor_operator_const_arg, ctx: Context) error{Stop}!void { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { cursel.selection = null; try with_cursor_const_arg(root, move, cursel, ctx, self.plane); }; self.collapse_cursors(); } fn with_cursor_and_view_const(root: Buffer.Root, move: cursor_view_operator_const, cursel: *CurSel, view: *const View, plane: Plane) error{Stop}!void { try move(root, &cursel.cursor, view, plane); } fn with_cursors_and_view_const(self: *Self, root: Buffer.Root, move: cursor_view_operator_const, view: *const View) error{Stop}!void { var someone_stopped = false; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| with_cursor_and_view_const(root, move, cursel, view, self.plane) catch { someone_stopped = true; }; self.collapse_cursors(); return if (someone_stopped) error.Stop else {}; } fn with_cursor(root: Buffer.Root, move: cursor_operator, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { return try move(root, &cursel.cursor, allocator); } fn with_cursors(self: *Self, root_: Buffer.Root, move: cursor_operator, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; for (self.cursels.items) |*cursel| { cursel.selection = null; root = try with_cursor(root, move, cursel, allocator); } self.collapse_cursors(); return root; } fn with_selection_const(root: Buffer.Root, move: cursor_operator_const, cursel: *CurSel, plane: Plane) error{Stop}!void { const sel = cursel.enable_selection(); try move(root, &sel.end, plane); cursel.cursor = sel.end; cursel.check_selection(); } fn with_selections_const(self: *Self, root: Buffer.Root, move: cursor_operator_const) error{Stop}!void { var someone_stopped = false; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| with_selection_const(root, move, cursel, self.plane) catch { someone_stopped = true; }; self.collapse_cursors(); return if (someone_stopped) error.Stop else {}; } fn with_selection_const_arg(root: Buffer.Root, move: cursor_operator_const_arg, cursel: *CurSel, ctx: Context, plane: Plane) error{Stop}!void { const sel = cursel.enable_selection(); try move(root, &sel.end, ctx, plane); cursel.cursor = sel.end; cursel.check_selection(); } fn with_selections_const_arg(self: *Self, root: Buffer.Root, move: cursor_operator_const_arg, ctx: Context) error{Stop}!void { var someone_stopped = false; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| with_selection_const_arg(root, move, cursel, ctx, self.plane) catch { someone_stopped = true; }; self.collapse_cursors(); return if (someone_stopped) error.Stop else {}; } fn with_selection_and_view_const(root: Buffer.Root, move: cursor_view_operator_const, cursel: *CurSel, view: *const View, plane: Plane) error{Stop}!void { const sel = cursel.enable_selection(); try move(root, &sel.end, view, plane); cursel.cursor = sel.end; } fn with_selections_and_view_const(self: *Self, root: Buffer.Root, move: cursor_view_operator_const, view: *const View) error{Stop}!void { var someone_stopped = false; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| with_selection_and_view_const(root, move, cursel, view, self.plane) catch { someone_stopped = true; }; self.collapse_cursors(); return if (someone_stopped) error.Stop else {}; } fn with_cursel(root: Buffer.Root, op: cursel_operator, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { return op(root, cursel, allocator); } fn with_cursels(self: *Self, root_: Buffer.Root, move: cursel_operator, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; var someone_stopped = false; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { root = with_cursel(root, move, cursel, allocator) catch ret: { someone_stopped = true; break :ret root; }; }; self.collapse_cursors(); return if (someone_stopped) error.Stop else root; } fn with_cursel_mut(self: *Self, root: Buffer.Root, op: cursel_operator_mut, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { return op(self, root, cursel, allocator); } fn with_cursels_mut(self: *Self, root_: Buffer.Root, move: cursel_operator_mut, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; var someone_stopped = false; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { root = self.with_cursel_mut(root, move, cursel, allocator) catch ret: { someone_stopped = true; break :ret root; }; }; self.collapse_cursors(); return if (someone_stopped) error.Stop else root; } fn with_cursel_const(root: Buffer.Root, op: cursel_operator_const, cursel: *CurSel) error{Stop}!void { return op(root, cursel); } fn with_cursels_const(self: *Self, root: Buffer.Root, move: cursel_operator_const) error{Stop}!void { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| with_cursel_const(root, move, cursel) catch return error.Stop; self.collapse_cursors(); } fn nudge_insert(self: *Self, nudge: Selection, exclude: *const CurSel, size: usize) void { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel != exclude) cursel.nudge_insert(nudge); for (self.matches.items) |*match_| if (match_.*) |*match| match.nudge_insert(nudge); if (self.syntax) |syn| { const root = self.buf_root() catch return; const start_byte = root.get_byte_pos(nudge.begin, self.plane.metrics()) catch return; syn.edit(.{ .start_byte = @intCast(start_byte), .old_end_byte = @intCast(start_byte), .new_end_byte = @intCast(start_byte + size), .start_point = .{ .row = @intCast(nudge.begin.row), .column = @intCast(nudge.begin.col) }, .old_end_point = .{ .row = @intCast(nudge.begin.row), .column = @intCast(nudge.begin.col) }, .new_end_point = .{ .row = @intCast(nudge.end.row), .column = @intCast(nudge.end.col) }, }); } } fn nudge_delete(self: *Self, nudge: Selection, exclude: *const CurSel, size: usize) void { for (self.cursels.items, 0..) |*cursel_, i| if (cursel_.*) |*cursel| if (cursel != exclude) if (!cursel.nudge_delete(nudge)) { self.cursels.items[i] = null; }; for (self.matches.items, 0..) |*match_, i| if (match_.*) |*match| if (!match.nudge_delete(nudge)) { self.matches.items[i] = null; }; if (self.syntax) |syn| { const root = self.buf_root() catch return; const start_byte = root.get_byte_pos(nudge.begin, self.plane.metrics()) catch return; syn.edit(.{ .start_byte = @intCast(start_byte), .old_end_byte = @intCast(start_byte + size), .new_end_byte = @intCast(start_byte), .start_point = .{ .row = @intCast(nudge.begin.row), .column = @intCast(nudge.begin.col) }, .old_end_point = .{ .row = @intCast(nudge.end.row), .column = @intCast(nudge.end.col) }, .new_end_point = .{ .row = @intCast(nudge.begin.row), .column = @intCast(nudge.begin.col) }, }); } } fn delete_selection(self: *Self, root: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var sel: Selection = cursel.selection orelse return error.Stop; sel.normalize(); cursel.cursor = sel.begin; cursel.selection = null; var size: usize = 0; const root_ = try root.delete_range(sel, allocator, &size, self.plane.metrics()); self.nudge_delete(sel, cursel, size); return root_; } fn delete_to(self: *Self, move: cursor_operator_const, root_: Buffer.Root, allocator: Allocator) error{Stop}!Buffer.Root { var all_stop = true; var root = root_; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { if (cursel.selection) |_| { root = self.delete_selection(root, cursel, allocator) catch continue; all_stop = false; continue; } with_selection_const(root, move, cursel, self.plane) catch continue; root = self.delete_selection(root, cursel, allocator) catch continue; all_stop = false; }; if (all_stop) return error.Stop; return root; } const cursor_predicate = *const fn (root: Buffer.Root, cursor: *Cursor, plane: Plane) bool; const cursor_operator_const = *const fn (root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void; const cursor_operator_const_arg = *const fn (root: Buffer.Root, cursor: *Cursor, ctx: Context, plane: Plane) error{Stop}!void; const cursor_view_operator_const = *const fn (root: Buffer.Root, cursor: *Cursor, view: *const View, plane: Plane) error{Stop}!void; const cursel_operator_const = *const fn (root: Buffer.Root, cursel: *CurSel) error{Stop}!void; const cursor_operator = *const fn (root: Buffer.Root, cursor: *Cursor, allocator: Allocator) error{Stop}!Buffer.Root; const cursel_operator = *const fn (root: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root; const cursel_operator_mut = *const fn (self: *Self, root: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root; fn is_not_word_char(c: []const u8) bool { if (c.len == 0) return true; return switch (c[0]) { ' ' => true, '=' => true, '"' => true, '\'' => true, '\t' => true, '\n' => true, '/' => true, '\\' => true, '*' => true, ':' => true, '.' => true, ',' => true, '(' => true, ')' => true, '{' => true, '}' => true, '[' => true, ']' => true, ';' => true, '|' => true, '?' => true, '&' => true, else => false, }; } fn is_word_char(c: []const u8) bool { return !is_not_word_char(c); } fn is_word_char_at_cursor(root: Buffer.Root, cursor: *const Cursor, plane: Plane) bool { return cursor.test_at(root, is_word_char, plane.metrics()); } fn is_non_word_char_at_cursor(root: Buffer.Root, cursor: *const Cursor, plane: Plane) bool { return cursor.test_at(root, is_not_word_char, plane.metrics()); } fn is_word_boundary_left(root: Buffer.Root, cursor: *const Cursor, plane: Plane) bool { if (cursor.col == 0) return true; if (is_non_word_char_at_cursor(root, cursor, plane)) return false; var next = cursor.*; next.move_left(root, plane.metrics()) catch return true; if (is_non_word_char_at_cursor(root, &next, plane)) return true; return false; } fn is_non_word_boundary_left(root: Buffer.Root, cursor: *const Cursor, plane: Plane) bool { if (cursor.col == 0) return true; if (is_word_char_at_cursor(root, cursor, plane)) return false; var next = cursor.*; next.move_left(root, plane.metrics()) catch return true; if (is_word_char_at_cursor(root, &next, plane)) return true; return false; } fn is_word_boundary_right(root: Buffer.Root, cursor: *const Cursor, plane: Plane) bool { const line_width = root.line_width(cursor.row, plane.metrics()) catch return true; if (cursor.col >= line_width) return true; if (is_non_word_char_at_cursor(root, cursor, plane)) return false; var next = cursor.*; next.move_right(root, plane.metrics()) catch return true; if (is_non_word_char_at_cursor(root, &next, plane)) return true; return false; } fn is_non_word_boundary_right(root: Buffer.Root, cursor: *const Cursor, plane: Plane) bool { const line_width = root.line_width(cursor.row, plane.metrics()) catch return true; if (cursor.col >= line_width) return true; if (is_word_char_at_cursor(root, cursor, plane)) return false; var next = cursor.*; next.move_right(root, plane.metrics()) catch return true; if (is_word_char_at_cursor(root, &next, plane)) return true; return false; } fn is_eol_left(_: Buffer.Root, cursor: *const Cursor, _: Plane) bool { if (cursor.col == 0) return true; return false; } fn is_eol_right(root: Buffer.Root, cursor: *const Cursor, plane: Plane) bool { const line_width = root.line_width(cursor.row, plane.metrics()) catch return true; if (cursor.col >= line_width) return true; return false; } fn is_eol_right_vim(root: Buffer.Root, cursor: *const Cursor, plane: Plane) bool { const line_width = root.line_width(cursor.row, plane.metrics()) catch return true; if (line_width == 0) return true; if (cursor.col >= line_width - 1) return true; return false; } fn move_cursor_left(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void { try cursor.move_left(root, plane.metrics()); } fn move_cursor_left_until(root: Buffer.Root, cursor: *Cursor, pred: cursor_predicate, plane: Plane) void { while (!pred(root, cursor, plane)) move_cursor_left(root, cursor, plane) catch return; } fn move_cursor_left_unless(root: Buffer.Root, cursor: *Cursor, pred: cursor_predicate, plane: Plane) void { if (!pred(root, cursor, plane)) move_cursor_left(root, cursor, plane) catch return; } fn move_cursor_begin(_: Buffer.Root, cursor: *Cursor, _: Plane) !void { cursor.move_begin(); } fn smart_move_cursor_begin(root: Buffer.Root, cursor: *Cursor, plane: Plane) !void { const first = find_first_non_ws(root, cursor.row, plane); return if (cursor.col == first) cursor.move_begin() else cursor.move_to(root, cursor.row, first, plane.metrics()); } fn move_cursor_right(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void { try cursor.move_right(root, plane.metrics()); } fn move_cursor_right_until(root: Buffer.Root, cursor: *Cursor, pred: cursor_predicate, plane: Plane) void { while (!pred(root, cursor, plane)) move_cursor_right(root, cursor, plane) catch return; } fn move_cursor_right_unless(root: Buffer.Root, cursor: *Cursor, pred: cursor_predicate, plane: Plane) void { if (!pred(root, cursor, plane)) move_cursor_right(root, cursor, plane) catch return; } fn move_cursor_end(root: Buffer.Root, cursor: *Cursor, plane: Plane) !void { cursor.move_end(root, plane.metrics()); } fn move_cursor_up(root: Buffer.Root, cursor: *Cursor, plane: Plane) !void { try cursor.move_up(root, plane.metrics()); } fn move_cursor_down(root: Buffer.Root, cursor: *Cursor, plane: Plane) !void { try cursor.move_down(root, plane.metrics()); } fn move_cursor_buffer_begin(_: Buffer.Root, cursor: *Cursor, _: Plane) !void { cursor.move_buffer_begin(); } fn move_cursor_buffer_end(root: Buffer.Root, cursor: *Cursor, plane: Plane) !void { cursor.move_buffer_end(root, plane.metrics()); } fn move_cursor_page_up(root: Buffer.Root, cursor: *Cursor, view: *const View, plane: Plane) !void { cursor.move_page_up(root, view, plane.metrics()); } fn move_cursor_page_down(root: Buffer.Root, cursor: *Cursor, view: *const View, plane: Plane) !void { cursor.move_page_down(root, view, plane.metrics()); } pub fn primary_click(self: *Self, y: c_int, x: c_int) !void { if (self.fast_scroll) try self.push_cursor() else self.cancel_all_selections(); const primary = self.get_primary(); primary.selection = null; self.selection_mode = .char; try self.send_editor_jump_source(); const root = self.buf_root() catch return; primary.cursor.move_abs(root, &self.view, @intCast(y), @intCast(x), self.plane.metrics()) catch return; self.clamp_mouse(); try self.send_editor_jump_destination(); if (self.jump_mode) try self.goto_definition(.{}); } pub fn primary_double_click(self: *Self, y: c_int, x: c_int) !void { const primary = self.get_primary(); primary.selection = null; self.selection_mode = .word; const root = self.buf_root() catch return; primary.cursor.move_abs(root, &self.view, @intCast(y), @intCast(x), self.plane.metrics()) catch return; _ = try self.select_word_at_cursor(primary); self.clamp_mouse(); } pub fn primary_triple_click(self: *Self, y: c_int, x: c_int) !void { const primary = self.get_primary(); primary.selection = null; self.selection_mode = .line; const root = self.buf_root() catch return; primary.cursor.move_abs(root, &self.view, @intCast(y), @intCast(x), self.plane.metrics()) catch return; try self.select_line_at_cursor(primary); self.clamp_mouse(); } pub fn primary_drag(self: *Self, y: c_int, x: c_int) void { const y_ = if (y < 0) 0 else y; const x_ = if (x < 0) 0 else x; const primary = self.get_primary(); const sel = primary.enable_selection(); const root = self.buf_root() catch return; sel.end.move_abs(root, &self.view, @intCast(y_), @intCast(x_), self.plane.metrics()) catch return; switch (self.selection_mode) { .char => {}, .word => if (sel.begin.right_of(sel.end)) with_selection_const(root, move_cursor_word_begin, primary, self.plane) catch return else with_selection_const(root, move_cursor_word_end, primary, self.plane) catch return, .line => if (sel.begin.right_of(sel.end)) with_selection_const(root, move_cursor_begin, primary, self.plane) catch return else { with_selection_const(root, move_cursor_end, primary, self.plane) catch return; with_selection_const(root, move_cursor_right, primary, self.plane) catch return; }, } primary.cursor = sel.end; primary.check_selection(); self.clamp_mouse(); } pub fn drag_to(self: *Self, ctx: Context) Result { var y: i32 = 0; var x: i32 = 0; if (!try ctx.args.match(.{ tp.extract(&y), tp.extract(&x) })) return error.InvalidArgument; return self.primary_drag(y, x); } pub fn secondary_click(self: *Self, y: c_int, x: c_int) !void { return self.primary_drag(y, x); } pub fn secondary_drag(self: *Self, y: c_int, x: c_int) !void { return self.primary_drag(y, x); } fn get_animation_min_lag() f64 { const ms: f64 = @floatFromInt(tui.current().config.animation_min_lag); return @max(ms * 0.001, 0.001); // to seconds } fn get_animation_max_lag() f64 { const ms: f64 = @floatFromInt(tui.current().config.animation_max_lag); return @max(ms * 0.001, 0.001); // to seconds } fn update_animation_lag(self: *Self) void { const ts = time.microTimestamp(); const tdiff = ts - self.animation_last_time; const lag: f64 = @as(f64, @floatFromInt(tdiff)) / time.us_per_s; self.animation_lag = @max(@min(lag, get_animation_max_lag()), get_animation_min_lag()); self.animation_last_time = ts; // self.logger.print("update_lag: {d} {d:.2}", .{ lag, self.animation_lag }) catch {}; } fn update_animation_step(self: *Self, dest: usize) void { const steps_ = @max(dest, self.view.row) - @min(dest, self.view.row); self.update_animation_lag(); const steps: f64 = @floatFromInt(steps_); const frame_rate: f64 = @floatFromInt(self.animation_frame_rate); const frame_time: f64 = 1.0 / frame_rate; const step_frames = self.animation_lag / frame_time; const step: f64 = steps / step_frames; self.animation_step = @intFromFloat(step); if (self.animation_step == 0) self.animation_step = 1; } fn update_scroll(self: *Self) void { const step = self.animation_step; const view = self.view.row; const dest = self.scroll_dest; if (view == dest) return; var row = view; if (view < dest) { row += step; if (dest < row) row = dest; } else if (dest < view) { row -= if (row < step) row else step; if (row < dest) row = dest; } self.view.row = row; } fn update_scroll_dest_abs(self: *Self, dest: usize) void { const root = self.buf_root() catch return; const max_view = if (root.lines() <= scroll_cursor_min_border_distance) 0 else root.lines() - scroll_cursor_min_border_distance; self.scroll_dest = @min(dest, max_view); self.update_animation_step(dest); } fn scroll_up(self: *Self) void { var dest: View = self.view; dest.row = if (dest.row > scroll_step_small) dest.row - scroll_step_small else 0; self.update_scroll_dest_abs(dest.row); } fn scroll_down(self: *Self) void { var dest: View = self.view; dest.row += scroll_step_small; self.update_scroll_dest_abs(dest.row); } fn scroll_pageup(self: *Self) void { var dest: View = self.view; dest.row = if (dest.row > dest.rows) dest.row - dest.rows else 0; self.update_scroll_dest_abs(dest.row); } fn scroll_pagedown(self: *Self) void { var dest: View = self.view; dest.row += dest.rows; self.update_scroll_dest_abs(dest.row); } pub fn scroll_up_pageup(self: *Self, _: Context) Result { if (self.fast_scroll) self.scroll_pageup() else self.scroll_up(); } pub fn scroll_down_pagedown(self: *Self, _: Context) Result { if (self.fast_scroll) self.scroll_pagedown() else self.scroll_down(); } pub fn scroll_to(self: *Self, row: usize) void { self.update_scroll_dest_abs(row); } fn scroll_view_offset(self: *Self, offset: usize) void { const primary = self.get_primary(); const row = if (primary.cursor.row > offset) primary.cursor.row - offset else 0; self.update_scroll_dest_abs(row); } pub fn scroll_view_center(self: *Self, _: Context) Result { return self.scroll_view_offset(self.view.rows / 2); } pub fn scroll_view_top(self: *Self, _: Context) Result { return self.scroll_view_offset(scroll_cursor_min_border_distance); } pub fn scroll_view_bottom(self: *Self, _: Context) Result { return self.scroll_view_offset(if (self.view.rows > scroll_cursor_min_border_distance) self.view.rows - scroll_cursor_min_border_distance else 0); } fn set_clipboard(self: *Self, text: []const u8) void { if (self.clipboard) |old| self.allocator.free(old); self.clipboard = text; tui.current().rdr.copy_to_system_clipboard(text); } fn copy_selection(root: Buffer.Root, sel: Selection, text_allocator: Allocator, plane: Plane) ![]const u8 { var size: usize = 0; _ = try root.get_range(sel, null, &size, null, plane.metrics()); const buf__ = try text_allocator.alloc(u8, size); return (try root.get_range(sel, buf__, null, null, plane.metrics())).?; } pub fn get_selection(self: *const Self, sel: Selection, text_allocator: Allocator) ![]const u8 { return copy_selection(try self.buf_root(), sel, text_allocator, self.plane); } fn copy_word_at_cursor(self: *Self, text_allocator: Allocator) ![]const u8 { const root = try self.buf_root(); const primary = self.get_primary(); const sel = if (primary.selection) |*sel| sel else try self.select_word_at_cursor(primary); return try copy_selection(root, sel.*, text_allocator, self.plane); } pub fn cut_selection(self: *Self, root: Buffer.Root, cursel: *CurSel) !struct { []const u8, Buffer.Root } { return if (cursel.selection) |sel| ret: { var old_selection: Selection = sel; old_selection.normalize(); const cut_text = try copy_selection(root, sel, self.allocator, self.plane); if (cut_text.len > 100) { self.logger.print("cut:{s}...", .{std.fmt.fmtSliceEscapeLower(cut_text[0..100])}); } else { self.logger.print("cut:{s}", .{std.fmt.fmtSliceEscapeLower(cut_text)}); } break :ret .{ cut_text, try self.delete_selection(root, cursel, try self.buf_a()) }; } else error.Stop; } fn expand_selection_to_all(root: Buffer.Root, sel: *Selection, plane: Plane) !void { try move_cursor_buffer_begin(root, &sel.begin, plane); try move_cursor_buffer_end(root, &sel.end, plane); } fn insert(self: *Self, root: Buffer.Root, cursel: *CurSel, s: []const u8, allocator: Allocator) !Buffer.Root { var root_ = if (cursel.selection) |_| try self.delete_selection(root, cursel, allocator) else root; const cursor = &cursel.cursor; const begin = cursel.cursor; cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, self.plane.metrics()); cursor.target = cursor.col; self.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len); return root_; } pub fn cut(self: *Self, _: Context) Result { const primary = self.get_primary(); const b = self.buf_for_update() catch return; var root = b.root; if (self.cursels.items.len == 1) if (primary.selection) |_| {} else { const sel = primary.enable_selection(); try move_cursor_begin(root, &sel.begin, self.plane); try move_cursor_end(root, &sel.end, self.plane); try move_cursor_right(root, &sel.end, self.plane); }; var first = true; var text = std.ArrayList(u8).init(self.allocator); for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { const cut_text, root = try self.cut_selection(root, cursel); if (first) { first = false; } else { try text.appendSlice("\n"); } try text.appendSlice(cut_text); }; try self.update_buf(root); self.set_clipboard(text.items); self.clamp(); } pub fn copy(self: *Self, _: Context) Result { const root = self.buf_root() catch return; var first = true; var text = std.ArrayList(u8).init(self.allocator); for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { if (cursel.selection) |sel| { const copy_text = try copy_selection(root, sel, self.allocator, self.plane); if (first) { first = false; } else { try text.appendSlice("\n"); } try text.appendSlice(copy_text); } }; if (text.items.len > 0) { if (text.items.len > 100) { self.logger.print("copy:{s}...", .{std.fmt.fmtSliceEscapeLower(text.items[0..100])}); } else { self.logger.print("copy:{s}", .{std.fmt.fmtSliceEscapeLower(text.items)}); } self.set_clipboard(text.items); } } pub fn paste(self: *Self, ctx: Context) Result { var text: []const u8 = undefined; if (!(ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text)}))) { if (self.clipboard) |text_| text = text_ else return; } self.logger.print("paste: {d} bytes", .{text.len}); const b = try self.buf_for_update(); var root = b.root; if (self.cursels.items.len == 1) { const primary = self.get_primary(); root = try self.insert(root, primary, text, b.allocator); } else { if (std.mem.indexOfScalar(u8, text, '\n')) |_| { var pos: usize = 0; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { if (std.mem.indexOfScalarPos(u8, text, pos, '\n')) |next| { root = try self.insert(root, cursel, text[pos..next], b.allocator); pos = next + 1; } else { root = try self.insert(root, cursel, text[pos..], b.allocator); pos = 0; } }; } else { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { root = try self.insert(root, cursel, text, b.allocator); }; } } try self.update_buf(root); self.clamp(); self.need_render(); } pub fn system_paste(self: *Self, _: Context) Result { if (builtin.os.tag == .windows) return self.paste(.{}); tui.current().rdr.request_system_clipboard(); } pub fn delete_forward(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.delete_to(move_cursor_right, b.root, b.allocator); try self.update_buf(root); self.clamp(); } pub fn delete_backward(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.delete_to(move_cursor_left, b.root, b.allocator); try self.update_buf(root); self.clamp(); } pub fn delete_word_left(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.delete_to(move_cursor_word_left_space, b.root, b.allocator); try self.update_buf(root); self.clamp(); } pub fn delete_word_right(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.delete_to(move_cursor_word_right_space, b.root, b.allocator); try self.update_buf(root); self.clamp(); } pub fn delete_to_begin(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.delete_to(move_cursor_begin, b.root, b.allocator); try self.update_buf(root); self.clamp(); } pub fn delete_to_end(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.delete_to(move_cursor_end, b.root, b.allocator); try self.update_buf(root); self.clamp(); } pub fn join_next_line(self: *Self, _: Context) Result { const b = try self.buf_for_update(); try self.with_cursors_const(b.root, move_cursor_end); const root = try self.delete_to(move_cursor_right, b.root, b.allocator); try self.update_buf(root); self.clamp(); } pub fn move_left(self: *Self, _: Context) Result { const root = try self.buf_root(); self.with_cursors_const(root, move_cursor_left) catch {}; self.clamp(); } pub fn move_right(self: *Self, _: Context) Result { const root = try self.buf_root(); self.with_cursors_const(root, move_cursor_right) catch {}; self.clamp(); } fn move_cursor_left_vim(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void { move_cursor_left_unless(root, cursor, is_eol_left, plane); } fn move_cursor_right_vim(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void { move_cursor_right_unless(root, cursor, is_eol_right_vim, plane); } pub fn move_left_vim(self: *Self, _: Context) Result { const root = try self.buf_root(); self.with_cursors_const(root, move_cursor_left_vim) catch {}; self.clamp(); } pub fn move_right_vim(self: *Self, _: Context) Result { const root = try self.buf_root(); self.with_cursors_const(root, move_cursor_right_vim) catch {}; self.clamp(); } fn move_cursor_word_begin(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void { if (is_non_word_char_at_cursor(root, cursor, plane)) { move_cursor_left_until(root, cursor, is_word_boundary_right, plane); try move_cursor_right(root, cursor, plane); } else { move_cursor_left_until(root, cursor, is_word_boundary_left, plane); } } fn move_cursor_word_end(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void { if (is_non_word_char_at_cursor(root, cursor, plane)) { move_cursor_right_until(root, cursor, is_word_boundary_left, plane); try move_cursor_left(root, cursor, plane); } else { move_cursor_right_until(root, cursor, is_word_boundary_right, plane); } try move_cursor_right(root, cursor, plane); } fn move_cursor_word_left(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void { try move_cursor_left(root, cursor, plane); move_cursor_left_until(root, cursor, is_word_boundary_left, plane); } fn move_cursor_word_left_space(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void { try move_cursor_left(root, cursor, plane); var next = cursor.*; next.move_left(root, plane.metrics()) catch return move_cursor_left_until(root, cursor, is_word_boundary_left, plane); if (is_non_word_char_at_cursor(root, cursor, plane) and is_non_word_char_at_cursor(root, &next, plane)) move_cursor_left_until(root, cursor, is_non_word_boundary_left, plane) else move_cursor_left_until(root, cursor, is_word_boundary_left, plane); } pub fn move_cursor_word_right(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void { move_cursor_right_until(root, cursor, is_word_boundary_right, plane); try move_cursor_right(root, cursor, plane); } pub fn move_cursor_word_right_vim(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void { try move_cursor_right(root, cursor, plane); move_cursor_right_until(root, cursor, is_word_boundary_left, plane); } pub fn move_cursor_word_right_space(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void { var next = cursor.*; next.move_right(root, plane.metrics()) catch { move_cursor_right_until(root, cursor, is_word_boundary_right, plane); try move_cursor_right(root, cursor, plane); return; }; if (is_non_word_char_at_cursor(root, cursor, plane) and is_non_word_char_at_cursor(root, &next, plane)) move_cursor_right_until(root, cursor, is_non_word_boundary_right, plane) else move_cursor_right_until(root, cursor, is_word_boundary_right, plane); try move_cursor_right(root, cursor, plane); } pub fn move_word_left(self: *Self, _: Context) Result { const root = try self.buf_root(); self.with_cursors_const(root, move_cursor_word_left) catch {}; self.clamp(); } pub fn move_word_right(self: *Self, _: Context) Result { const root = try self.buf_root(); self.with_cursors_const(root, move_cursor_word_right) catch {}; self.clamp(); } pub fn move_word_right_vim(self: *Self, _: Context) Result { const root = try self.buf_root(); self.with_cursors_const(root, move_cursor_word_right_vim) catch {}; self.clamp(); } fn move_cursor_to_char_left(root: Buffer.Root, cursor: *Cursor, ctx: Context, plane: Plane) error{Stop}!void { var egc: []const u8 = undefined; if (!(ctx.args.match(.{tp.extract(&egc)}) catch return error.Stop)) return error.Stop; try move_cursor_left(root, cursor, plane); while (true) { const curr_egc, _, _ = root.ecg_at(cursor.row, cursor.col, plane.metrics()) catch return error.Stop; if (std.mem.eql(u8, curr_egc, egc)) return; if (is_eol_left(root, cursor, plane)) return; move_cursor_left(root, cursor, plane) catch return error.Stop; } } pub fn move_cursor_to_char_right(root: Buffer.Root, cursor: *Cursor, ctx: Context, plane: Plane) error{Stop}!void { var egc: []const u8 = undefined; if (!(ctx.args.match(.{tp.extract(&egc)}) catch return error.Stop)) return error.Stop; try move_cursor_right(root, cursor, plane); while (true) { const curr_egc, _, _ = root.ecg_at(cursor.row, cursor.col, plane.metrics()) catch return error.Stop; if (std.mem.eql(u8, curr_egc, egc)) return; if (is_eol_right(root, cursor, plane)) return; move_cursor_right(root, cursor, plane) catch return error.Stop; } } pub fn move_to_char_left(self: *Self, ctx: Context) Result { const root = try self.buf_root(); self.with_cursors_const_arg(root, move_cursor_to_char_left, ctx) catch {}; self.clamp(); } pub fn move_to_char_right(self: *Self, ctx: Context) Result { const root = try self.buf_root(); self.with_cursors_const_arg(root, move_cursor_to_char_right, ctx) catch {}; self.clamp(); } pub fn move_up(self: *Self, _: Context) Result { const root = try self.buf_root(); self.with_cursors_const(root, move_cursor_up) catch {}; self.clamp(); } pub fn add_cursor_up(self: *Self, _: Context) Result { try self.push_cursor(); const primary = self.get_primary(); const root = try self.buf_root(); move_cursor_up(root, &primary.cursor, self.plane) catch {}; self.clamp(); } pub fn move_down(self: *Self, _: Context) Result { const root = try self.buf_root(); self.with_cursors_const(root, move_cursor_down) catch {}; self.clamp(); } pub fn add_cursor_down(self: *Self, _: Context) Result { try self.push_cursor(); const primary = self.get_primary(); const root = try self.buf_root(); move_cursor_down(root, &primary.cursor, self.plane) catch {}; self.clamp(); } pub fn add_cursor_next_match(self: *Self, _: Context) Result { try self.send_editor_jump_source(); if (self.matches.items.len == 0) { const root = self.buf_root() catch return; self.with_cursors_const(root, move_cursor_word_begin) catch {}; try self.with_selections_const(root, move_cursor_word_end); } else if (self.get_next_match(self.get_primary().cursor)) |match| { try self.push_cursor(); const primary = self.get_primary(); const root = self.buf_root() catch return; primary.selection = match.to_selection(); match.has_selection = true; primary.cursor.move_to(root, match.end.row, match.end.col, self.plane.metrics()) catch return; } self.clamp(); try self.send_editor_jump_destination(); } pub fn add_cursor_all_matches(self: *Self, _: Context) Result { if (self.matches.items.len == 0) return; try self.send_editor_jump_source(); while (self.get_next_match(self.get_primary().cursor)) |match| { try self.push_cursor(); const primary = self.get_primary(); const root = self.buf_root() catch return; primary.selection = match.to_selection(); match.has_selection = true; primary.cursor.move_to(root, match.end.row, match.end.col, self.plane.metrics()) catch return; } self.clamp(); try self.send_editor_jump_destination(); } fn add_cursors_to_cursel_line_ends(self: *Self, root: Buffer.Root, cursel: *CurSel) !void { var sel = cursel.enable_selection(); sel.normalize(); var row = sel.begin.row; while (row <= sel.end.row) : (row += 1) { const new_cursel = try self.cursels.addOne(); new_cursel.* = CurSel{ .selection = null, .cursor = .{ .row = row, .col = 0, }, }; new_cursel.*.?.cursor.move_end(root, self.plane.metrics()); } } pub fn add_cursors_to_line_ends(self: *Self, _: Context) Result { const root = try self.buf_root(); const cursels = try self.cursels.toOwnedSlice(); defer self.cursels.allocator.free(cursels); for (cursels) |*cursel_| if (cursel_.*) |*cursel| try self.add_cursors_to_cursel_line_ends(root, cursel); self.collapse_cursors(); self.clamp(); } fn pull_cursel_up(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; const saved = cursel.*; const sel = cursel.expand_selection_to_line(root, self.plane); var sfa = std.heap.stackFallback(4096, self.allocator); const cut_text = copy_selection(root, sel.*, sfa.get(), self.plane) catch return error.Stop; defer allocator.free(cut_text); root = try self.delete_selection(root, cursel, allocator); try cursel.cursor.move_up(root, self.plane.metrics()); root = self.insert(root, cursel, cut_text, allocator) catch return error.Stop; cursel.* = saved; try cursel.cursor.move_up(root, self.plane.metrics()); if (cursel.selection) |*sel_| { try sel_.begin.move_up(root, self.plane.metrics()); try sel_.end.move_up(root, self.plane.metrics()); } return root; } pub fn pull_up(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.with_cursels_mut(b.root, pull_cursel_up, b.allocator); try self.update_buf(root); self.clamp(); } fn pull_cursel_down(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; const saved = cursel.*; const sel = cursel.expand_selection_to_line(root, self.plane); var sfa = std.heap.stackFallback(4096, self.allocator); const cut_text = copy_selection(root, sel.*, sfa.get(), self.plane) catch return error.Stop; defer allocator.free(cut_text); root = try self.delete_selection(root, cursel, allocator); try cursel.cursor.move_down(root, self.plane.metrics()); root = self.insert(root, cursel, cut_text, allocator) catch return error.Stop; cursel.* = saved; try cursel.cursor.move_down(root, self.plane.metrics()); if (cursel.selection) |*sel_| { try sel_.begin.move_down(root, self.plane.metrics()); try sel_.end.move_down(root, self.plane.metrics()); } return root; } pub fn pull_down(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.with_cursels_mut(b.root, pull_cursel_down, b.allocator); try self.update_buf(root); self.clamp(); } fn dupe_cursel_up(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; const sel: Selection = if (cursel.selection) |sel_| sel_ else Selection.line_from_cursor(cursel.cursor, root, self.plane.metrics()); cursel.selection = null; var sfa = std.heap.stackFallback(4096, self.allocator); const text = copy_selection(root, sel, sfa.get(), self.plane) catch return error.Stop; defer allocator.free(text); cursel.cursor = sel.begin; root = self.insert(root, cursel, text, allocator) catch return error.Stop; cursel.selection = .{ .begin = sel.begin, .end = sel.end }; cursel.cursor = sel.begin; return root; } pub fn dupe_up(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.with_cursels_mut(b.root, dupe_cursel_up, b.allocator); try self.update_buf(root); self.clamp(); } fn dupe_cursel_down(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; const sel: Selection = if (cursel.selection) |sel_| sel_ else Selection.line_from_cursor(cursel.cursor, root, self.plane.metrics()); cursel.selection = null; var sfa = std.heap.stackFallback(4096, self.allocator); const text = copy_selection(root, sel, sfa.get(), self.plane) catch return error.Stop; defer allocator.free(text); cursel.cursor = sel.end; root = self.insert(root, cursel, text, allocator) catch return error.Stop; cursel.selection = .{ .begin = sel.end, .end = cursel.cursor }; return root; } pub fn dupe_down(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.with_cursels_mut(b.root, dupe_cursel_down, b.allocator); try self.update_buf(root); self.clamp(); } fn toggle_cursel_prefix(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; const saved = cursel.*; const sel = cursel.expand_selection_to_line(root, self.plane); var sfa = std.heap.stackFallback(4096, self.allocator); const alloc = sfa.get(); const text = copy_selection(root, sel.*, alloc, self.plane) catch return error.Stop; defer allocator.free(text); root = try self.delete_selection(root, cursel, allocator); const new_text = text_manip.toggle_prefix_in_text(self.prefix, text, alloc) catch return error.Stop; root = self.insert(root, cursel, new_text, allocator) catch return error.Stop; cursel.* = saved; return root; } pub fn toggle_prefix(self: *Self, ctx: Context) Result { var prefix: []const u8 = undefined; if (!try ctx.args.match(.{tp.extract(&prefix)})) return; @memcpy(self.prefix_buf[0..prefix.len], prefix); self.prefix = self.prefix_buf[0..prefix.len]; const b = try self.buf_for_update(); const root = try self.with_cursels_mut(b.root, toggle_cursel_prefix, b.allocator); try self.update_buf(root); } pub fn toggle_comment(self: *Self, _: Context) Result { const comment = if (self.syntax) |syn| syn.file_type.comment else "//"; return self.toggle_prefix(command.fmt(.{comment})); } fn indent_cursor(self: *Self, root: Buffer.Root, cursor: Cursor, allocator: Allocator) error{Stop}!Buffer.Root { const space = " "; var cursel: CurSel = .{}; cursel.cursor = cursor; const cols = 4 - find_first_non_ws(root, cursel.cursor.row, self.plane) % 4; try smart_move_cursor_begin(root, &cursel.cursor, self.plane); return self.insert(root, &cursel, space[0..cols], allocator) catch return error.Stop; } fn indent_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { if (cursel.selection) |sel_| { var root = root_; var sel = sel_; sel.normalize(); while (sel.begin.row < sel.end.row) : (sel.begin.row += 1) root = try self.indent_cursor(root, sel.begin, allocator); if (sel.end.col > 0) root = try self.indent_cursor(root, sel.end, allocator); return root; } else return try self.indent_cursor(root_, cursel.cursor, allocator); } pub fn indent(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.with_cursels_mut(b.root, indent_cursel, b.allocator); try self.update_buf(root); } fn unindent_cursor(self: *Self, root: Buffer.Root, cursel: *CurSel, cursor: Cursor, allocator: Allocator) error{Stop}!Buffer.Root { const saved = cursel.*; var newroot = root; defer { cursel.* = saved; cursel.cursor.clamp_to_buffer(newroot, self.plane.metrics()); } cursel.selection = null; cursel.cursor = cursor; const first = find_first_non_ws(root, cursel.cursor.row, self.plane); if (first == 0) return error.Stop; const off = first % 4; const cols = if (off == 0) 4 else off; const sel = cursel.enable_selection(); sel.begin.move_begin(); try sel.end.move_to(root, sel.end.row, cols, self.plane.metrics()); if (cursel.cursor.col < cols) try cursel.cursor.move_to(root, cursel.cursor.row, cols, self.plane.metrics()); newroot = try self.delete_selection(root, cursel, allocator); return newroot; } fn unindent_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { if (cursel.selection) |sel_| { var root = root_; var sel = sel_; sel.normalize(); while (sel.begin.row < sel.end.row) : (sel.begin.row += 1) root = try self.unindent_cursor(root, cursel, sel.begin, allocator); if (sel.end.col > 0) root = try self.unindent_cursor(root, cursel, sel.end, allocator); return root; } else return self.unindent_cursor(root_, cursel, cursel.cursor, allocator); } pub fn unindent(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.with_cursels_mut(b.root, unindent_cursel, b.allocator); try self.update_buf(root); } pub fn move_scroll_up(self: *Self, _: Context) Result { const root = try self.buf_root(); self.with_cursors_const(root, move_cursor_up) catch {}; self.view.move_up() catch {}; self.clamp(); } pub fn move_scroll_down(self: *Self, _: Context) Result { const root = try self.buf_root(); self.with_cursors_const(root, move_cursor_down) catch {}; self.view.move_down(root) catch {}; self.clamp(); } pub fn move_scroll_left(self: *Self, _: Context) Result { self.view.move_left() catch {}; } pub fn move_scroll_right(self: *Self, _: Context) Result { self.view.move_right() catch {}; } pub fn move_scroll_page_up(self: *Self, _: Context) Result { if (self.screen_cursor(&self.get_primary().cursor)) |cursor| { const root = try self.buf_root(); self.with_cursors_and_view_const(root, move_cursor_page_up, &self.view) catch {}; const new_cursor_row = self.get_primary().cursor.row; self.update_scroll_dest_abs(if (cursor.row > new_cursor_row) 0 else new_cursor_row - cursor.row); } else { return self.move_page_up(.{}); } } pub fn move_scroll_page_down(self: *Self, _: Context) Result { if (self.screen_cursor(&self.get_primary().cursor)) |cursor| { const root = try self.buf_root(); self.with_cursors_and_view_const(root, move_cursor_page_down, &self.view) catch {}; const new_cursor_row = self.get_primary().cursor.row; self.update_scroll_dest_abs(if (cursor.row > new_cursor_row) 0 else new_cursor_row - cursor.row); } else { return self.move_page_down(.{}); } } pub fn smart_move_begin(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_cursors_const(root, smart_move_cursor_begin); self.clamp(); } pub fn move_begin(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_cursors_const(root, move_cursor_begin); self.clamp(); } pub fn move_end(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_cursors_const(root, move_cursor_end); self.clamp(); } pub fn move_page_up(self: *Self, _: Context) Result { try self.send_editor_jump_source(); const root = try self.buf_root(); try self.with_cursors_and_view_const(root, move_cursor_page_up, &self.view); self.clamp(); } pub fn move_page_down(self: *Self, _: Context) Result { try self.send_editor_jump_source(); const root = try self.buf_root(); try self.with_cursors_and_view_const(root, move_cursor_page_down, &self.view); self.clamp(); try self.send_editor_jump_destination(); } pub fn move_buffer_begin(self: *Self, _: Context) Result { try self.send_editor_jump_source(); self.cancel_all_selections(); self.get_primary().cursor.move_buffer_begin(); self.clamp(); try self.send_editor_jump_destination(); } pub fn move_buffer_end(self: *Self, _: Context) Result { try self.send_editor_jump_source(); self.cancel_all_selections(); const root = self.buf_root() catch return; self.get_primary().cursor.move_buffer_end(root, self.plane.metrics()); self.clamp(); try self.send_editor_jump_destination(); } pub fn cancel(self: *Self, _: Context) Result { self.cancel_all_selections(); self.cancel_all_matches(); } pub fn select_up(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_up); self.clamp(); } pub fn select_down(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_down); self.clamp(); } pub fn select_scroll_up(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_up); self.view.move_up() catch {}; self.clamp(); } pub fn select_scroll_down(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_down); self.view.move_down(root) catch {}; self.clamp(); } pub fn select_left(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_left); self.clamp(); } pub fn select_right(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_right); self.clamp(); } pub fn select_word_left(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_word_left); self.clamp(); } pub fn select_word_right(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_word_right); self.clamp(); } pub fn select_word_begin(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_word_begin); self.clamp(); } pub fn select_word_end(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_word_end); self.clamp(); } pub fn select_to_char_left(self: *Self, ctx: Context) Result { const root = try self.buf_root(); self.with_selections_const_arg(root, move_cursor_to_char_left, ctx) catch {}; self.clamp(); } pub fn select_to_char_right(self: *Self, ctx: Context) Result { const root = try self.buf_root(); self.with_selections_const_arg(root, move_cursor_to_char_right, ctx) catch {}; self.clamp(); } pub fn select_begin(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_begin); self.clamp(); } pub fn smart_select_begin(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, smart_move_cursor_begin); self.clamp(); } pub fn select_end(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_end); self.clamp(); } pub fn select_buffer_begin(self: *Self, _: Context) Result { try self.send_editor_jump_source(); const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_buffer_begin); self.clamp(); try self.send_editor_jump_destination(); } pub fn select_buffer_end(self: *Self, _: Context) Result { try self.send_editor_jump_source(); const root = try self.buf_root(); try self.with_selections_const(root, move_cursor_buffer_end); self.clamp(); try self.send_editor_jump_destination(); } pub fn select_page_up(self: *Self, _: Context) Result { try self.send_editor_jump_source(); const root = try self.buf_root(); try self.with_selections_and_view_const(root, move_cursor_page_up, &self.view); self.clamp(); try self.send_editor_jump_destination(); } pub fn select_page_down(self: *Self, _: Context) Result { try self.send_editor_jump_source(); const root = try self.buf_root(); try self.with_selections_and_view_const(root, move_cursor_page_down, &self.view); self.clamp(); try self.send_editor_jump_destination(); } pub fn select_all(self: *Self, _: Context) Result { try self.send_editor_jump_source(); self.cancel_all_selections(); const primary = self.get_primary(); const sel = primary.enable_selection(); const root = try self.buf_root(); try expand_selection_to_all(root, sel, self.plane); primary.cursor = sel.end; self.clamp(); try self.send_editor_jump_destination(); } fn select_word_at_cursor(self: *Self, cursel: *CurSel) !*Selection { const root = try self.buf_root(); const sel = cursel.enable_selection(); defer cursel.check_selection(); sel.normalize(); try move_cursor_word_begin(root, &sel.begin, self.plane); try move_cursor_word_end(root, &sel.end, self.plane); cursel.cursor = sel.end; return sel; } fn select_line_at_cursor(self: *Self, cursel: *CurSel) !void { const root = try self.buf_root(); const sel = cursel.enable_selection(); sel.normalize(); try move_cursor_begin(root, &sel.begin, self.plane); try move_cursor_end(root, &sel.end, self.plane); cursel.cursor = sel.end; } fn selection_reverse(_: Buffer.Root, cursel: *CurSel) !void { if (cursel.selection) |*sel| { sel.reverse(); cursel.cursor = sel.end; } } pub fn selections_reverse(self: *Self, _: Context) Result { const root = try self.buf_root(); try self.with_cursels_const(root, selection_reverse); self.clamp(); } pub fn insert_chars(self: *Self, ctx: Context) Result { var chars: []const u8 = undefined; if (!try ctx.args.match(.{tp.extract(&chars)})) return error.InvalidArgument; const b = try self.buf_for_update(); var root = b.root; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { root = try self.insert(root, cursel, chars, b.allocator); }; try self.update_buf(root); self.clamp(); } pub fn insert_line(self: *Self, _: Context) Result { const b = try self.buf_for_update(); var root = b.root; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { root = try self.insert(root, cursel, "\n", b.allocator); }; try self.update_buf(root); self.clamp(); } pub fn smart_insert_line(self: *Self, _: Context) Result { const b = try self.buf_for_update(); var root = b.root; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { var leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.plane), cursel.cursor.col); var sfa = std.heap.stackFallback(512, self.allocator); const allocator = sfa.get(); var stream = std.ArrayList(u8).init(allocator); defer stream.deinit(); var writer = stream.writer(); _ = try writer.write("\n"); while (leading_ws > 0) : (leading_ws -= 1) _ = try writer.write(" "); root = try self.insert(root, cursel, stream.items, b.allocator); }; try self.update_buf(root); self.clamp(); } pub fn insert_line_before(self: *Self, _: Context) Result { const b = try self.buf_for_update(); var root = b.root; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { try move_cursor_begin(root, &cursel.cursor, self.plane); root = try self.insert(root, cursel, "\n", b.allocator); try move_cursor_left(root, &cursel.cursor, self.plane); }; try self.update_buf(root); self.clamp(); } pub fn smart_insert_line_before(self: *Self, _: Context) Result { const b = try self.buf_for_update(); var root = b.root; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { var leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.plane), cursel.cursor.col); try move_cursor_begin(root, &cursel.cursor, self.plane); root = try self.insert(root, cursel, "\n", b.allocator); try move_cursor_left(root, &cursel.cursor, self.plane); var sfa = std.heap.stackFallback(512, self.allocator); const allocator = sfa.get(); var stream = std.ArrayList(u8).init(allocator); defer stream.deinit(); var writer = stream.writer(); while (leading_ws > 0) : (leading_ws -= 1) _ = try writer.write(" "); if (stream.items.len > 0) root = try self.insert(root, cursel, stream.items, b.allocator); }; try self.update_buf(root); self.clamp(); } pub fn insert_line_after(self: *Self, _: Context) Result { const b = try self.buf_for_update(); var root = b.root; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { try move_cursor_end(root, &cursel.cursor, self.plane); root = try self.insert(root, cursel, "\n", b.allocator); }; try self.update_buf(root); self.clamp(); } pub fn smart_insert_line_after(self: *Self, _: Context) Result { const b = try self.buf_for_update(); var root = b.root; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { var leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.plane), cursel.cursor.col); try move_cursor_end(root, &cursel.cursor, self.plane); var sfa = std.heap.stackFallback(512, self.allocator); const allocator = sfa.get(); var stream = std.ArrayList(u8).init(allocator); defer stream.deinit(); var writer = stream.writer(); _ = try writer.write("\n"); while (leading_ws > 0) : (leading_ws -= 1) _ = try writer.write(" "); if (stream.items.len > 0) root = try self.insert(root, cursel, stream.items, b.allocator); }; try self.update_buf(root); self.clamp(); } pub fn enable_fast_scroll(self: *Self, _: Context) Result { self.fast_scroll = true; } pub fn disable_fast_scroll(self: *Self, _: Context) Result { self.fast_scroll = false; } pub fn enable_jump_mode(self: *Self, _: Context) Result { self.jump_mode = true; tui.current().rdr.request_mouse_cursor_pointer(true); } pub fn disable_jump_mode(self: *Self, _: Context) Result { self.jump_mode = false; tui.current().rdr.request_mouse_cursor_text(true); } fn update_syntax(self: *Self) !void { const frame = tracy.initZone(@src(), .{ .name = "editor update syntax" }); defer frame.deinit(); const root = try self.buf_root(); const token = @intFromPtr(root); if (root.lines() > root_mod.max_syntax_lines) return; if (self.syntax_token == token) return; if (self.syntax) |syn| { if (self.syntax_refresh_full) { var content = std.ArrayList(u8).init(self.allocator); defer content.deinit(); try root.store(content.writer()); try syn.refresh_full(content.items); self.syntax_refresh_full = false; } else { try syn.refresh_from_buffer(root, self.plane.metrics()); } self.syntax_token = token; } else { var content = std.ArrayList(u8).init(self.allocator); defer content.deinit(); try root.store(content.writer()); self.syntax = if (tp.env.get().is("no-syntax")) null else syntax.create_guess_file_type(self.allocator, content.items, self.file_path) catch |e| switch (e) { error.NotFound => null, else => return e, }; } } fn reset_syntax(self: *Self) void { if (self.syntax) |_| self.syntax_refresh_full = true; } pub fn dump_current_line(self: *Self, _: Context) Result { const root = self.buf_root() catch return; const primary = self.get_primary(); var tree = std.ArrayList(u8).init(self.allocator); defer tree.deinit(); root.debug_render_chunks(primary.cursor.row, &tree, self.plane.metrics()) catch |e| return self.logger.print("line {d}: {any}", .{ primary.cursor.row, e }); self.logger.print("line {d}:{s}", .{ primary.cursor.row, std.fmt.fmtSliceEscapeLower(tree.items) }); } pub fn dump_current_line_tree(self: *Self, _: Context) Result { const root = self.buf_root() catch return; const primary = self.get_primary(); var tree = std.ArrayList(u8).init(self.allocator); defer tree.deinit(); root.debug_line_render_tree(primary.cursor.row, &tree) catch |e| return self.logger.print("line {d} ast: {any}", .{ primary.cursor.row, e }); self.logger.print("line {d} ast:{s}", .{ primary.cursor.row, std.fmt.fmtSliceEscapeLower(tree.items) }); } pub fn undo(self: *Self, _: Context) Result { try self.restore_undo(); self.clamp(); } pub fn redo(self: *Self, _: Context) Result { try self.restore_redo(); self.clamp(); } pub fn open_buffer_from_file(self: *Self, ctx: Context) Result { var file_path: []const u8 = undefined; if (ctx.args.match(.{tp.extract(&file_path)}) catch false) { try self.open(file_path); self.clamp(); } else return error.InvalidArgument; } pub fn open_scratch_buffer(self: *Self, ctx: Context) Result { var file_path: []const u8 = undefined; var content: []const u8 = undefined; if (ctx.args.match(.{ tp.extract(&file_path), tp.extract(&content) }) catch false) { try self.open_scratch(file_path, content); self.clamp(); } else return error.InvalidArgument; } pub fn save_file(self: *Self, _: Context) Result { if (tui.current().config.enable_format_on_save) if (self.get_formatter()) |_| { self.need_save_after_filter = true; const primary = self.get_primary(); const sel = primary.selection; primary.selection = null; defer primary.selection = sel; try self.format(.{}); return; }; try self.save(); } pub fn save_file_as(self: *Self, ctx: Context) Result { var file_path: []const u8 = undefined; if (ctx.args.match(.{tp.extract(&file_path)}) catch false) { try self.save_as(file_path); } else return error.InvalidArgument; } pub fn close_file(self: *Self, _: Context) Result { self.cancel_all_selections(); try self.close(); } pub fn close_file_without_saving(self: *Self, _: Context) Result { self.cancel_all_selections(); try self.close_dirty(); } pub fn find_query(self: *Self, ctx: Context) Result { var query: []const u8 = undefined; if (ctx.args.match(.{tp.extract(&query)}) catch false) { try self.find_in_buffer(query); self.clamp(); } else return error.InvalidArgument; } fn find_in(self: *Self, query: []const u8, comptime find_f: ripgrep.FindF, write_buffer: bool) !void { const root = try self.buf_root(); self.cancel_all_matches(); if (std.mem.indexOfScalar(u8, query, '\n')) |_| return; self.logger.print("find:{s}", .{std.fmt.fmtSliceEscapeLower(query)}); var rg = try find_f(self.allocator, query, "A"); defer rg.deinit(); if (write_buffer) { var rg_buffer = rg.bufferedWriter(); try root.store(rg_buffer.writer()); try rg_buffer.flush(); } } pub fn push_find_history(self: *Self, query: []const u8) void { if (query.len == 0) return; const history = if (self.find_history) |*hist| hist else ret: { self.find_history = std.ArrayList([]const u8).init(self.allocator); break :ret &self.find_history.?; }; for (history.items, 0..) |entry, i| if (std.mem.eql(u8, entry, query)) self.allocator.free(history.orderedRemove(i)); const new = self.allocator.dupe(u8, query) catch return; (history.addOne() catch return).* = new; } fn set_last_find_query(self: *Self, query: []const u8) void { if (self.last_find_query) |last| { if (query.ptr != last.ptr) { self.allocator.free(last); self.last_find_query = self.allocator.dupe(u8, query) catch return; } } else self.last_find_query = self.allocator.dupe(u8, query) catch return; } pub fn find_in_buffer(self: *Self, query: []const u8) !void { self.set_last_find_query(query); return self.find_in_buffer_sync(query); } fn find_in_buffer_sync(self: *Self, query: []const u8) !void { const Ctx = struct { matches: usize = 0, self: *Self, fn cb(ctx_: *anyopaque, begin_row: usize, begin_col: usize, end_row: usize, end_col: usize) error{Stop}!void { const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); ctx.matches += 1; ctx.self.add_match_internal(begin_row, begin_col, end_row, end_col); if (ctx.matches >= max_matches) return error.Stop; } }; const root = try self.buf_root(); defer self.add_match_done(); var ctx: Ctx = .{ .self = self }; self.init_matches_update(); try root.find_all_ranges(query, &ctx, Ctx.cb, self.allocator); } fn find_in_buffer_async(self: *Self, query: []const u8) !void { const finder = struct { allocator: Allocator, query: []const u8, parent: tp.pid_ref, root: Buffer.Root, token: usize, matches: Match.List, const finder = @This(); fn start(fdr: *finder) tp.result { fdr.find() catch {}; return tp.exit_normal(); } fn find(fdr: *finder) !void { const Ctx = struct { matches: usize = 0, fdr: *finder, const Ctx = @This(); fn cb(ctx_: *anyopaque, begin_row: usize, begin_col: usize, end_row: usize, end_col: usize) error{Stop}!void { const ctx = @as(*Ctx, @ptrCast(@alignCast(ctx_))); ctx.matches += 1; const match: Match = .{ .begin = .{ .row = begin_row, .col = begin_col }, .end = .{ .row = end_row, .col = end_col } }; (ctx.fdr.matches.addOne() catch return).* = match; if (ctx.fdr.matches.items.len >= max_match_batch) ctx.fdr.send_batch() catch return error.Stop; if (ctx.matches >= max_matches) return error.Stop; } }; defer fdr.parent.send(.{ "A", "done", fdr.token }) catch {}; defer fdr.allocator.free(fdr.query); var ctx: Ctx = .{ .fdr = fdr }; try fdr.root.find_all_ranges(fdr.query, &ctx, Ctx.cb, fdr.a); try fdr.send_batch(); } fn send_batch(fdr: *finder) !void { if (fdr.matches.items.len == 0) return; var buf: [max_match_batch * @sizeOf(Match)]u8 = undefined; var stream = std.io.fixedBufferStream(&buf); const writer = stream.writer(); try cbor.writeArrayHeader(writer, 4); try cbor.writeValue(writer, "A"); try cbor.writeValue(writer, "batch"); try cbor.writeValue(writer, fdr.token); try cbor.writeArrayHeader(writer, fdr.matches.items.len); for (fdr.matches.items) |m_| if (m_) |m| { try cbor.writeArray(writer, .{ m.begin.row, m.begin.col, m.end.row, m.end.col }); }; try fdr.parent.send_raw(.{ .buf = stream.getWritten() }); fdr.matches.clearRetainingCapacity(); } }; self.init_matches_update(); const fdr = try self.allocator.create(finder); fdr.* = .{ .a = self.allocator, .query = try self.allocator.dupe(u8, query), .parent = tp.self_pid(), .root = try self.buf_root(), .token = self.match_token, .matches = Match.List.init(self.allocator), }; const pid = try tp.spawn_link(self.allocator, fdr, finder.start, "editor.find"); pid.deinit(); } pub fn find_in_buffer_ext(self: *Self, query: []const u8) !void { return self.find_in(query, ripgrep.find_in_stdin, true); } pub fn add_match(self: *Self, m: tp.message) !void { var begin_line: usize = undefined; var begin_pos: usize = undefined; var end_line: usize = undefined; var end_pos: usize = undefined; var batch_cbor: []const u8 = undefined; if (try m.match(.{ "A", "done", self.match_token })) { self.add_match_done(); } else if (try m.match(.{ tp.any, "batch", self.match_token, tp.extract_cbor(&batch_cbor) })) { try self.add_match_batch(batch_cbor); } else if (try m.match(.{ tp.any, self.match_token, tp.extract(&begin_line), tp.extract(&begin_pos), tp.extract(&end_line), tp.extract(&end_pos) })) { self.add_match_internal(begin_line, begin_pos, end_line, end_pos); } else if (try m.match(.{ tp.any, tp.extract(&begin_line), tp.extract(&begin_pos), tp.extract(&end_line), tp.extract(&end_pos) })) { self.add_match_internal(begin_line, begin_pos, end_line, end_pos); } } pub fn add_match_batch(self: *Self, cb: []const u8) !void { var iter = cb; var begin_line: usize = undefined; var begin_pos: usize = undefined; var end_line: usize = undefined; var end_pos: usize = undefined; var len = try cbor.decodeArrayHeader(&iter); while (len > 0) : (len -= 1) if (try cbor.matchValue(&iter, .{ tp.extract(&begin_line), tp.extract(&begin_pos), tp.extract(&end_line), tp.extract(&end_pos) })) { self.add_match_internal(begin_line, begin_pos, end_line, end_pos); } else return; } fn add_match_done(self: *Self) void { if (self.matches.items.len > 0) { if (self.find_operation) |op| { self.find_operation = null; switch (op) { .goto_next_match => self.goto_next_match(.{}) catch {}, .goto_prev_match => self.goto_prev_match(.{}) catch {}, } return; } } self.match_done_token = self.match_token; self.need_render(); } fn add_match_internal(self: *Self, begin_line_: usize, begin_pos_: usize, end_line_: usize, end_pos_: usize) void { const root = self.buf_root() catch return; const begin_line = begin_line_ - 1; const end_line = end_line_ - 1; const begin_pos = root.pos_to_width(begin_line, begin_pos_, self.plane.metrics()) catch return; const end_pos = root.pos_to_width(end_line, end_pos_, self.plane.metrics()) catch return; var match: Match = .{ .begin = .{ .row = begin_line, .col = begin_pos }, .end = .{ .row = end_line, .col = end_pos } }; if (match.end.eql(self.get_primary().cursor)) match.has_selection = true; (self.matches.addOne() catch return).* = match; } fn find_selection_match(self: *const Self, sel: Selection) ?*Match { for (self.matches.items) |*match_| if (match_.*) |*match| { if (match.to_selection().eql(sel)) return match; }; return null; } fn scan_first_match(self: *const Self) ?*Match { for (self.matches.items) |*match_| if (match_.*) |*match| { if (match.has_selection) continue; return match; }; return null; } fn scan_next_match(self: *const Self, cursor: Cursor) ?*Match { const row = cursor.row; const col = cursor.col; const multi_cursor = self.cursels.items.len > 1; for (self.matches.items) |*match_| if (match_.*) |*match| if ((!multi_cursor or !match.has_selection) and (row < match.begin.row or (row == match.begin.row and col < match.begin.col))) return match; return null; } fn get_next_match(self: *const Self, cursor: Cursor) ?*Match { if (self.scan_next_match(cursor)) |match| return match; var cursor_ = cursor; cursor_.move_buffer_begin(); return self.scan_first_match(); } fn scan_prev_match(self: *const Self, cursor: Cursor) ?*Match { const row = cursor.row; const col = cursor.col; const count = self.matches.items.len; for (0..count) |i| { const match = if (self.matches.items[count - 1 - i]) |*m| m else continue; if (!match.has_selection and (row > match.end.row or (row == match.end.row and col > match.end.col))) return match; } return null; } fn get_prev_match(self: *const Self, cursor: Cursor) ?*Match { if (self.scan_prev_match(cursor)) |match| return match; const root = self.buf_root() catch return null; var cursor_ = cursor; cursor_.move_buffer_end(root, self.plane.metrics()); return self.scan_prev_match(cursor_); } pub fn move_cursor_next_match(self: *Self, _: Context) Result { const primary = self.get_primary(); if (self.get_next_match(primary.cursor)) |match| { const root = self.buf_root() catch return; if (primary.selection) |sel| if (self.find_selection_match(sel)) |match_| { match_.has_selection = false; }; primary.selection = match.to_selection(); primary.cursor.move_to(root, match.end.row, match.end.col, self.plane.metrics()) catch return; self.clamp(); } } pub fn goto_next_match(self: *Self, ctx: Context) Result { try self.send_editor_jump_source(); self.cancel_all_selections(); if (self.matches.items.len == 0) { if (self.last_find_query) |last| { self.find_operation = .goto_next_match; try self.find_in_buffer(last); } } try self.move_cursor_next_match(ctx); try self.send_editor_jump_destination(); } pub fn move_cursor_prev_match(self: *Self, _: Context) Result { const primary = self.get_primary(); if (self.get_prev_match(primary.cursor)) |match| { const root = self.buf_root() catch return; if (primary.selection) |sel| if (self.find_selection_match(sel)) |match_| { match_.has_selection = false; }; primary.selection = match.to_selection(); primary.selection.?.reverse(); primary.cursor.move_to(root, match.begin.row, match.begin.col, self.plane.metrics()) catch return; self.clamp(); } } pub fn goto_prev_match(self: *Self, ctx: Context) Result { try self.send_editor_jump_source(); self.cancel_all_selections(); if (self.matches.items.len == 0) { if (self.last_find_query) |last| { self.find_operation = .goto_prev_match; try self.find_in_buffer(last); } } try self.move_cursor_prev_match(ctx); try self.send_editor_jump_destination(); } pub fn goto_next_diagnostic(self: *Self, _: Context) Result { if (self.diagnostics.items.len == 0) { if (command.getId("goto_next_file")) |id| return command.execute(id, .{}); 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, _: Context) Result { if (self.diagnostics.items.len == 0) { if (command.getId("goto_prev_file")) |id| return command.execute(id, .{}); 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) !void { const root = self.buf_root() catch return; const primary = self.get_primary(); try self.send_editor_jump_source(); self.cancel_all_selections(); try primary.cursor.move_to(root, diag.sel.begin.row, diag.sel.begin.col, self.plane.metrics()); 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: Context) Result { try self.send_editor_jump_source(); var line: usize = 0; if (!try ctx.args.match(.{tp.extract(&line)})) return error.InvalidArgument; const root = self.buf_root() catch return; self.cancel_all_selections(); const primary = self.get_primary(); try primary.cursor.move_to(root, @intCast(if (line < 1) 0 else line - 1), primary.cursor.col, self.plane.metrics()); self.clamp(); try self.send_editor_jump_destination(); } pub fn goto_column(self: *Self, ctx: Context) Result { var column: usize = 0; if (!try ctx.args.match(.{tp.extract(&column)})) return error.InvalidArgument; const root = self.buf_root() catch return; const primary = self.get_primary(); try primary.cursor.move_to(root, primary.cursor.row, @intCast(if (column < 1) 0 else column - 1), self.plane.metrics()); self.clamp(); } pub fn goto_line_and_column(self: *Self, ctx: Context) Result { try self.send_editor_jump_source(); var line: usize = 0; var column: usize = 0; var have_sel: bool = false; var sel: Selection = .{}; if (try ctx.args.match(.{ tp.extract(&line), tp.extract(&column), })) { // self.logger.print("goto: l:{d} c:{d}", .{ line, column }); } else if (try ctx.args.match(.{ tp.extract(&line), tp.extract(&column), tp.extract(&sel.begin.row), tp.extract(&sel.begin.col), tp.extract(&sel.end.row), tp.extract(&sel.end.col), })) { // self.logger.print("goto: l:{d} c:{d} {any}", .{ line, column, sel }); have_sel = true; } else return error.InvalidArgument; self.cancel_all_selections(); const root = self.buf_root() catch return; const primary = self.get_primary(); try primary.cursor.move_to( root, @intCast(if (line < 1) 0 else line - 1), @intCast(if (column < 1) 0 else column - 1), self.plane.metrics(), ); if (have_sel) primary.selection = sel; if (self.view.is_visible(&primary.cursor)) self.clamp() else try self.scroll_view_center(.{}); try self.send_editor_jump_destination(); self.need_render(); } pub fn goto_definition(self: *Self, _: Context) Result { const file_path = self.file_path orelse return; const primary = self.get_primary(); return project_manager.goto_definition(file_path, primary.cursor.row, primary.cursor.col); } pub fn goto_declaration(self: *Self, _: Context) Result { const file_path = self.file_path orelse return; const primary = self.get_primary(); return project_manager.goto_declaration(file_path, primary.cursor.row, primary.cursor.col); } pub fn goto_implementation(self: *Self, _: Context) Result { const file_path = self.file_path orelse return; const primary = self.get_primary(); return project_manager.goto_implementation(file_path, primary.cursor.row, primary.cursor.col); } pub fn goto_type_definition(self: *Self, _: Context) Result { const file_path = self.file_path orelse return; const primary = self.get_primary(); return project_manager.goto_type_definition(file_path, primary.cursor.row, primary.cursor.col); } pub fn references(self: *Self, _: Context) Result { const file_path = self.file_path orelse return; const primary = self.get_primary(); return project_manager.references(file_path, primary.cursor.row, primary.cursor.col); } pub fn completion(self: *Self, _: Context) Result { const file_path = self.file_path orelse return; const primary = self.get_primary(); return project_manager.completion(file_path, primary.cursor.row, primary.cursor.col); } pub fn add_diagnostic( self: *Self, file_path: []const u8, source: []const u8, code: []const u8, message: []const u8, severity: i32, sel: Selection, ) Result { if (!std.mem.eql(u8, file_path, self.file_path orelse return)) return; (try self.diagnostics.addOne()).* = .{ .source = try self.diagnostics.allocator.dupe(u8, source), .code = try self.diagnostics.allocator.dupe(u8, code), .message = try self.diagnostics.allocator.dupe(u8, message), .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.need_render(); } pub fn clear_diagnostics(self: *Self) void { self.diagnostics.clearRetainingCapacity(); self.diag_errors = 0; self.diag_warnings = 0; self.diag_info = 0; self.diag_hints = 0; self.send_editor_diagnostics() catch {}; self.need_render(); } pub fn select(self: *Self, ctx: Context) Result { var sel: Selection = .{}; if (!try ctx.args.match(.{ tp.extract(&sel.begin.row), tp.extract(&sel.begin.col), tp.extract(&sel.end.row), tp.extract(&sel.end.col) })) return error.InvalidArgument; self.get_primary().selection = sel; } fn get_formatter(self: *Self) ?[]const []const u8 { if (self.syntax) |syn| if (syn.file_type.formatter) |fmtr| if (fmtr.len > 0) return fmtr; return null; } pub fn format(self: *Self, ctx: Context) Result { if (ctx.args.buf.len > 0 and try ctx.args.match(.{ tp.string, tp.more })) { try self.filter_cmd(ctx.args); return; } if (self.get_formatter()) |fmtr| { var args = std.ArrayList(u8).init(self.allocator); const writer = args.writer(); try cbor.writeArrayHeader(writer, fmtr.len); for (fmtr) |arg| try cbor.writeValue(writer, arg); try self.filter_cmd(.{ .buf = args.items }); return; } return tp.exit("no formatter"); } pub fn filter(self: *Self, ctx: Context) Result { if (!try ctx.args.match(.{ tp.string, tp.more })) return error.InvalidArgument; try self.filter_cmd(ctx.args); } fn filter_cmd(self: *Self, cmd: tp.message) !void { if (self.filter) |_| return error.Stop; const root = self.buf_root() catch return; const buf_a_ = try self.buf_a(); const primary = self.get_primary(); var sel: Selection = if (primary.selection) |sel_| sel_ else val: { var sel_: Selection = .{}; try expand_selection_to_all(root, &sel_, self.plane); break :val sel_; }; const reversed = sel.begin.right_of(sel.end); sel.normalize(); self.filter = .{ .before_root = root, .work_root = root, .begin = sel.begin, .pos = .{ .cursor = sel.begin }, .old_primary = primary.*, .old_primary_reversed = reversed, .whole_file = if (primary.selection) |_| null else std.ArrayList(u8).init(self.allocator), }; errdefer self.filter_deinit(); const state = &self.filter.?; var buf: [1024]u8 = undefined; const json = try cmd.to_json(&buf); self.logger.print("filter: start {s}", .{json}); var sp = try tp.subprocess.init(self.allocator, cmd, "filter", .Pipe); defer { sp.close() catch {}; sp.deinit(); } var buffer = sp.bufferedWriter(); try self.write_range(state.before_root, sel, buffer.writer(), tp.exit_error, null, self.plane); try buffer.flush(); self.logger.print("filter: sent", .{}); state.work_root = try state.work_root.delete_range(sel, buf_a_, null, self.plane.metrics()); } fn filter_stdout(self: *Self, bytes: []const u8) !void { const state = if (self.filter) |*s| s else return error.Stop; errdefer self.filter_deinit(); const buf_a_ = try self.buf_a(); if (state.whole_file) |*buf| { try buf.appendSlice(bytes); } else { const cursor = &state.pos.cursor; cursor.row, cursor.col, state.work_root = try state.work_root.insert_chars(cursor.row, cursor.col, bytes, buf_a_, self.plane.metrics()); state.bytes += bytes.len; state.chunks += 1; } } fn filter_error(self: *Self, bytes: []const u8) !void { defer self.filter_deinit(); self.logger.print("filter: ERR: {s}", .{bytes}); if (self.need_save_after_filter) try self.save(); } fn filter_done(self: *Self) !void { const b = try self.buf_for_update(); const root = self.buf_root() catch return; const state = if (self.filter) |*s| s else return error.Stop; if (state.before_root != root) return error.Stop; defer self.filter_deinit(); const primary = self.get_primary(); self.cancel_all_selections(); self.cancel_all_matches(); if (state.whole_file) |buf| { state.work_root = try b.load_from_string(buf.items); state.bytes = buf.items.len; state.chunks = 1; primary.cursor = state.old_primary.cursor; } else { const sel = primary.enable_selection(); sel.begin = state.begin; sel.end = state.pos.cursor; if (state.old_primary_reversed) sel.reverse(); primary.cursor = sel.end; } try self.update_buf(state.work_root); primary.cursor.clamp_to_buffer(state.work_root, self.plane.metrics()); self.logger.print("filter: done (bytes:{d} chunks:{d})", .{ state.bytes, state.chunks }); self.reset_syntax(); self.clamp(); self.need_render(); if (self.need_save_after_filter) try self.save(); } fn filter_deinit(self: *Self) void { const state = if (self.filter) |*s| s else return; if (state.whole_file) |*buf| buf.deinit(); self.filter = null; } fn to_upper_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; const saved = cursel.*; const sel = if (cursel.selection) |*sel| sel else ret: { var sel = cursel.enable_selection(); move_cursor_word_begin(root, &sel.begin, self.plane) catch return error.Stop; move_cursor_word_end(root, &sel.end, self.plane) catch return error.Stop; break :ret sel; }; var sfa = std.heap.stackFallback(4096, self.allocator); const cut_text = copy_selection(root, sel.*, sfa.get(), self.plane) catch return error.Stop; defer allocator.free(cut_text); const cd = CaseData.init(allocator) catch return error.Stop; defer cd.deinit(); const ucased = cd.toUpperStr(allocator, cut_text) catch return error.Stop; defer allocator.free(ucased); root = try self.delete_selection(root, cursel, allocator); root = self.insert(root, cursel, ucased, allocator) catch return error.Stop; cursel.* = saved; return root; } pub fn to_upper(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.with_cursels_mut(b.root, to_upper_cursel, b.allocator); try self.update_buf(root); self.clamp(); } fn to_lower_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; const saved = cursel.*; const sel = if (cursel.selection) |*sel| sel else ret: { var sel = cursel.enable_selection(); move_cursor_word_begin(root, &sel.begin, self.plane) catch return error.Stop; move_cursor_word_end(root, &sel.end, self.plane) catch return error.Stop; break :ret sel; }; var sfa = std.heap.stackFallback(4096, self.allocator); const cut_text = copy_selection(root, sel.*, sfa.get(), self.plane) catch return error.Stop; defer allocator.free(cut_text); const cd = CaseData.init(allocator) catch return error.Stop; defer cd.deinit(); const ucased = cd.toLowerStr(allocator, cut_text) catch return error.Stop; defer allocator.free(ucased); root = try self.delete_selection(root, cursel, allocator); root = self.insert(root, cursel, ucased, allocator) catch return error.Stop; cursel.* = saved; return root; } pub fn to_lower(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.with_cursels_mut(b.root, to_lower_cursel, b.allocator); try self.update_buf(root); self.clamp(); } }; pub fn create(allocator: Allocator, parent: Widget) !Widget { return EditorWidget.create(allocator, parent); } pub const EditorWidget = struct { plane: Plane, parent: Plane, editor: Editor, commands: Commands = undefined, last_btn: c_int = -1, last_btn_time_ms: i64 = 0, last_btn_count: usize = 0, hover: bool = false, const Self = @This(); const Commands = command.Collection(Editor); fn create(allocator: Allocator, parent: Widget) !Widget { const container = try WidgetList.createH(allocator, parent, "editor.container", .dynamic); const self: *Self = try allocator.create(Self); try self.init(allocator, container.widget()); try self.commands.init(&self.editor); const editorWidget = Widget.to(self); try container.add(try editor_gutter.create(allocator, container.widget(), editorWidget, &self.editor)); try container.add(editorWidget); try container.add(try scrollbar_v.create(allocator, container.widget(), editorWidget, EventHandler.to_unowned(container))); return container.widget(); } fn init(self: *Self, allocator: Allocator, parent: Widget) !void { var n = try Plane.init(&(Widget.Box{}).opts("editor"), parent.plane.*); errdefer n.deinit(); self.* = .{ .parent = parent.plane.*, .plane = n, .editor = undefined, }; self.editor.init(allocator, n); errdefer self.editor.deinit(); try self.editor.push_cursor(); } pub fn deinit(self: *Self, allocator: Allocator) void { self.commands.deinit(); self.editor.deinit(); self.plane.deinit(); allocator.destroy(self); } pub fn update(self: *Self) void { self.editor.update(); } pub fn render(self: *Self, theme: *const Widget.Theme) bool { return self.editor.render(theme); } pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { return self.receive_safe(m) catch |e| return tp.exit_error(e, @errorReturnTrace()); } fn receive_safe(self: *Self, m: tp.message) !bool { var evtype: c_int = undefined; var btn: c_int = undefined; var x: c_int = undefined; var y: c_int = undefined; var xpx: c_int = undefined; var ypx: c_int = undefined; var pos: u32 = 0; var bytes: []u8 = ""; if (try m.match(.{ "B", tp.extract(&evtype), tp.extract(&btn), tp.any, tp.extract(&x), tp.extract(&y), tp.extract(&xpx), tp.extract(&ypx) })) { try self.mouse_click_event(evtype, btn, y, x, ypx, xpx); } else if (try m.match(.{ "D", tp.extract(&evtype), tp.extract(&btn), tp.any, tp.extract(&x), tp.extract(&y), tp.extract(&xpx), tp.extract(&ypx) })) { try self.mouse_drag_event(evtype, btn, y, x, ypx, xpx); } else if (try m.match(.{ "scroll_to", tp.extract(&pos) })) { self.editor.scroll_to(pos); } else if (try m.match(.{ "filter", "stdout", tp.extract(&bytes) })) { self.editor.filter_stdout(bytes) catch {}; } else if (try m.match(.{ "filter", "stderr", tp.extract(&bytes) })) { try self.editor.filter_error(bytes); } else if (try m.match(.{ "filter", "term", tp.more })) { try self.editor.filter_done(); } else if (try m.match(.{ "A", tp.more })) { self.editor.add_match(m) catch {}; } else if (try m.match(.{ "H", tp.extract(&self.hover) })) { if (self.editor.jump_mode) tui.current().rdr.request_mouse_cursor_pointer(self.hover) else tui.current().rdr.request_mouse_cursor_text(self.hover); } else if (try m.match(.{ "show_whitespace", tp.extract(&self.editor.show_whitespace) })) { _ = ""; } else { return false; } return true; } const Result = command.Result; fn mouse_click_event(self: *Self, evtype: c_int, btn: c_int, y: c_int, x: c_int, ypx: c_int, xpx: c_int) Result { if (evtype != event_type.PRESS) return; const ret = (switch (btn) { key.BUTTON1 => &mouse_click_button1, key.BUTTON2 => &mouse_click_button2, key.BUTTON3 => &mouse_click_button3, key.BUTTON4 => &mouse_click_button4, key.BUTTON5 => &mouse_click_button5, key.BUTTON8 => &mouse_click_button8, //back key.BUTTON9 => &mouse_click_button9, //forward else => return, })(self, y, x, ypx, xpx); self.last_btn = btn; self.last_btn_time_ms = time.milliTimestamp(); return ret; } fn mouse_drag_event(self: *Self, evtype: c_int, btn: c_int, y: c_int, x: c_int, ypx: c_int, xpx: c_int) Result { if (evtype != event_type.PRESS) return; return (switch (btn) { key.BUTTON1 => &mouse_drag_button1, key.BUTTON2 => &mouse_drag_button2, key.BUTTON3 => &mouse_drag_button3, else => return, })(self, y, x, ypx, xpx); } fn mouse_click_button1(self: *Self, y: c_int, x: c_int, _: c_int, _: c_int) Result { const y_, const x_ = self.editor.plane.abs_yx_to_rel(y, x); if (self.last_btn == key.BUTTON1) { const click_time_ms = time.milliTimestamp() - self.last_btn_time_ms; if (click_time_ms <= double_click_time_ms) { if (self.last_btn_count == 2) { self.last_btn_count = 3; try self.editor.primary_triple_click(y_, x_); return; } self.last_btn_count = 2; try self.editor.primary_double_click(y_, x_); return; } } self.last_btn_count = 1; try self.editor.primary_click(y_, x_); return; } fn mouse_drag_button1(self: *Self, y: c_int, x: c_int, _: c_int, _: c_int) Result { const y_, const x_ = self.editor.plane.abs_yx_to_rel(y, x); self.editor.primary_drag(y_, x_); } fn mouse_click_button2(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result {} fn mouse_drag_button2(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result {} fn mouse_click_button3(self: *Self, y: c_int, x: c_int, _: c_int, _: c_int) Result { const y_, const x_ = self.editor.plane.abs_yx_to_rel(y, x); try self.editor.secondary_click(y_, x_); } fn mouse_drag_button3(self: *Self, y: c_int, x: c_int, _: c_int, _: c_int) Result { const y_, const x_ = self.editor.plane.abs_yx_to_rel(y, x); try self.editor.secondary_drag(y_, x_); } fn mouse_click_button4(self: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result { try self.editor.scroll_up_pageup(.{}); } fn mouse_click_button5(self: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result { try self.editor.scroll_down_pagedown(.{}); } fn mouse_click_button8(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result { try command.executeName("jump_back", .{}); } fn mouse_click_button9(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result { try command.executeName("jump_forward", .{}); } pub fn handle_resize(self: *Self, pos: Widget.Box) void { self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return; self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return; self.editor.handle_resize(pos); } pub fn subscribe(self: *Self, h: EventHandler) !void { self.editor.handlers.add(h) catch {}; } pub fn unsubscribe(self: *Self, h: EventHandler) !void { self.editor.handlers.remove(h) catch {}; } }; pub const PosToWidthCache = struct { cache: std.ArrayList(usize), cached_line: usize = std.math.maxInt(usize), cached_root: ?Buffer.Root = null, const Self = @This(); pub fn init(allocator: Allocator) !Self { return .{ .cache = try std.ArrayList(usize).initCapacity(allocator, 2048), }; } pub fn deinit(self: *Self) void { self.cache.deinit(); } pub fn range_to_selection(self: *Self, range: syntax.Range, root: Buffer.Root, plane: Plane) ?Selection { const start = range.start_point; const end = range.end_point; if (root != self.cached_root or self.cached_line != start.row) { self.cache.clearRetainingCapacity(); self.cached_line = start.row; self.cached_root = root; root.get_line_width_map(self.cached_line, &self.cache, plane.metrics()) catch return null; } const start_col = if (start.column < self.cache.items.len) self.cache.items[start.column] else start.column; const end_col = if (end.row == start.row and end.column < self.cache.items.len) self.cache.items[end.column] else root.pos_to_width(end.row, end.column, plane.metrics()) catch end.column; return .{ .begin = .{ .row = start.row, .col = start_col }, .end = .{ .row = end.row, .col = end_col } }; } };