flow/src/tui/editor.zig
btipling 7230e7de86 split is a deprecated compile error and unused
using std.mem.split is a compileError as off this change

76fb2b685b
2024-06-22 00:45:26 -07:00

3854 lines
159 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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);
sel.end.move_right(root, plane) 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, a: std.mem.Allocator) void {
a.free(self.source);
a.free(self.code);
a.free(self.message);
}
const Severity = enum { Error, Warning, Information, Hint };
pub fn get_severity(self: Diagnostic) Severity {
return to_severity(self.severity);
}
pub fn to_severity(sev: i32) Severity {
return switch (sev) {
1 => .Error,
2 => .Warning,
3 => .Information,
4 => .Hint,
else => .Error,
};
}
};
pub const Editor = struct {
const SelectMode = enum {
char,
word,
line,
};
const Self = @This();
pub const Target = Self;
a: 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,
const StyleCache = std.AutoHashMap(u32, ?Widget.Theme.Token);
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.a.dupe(u8, clipboard) else null;
self.last_find_query = if (query.len > 0) try self.a.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, a: 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{
.a = a,
.plane = n,
.logger = logger,
.file_path = null,
.buffer = null,
.handlers = EventHandler.List.init(a),
.animation_lag = get_animation_max_lag(),
.animation_frame_rate = frame_rate,
.animation_last_time = time.microTimestamp(),
.cursels = CurSel.List.init(a),
.cursels_saved = CurSel.List.init(a),
.matches = Match.List.init(a),
.enable_terminal_cursor = tui.current().config.enable_terminal_cursor,
.show_whitespace = tui.current().config.show_whitespace,
.diagnostics = std.ArrayList(Diagnostic).init(a),
};
}
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 if (self.buffer) |p| p else 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.a 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 = if (self.buffer) |p| p else return false;
return b.is_dirty();
}
fn open(self: *Self, file_path: []const u8) !void {
var new_buf = try Buffer.create(self.a);
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.a);
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.a.dupe(u8, file_path);
if (self.buffer) |_| try self.close();
self.buffer = new_buf;
var content = std.ArrayList(u8).init(self.a);
defer content.deinit();
try new_buf.root.store(content.writer());
self.syntax = syntax: {
const lang_override = tp.env.get().str("language");
if (lang_override.len > 0)
break :syntax syntax.create_file_type(self.a, content.items, lang_override) catch null;
break :syntax syntax.create_guess_file_type(self.a, content.items, self.file_path) catch null;
};
if (self.syntax) |syn|
project_manager.did_open(file_path, syn.file_type, self.lsp_version, try content.toOwnedSlice()) catch {};
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 = if (self.buffer) |p| p else 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 = if (self.buffer) |p| p else return error.Stop;
if (!b.is_dirty()) return tp.exit("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;
}
pub fn push_cursor(self: *Self) !void {
const primary = if (self.cursels.getLastOrNull()) |c| c orelse CurSel{} else CurSel{};
(try self.cursels.addOne()).* = primary;
}
pub fn pop_cursor(self: *Self, _: command.Context) tp.result {
if (self.cursels.items.len > 1)
_ = self.cursels.popOrNull();
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, a: Allocator) ![]u8 {
var meta = std.ArrayList(u8).init(a);
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, a: Allocator) ![]u8 {
var meta = std.ArrayList(u8).init(a);
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 = if (self.buffer) |p| p else return error.Stop;
var sfa = std.heap.stackFallback(512, self.a);
const a = sfa.get();
const meta = try self.store_undo_meta(a);
defer a.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.a);
const a = sfa.get();
const redo_meta = try self.store_current_undo_meta(a);
defer a.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, _: Plane) 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) catch return 0;
return ctx.col;
}
fn write_range(
self: *const Self,
root: Buffer.Root,
sel: Selection,
writer: anytype,
map_error: fn (e: anyerror) @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, _: Plane) 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_) catch |e| return map_error(e);
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.a);
// self.logger.print("style_cache reset {s} -> {s}", .{ self.style_cache_theme, theme.name });
}
} else {
self.style_cache = StyleCache.init(self.a);
}
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, _: Plane) 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_.a.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_.a.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) 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));
tui.current().rdr.cursor_enable(y, x) 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();
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, _: Widget.Theme.Style) void {
var cell = self.plane.cell_init();
_ = self.plane.at_cursor_cell(&cell) catch return;
cell.set_style(.{ .fs = .undercurl });
_ = 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 = if (self.syntax) |syn| syn else 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 = row - ctx.self.view.row;
const x = begin_col - ctx.self.view.col;
const end_x = 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.a),
};
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|
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.a, 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 = if (self.cursels.getLast()) |p| p else 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) tp.result {
self.cancel_all_matches();
self.match_token += 1;
self.match_done_token = self.match_token;
}
fn init_matches_update(self: *Self) tp.result {
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: command.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: command.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, a: Allocator) error{Stop}!Buffer.Root {
return try move(root, &cursel.cursor, a);
}
fn with_cursors(self: *Self, root_: Buffer.Root, move: cursor_operator, a: Allocator) error{Stop}!Buffer.Root {
var root = root_;
for (self.cursels.items) |*cursel| {
cursel.selection = null;
root = try with_cursor(root, move, cursel, a);
}
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: command.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: command.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, a: Allocator) error{Stop}!Buffer.Root {
return op(root, cursel, a);
}
fn with_cursels(self: *Self, root_: Buffer.Root, move: cursel_operator, a: 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, a) 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, a: Allocator) error{Stop}!Buffer.Root {
return op(self, root, cursel, a);
}
fn with_cursels_mut(self: *Self, root_: Buffer.Root, move: cursel_operator_mut, a: 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, a) 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;
var start_byte: usize = 0;
_ = root.get_range(.{ .begin = .{}, .end = nudge.begin }, null, &start_byte, null, self.plane) 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 {
_ = size;
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;
};
self.reset_syntax();
}
fn delete_selection(self: *Self, root: Buffer.Root, cursel: *CurSel, a: Allocator) error{Stop}!Buffer.Root {
var sel: Selection = if (cursel.selection) |sel| sel else return error.Stop;
sel.normalize();
cursel.cursor = sel.begin;
cursel.selection = null;
var size: usize = 0;
const root_ = try root.delete_range(sel, a, &size, self.plane);
self.nudge_delete(sel, cursel, size);
return root_;
}
fn delete_to(self: *Self, move: cursor_operator_const, root_: Buffer.Root, a: 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, a) catch continue;
all_stop = false;
continue;
}
with_selection_const(root, move, cursel, self.plane) catch continue;
root = self.delete_selection(root, cursel, a) 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: command.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, a: Allocator) error{Stop}!Buffer.Root;
const cursel_operator = *const fn (root: Buffer.Root, cursel: *CurSel, a: Allocator) error{Stop}!Buffer.Root;
const cursel_operator_mut = *const fn (self: *Self, root: Buffer.Root, cursel: *CurSel, a: 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,
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);
}
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);
}
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) 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) 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) 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) 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) 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) 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) 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) 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);
}
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);
}
fn move_cursor_right(root: Buffer.Root, cursor: *Cursor, plane: Plane) error{Stop}!void {
try cursor.move_right(root, plane);
}
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);
}
fn move_cursor_up(root: Buffer.Root, cursor: *Cursor, plane: Plane) !void {
try cursor.move_up(root, plane);
}
fn move_cursor_down(root: Buffer.Root, cursor: *Cursor, plane: Plane) !void {
try cursor.move_down(root, plane);
}
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);
}
fn move_cursor_page_up(root: Buffer.Root, cursor: *Cursor, view: *const View, plane: Plane) !void {
cursor.move_page_up(root, view, plane);
}
fn move_cursor_page_down(root: Buffer.Root, cursor: *Cursor, view: *const View, plane: Plane) !void {
cursor.move_page_down(root, view, plane);
}
pub fn primary_click(self: *Self, y: c_int, x: c_int) tp.result {
if (self.fast_scroll)
self.push_cursor() catch |e| return tp.exit_error(e)
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) 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) tp.result {
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) catch return;
_ = self.select_word_at_cursor(primary) catch |e| return tp.exit_error(e);
self.clamp_mouse();
}
pub fn primary_triple_click(self: *Self, y: c_int, x: c_int) tp.result {
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) catch return;
self.select_line_at_cursor(primary) catch |e| return tp.exit_error(e);
self.clamp_mouse();
}
pub fn primary_drag(self: *Self, y: c_int, x: c_int) tp.result {
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) 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: command.Context) tp.result {
var y: i32 = 0;
var x: i32 = 0;
if (!try ctx.args.match(.{ tp.extract(&y), tp.extract(&x) }))
return tp.exit_error(error.InvalidArgument);
return self.primary_drag(y, x);
}
pub fn secondary_click(self: *Self, y: c_int, x: c_int) tp.result {
return self.primary_drag(y, x);
}
pub fn secondary_drag(self: *Self, y: c_int, x: c_int) tp.result {
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, _: command.Context) tp.result {
if (self.fast_scroll)
self.scroll_pageup()
else
self.scroll_up();
}
pub fn scroll_down_pagedown(self: *Self, _: command.Context) tp.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) tp.result {
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, _: command.Context) tp.result {
return self.scroll_view_offset(self.view.rows / 2);
}
pub fn scroll_view_top(self: *Self, _: command.Context) tp.result {
return self.scroll_view_offset(scroll_cursor_min_border_distance);
}
pub fn scroll_view_bottom(self: *Self, _: command.Context) tp.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.a.free(old);
self.clipboard = text;
tui.current().rdr.copy_to_system_clipboard(text);
}
fn copy_selection(root: Buffer.Root, sel: Selection, text_a: Allocator, plane: Plane) ![]const u8 {
var size: usize = 0;
_ = try root.get_range(sel, null, &size, null, plane);
const buf__ = try text_a.alloc(u8, size);
return (try root.get_range(sel, buf__, null, null, plane)).?;
}
pub fn get_selection(self: *const Self, sel: Selection, text_a: Allocator) ![]const u8 {
return copy_selection(try self.buf_root(), sel, text_a, self.plane);
}
fn copy_word_at_cursor(self: *Self, text_a: Allocator) ![]const u8 {
const root = self.buf_root() catch |e| return tp.exit_error(e);
const primary = self.get_primary();
const sel = if (primary.selection) |*sel| sel else self.select_word_at_cursor(primary) catch |e| return tp.exit_error(e);
return copy_selection(root, sel.*, text_a, self.plane) catch |e| return tp.exit_error(e);
}
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.a, 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, a: Allocator) !Buffer.Root {
var root_ = if (cursel.selection) |_| try self.delete_selection(root, cursel, a) else root;
const cursor = &cursel.cursor;
const begin = cursel.cursor;
cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, a, self.plane);
cursor.target = cursor.col;
self.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len);
return root_;
}
pub fn cut(self: *Self, _: command.Context) tp.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();
move_cursor_begin(root, &sel.begin, self.plane) catch |e| return tp.exit_error(e);
move_cursor_end(root, &sel.end, self.plane) catch |e| return tp.exit_error(e);
move_cursor_right(root, &sel.end, self.plane) catch |e| return tp.exit_error(e);
};
var first = true;
var text = std.ArrayList(u8).init(self.a);
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
const cut_text, root = self.cut_selection(root, cursel) catch |e| return tp.exit_error(e);
if (first) {
first = false;
} else {
text.appendSlice("\n") catch |e| return tp.exit_error(e);
}
text.appendSlice(cut_text) catch |e| return tp.exit_error(e);
};
self.update_buf(root) catch |e| return tp.exit_error(e);
self.set_clipboard(text.items);
self.clamp();
}
pub fn copy(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch return;
var first = true;
var text = std.ArrayList(u8).init(self.a);
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
if (cursel.selection) |sel| {
const copy_text = copy_selection(root, sel, self.a, self.plane) catch |e| return tp.exit_error(e);
if (first) {
first = false;
} else {
text.appendSlice("\n") catch |e| return tp.exit_error(e);
}
text.appendSlice(copy_text) catch |e| return tp.exit_error(e);
}
};
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: command.Context) tp.result {
var text: []const u8 = undefined;
if (!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 = self.buf_for_update() catch |e| return tp.exit_error(e);
var root = b.root;
if (self.cursels.items.len == 1) {
const primary = self.get_primary();
root = self.insert(root, primary, text, b.a) catch |e| return tp.exit_error(e);
} 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 = self.insert(root, cursel, text[pos..next], b.a) catch |e| return tp.exit_error(e);
pos = next + 1;
} else {
root = self.insert(root, cursel, text[pos..], b.a) catch |e| return tp.exit_error(e);
pos = 0;
}
};
} else {
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
root = self.insert(root, cursel, text, b.a) catch |e| return tp.exit_error(e);
};
}
}
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
self.need_render();
}
pub fn system_paste(_: *Self, _: command.Context) tp.result {
tui.current().rdr.request_system_clipboard();
}
pub fn delete_forward(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.delete_to(move_cursor_right, b.root, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn delete_backward(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.delete_to(move_cursor_left, b.root, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn delete_word_left(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.delete_to(move_cursor_word_left_space, b.root, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn delete_word_right(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.delete_to(move_cursor_word_right_space, b.root, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn delete_to_begin(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.delete_to(move_cursor_begin, b.root, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn delete_to_end(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.delete_to(move_cursor_end, b.root, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn join_next_line(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
self.with_cursors_const(b.root, move_cursor_end) catch |e| return tp.exit_error(e);
const root = self.delete_to(move_cursor_right, b.root, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn move_left(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const(root, move_cursor_left) catch {};
self.clamp();
}
pub fn move_right(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
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, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const(root, move_cursor_left_vim) catch {};
self.clamp();
}
pub fn move_right_vim(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
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) 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) 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, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const(root, move_cursor_word_left) catch {};
self.clamp();
}
pub fn move_word_right(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const(root, move_cursor_word_right) catch {};
self.clamp();
}
pub fn move_word_right_vim(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
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: command.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) 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: command.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) 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: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const_arg(root, move_cursor_to_char_left, ctx) catch {};
self.clamp();
}
pub fn move_to_char_right(self: *Self, ctx: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const_arg(root, move_cursor_to_char_right, ctx) catch {};
self.clamp();
}
pub fn move_up(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const(root, move_cursor_up) catch {};
self.clamp();
}
pub fn add_cursor_up(self: *Self, _: command.Context) tp.result {
self.push_cursor() catch |e| return tp.exit_error(e);
const primary = self.get_primary();
const root = self.buf_root() catch |e| return tp.exit_error(e);
move_cursor_up(root, &primary.cursor, self.plane) catch {};
self.clamp();
}
pub fn move_down(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const(root, move_cursor_down) catch {};
self.clamp();
}
pub fn add_cursor_down(self: *Self, _: command.Context) tp.result {
self.push_cursor() catch |e| return tp.exit_error(e);
const primary = self.get_primary();
const root = self.buf_root() catch |e| return tp.exit_error(e);
move_cursor_down(root, &primary.cursor, self.plane) catch {};
self.clamp();
}
pub fn add_cursor_next_match(self: *Self, _: command.Context) tp.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 {};
self.with_selections_const(root, move_cursor_word_end) catch |e| return tp.exit_error(e);
} else if (self.get_next_match(self.get_primary().cursor)) |match| {
self.push_cursor() catch |e| return tp.exit_error(e);
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) catch return;
}
self.clamp();
try self.send_editor_jump_destination();
}
pub fn add_cursor_all_matches(self: *Self, _: command.Context) tp.result {
if (self.matches.items.len == 0) return;
try self.send_editor_jump_source();
while (self.get_next_match(self.get_primary().cursor)) |match| {
self.push_cursor() catch |e| return tp.exit_error(e);
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) 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);
}
}
pub fn add_cursors_to_line_ends(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
const cursels = self.cursels.toOwnedSlice() catch |e| return tp.exit_error(e);
defer self.cursels.allocator.free(cursels);
for (cursels) |*cursel_| if (cursel_.*) |*cursel|
self.add_cursors_to_cursel_line_ends(root, cursel) catch |e| return tp.exit_error(e);
self.collapse_cursors();
self.clamp();
}
fn pull_cursel_up(self: *Self, root_: Buffer.Root, cursel: *CurSel, a: 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.a);
const cut_text = copy_selection(root, sel.*, sfa.get(), self.plane) catch return error.Stop;
defer a.free(cut_text);
root = try self.delete_selection(root, cursel, a);
try cursel.cursor.move_up(root, self.plane);
root = self.insert(root, cursel, cut_text, a) catch return error.Stop;
cursel.* = saved;
try cursel.cursor.move_up(root, self.plane);
if (cursel.selection) |*sel_| {
try sel_.begin.move_up(root, self.plane);
try sel_.end.move_up(root, self.plane);
}
return root;
}
pub fn pull_up(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.with_cursels_mut(b.root, pull_cursel_up, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
fn pull_cursel_down(self: *Self, root_: Buffer.Root, cursel: *CurSel, a: 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.a);
const cut_text = copy_selection(root, sel.*, sfa.get(), self.plane) catch return error.Stop;
defer a.free(cut_text);
root = try self.delete_selection(root, cursel, a);
try cursel.cursor.move_down(root, self.plane);
root = self.insert(root, cursel, cut_text, a) catch return error.Stop;
cursel.* = saved;
try cursel.cursor.move_down(root, self.plane);
if (cursel.selection) |*sel_| {
try sel_.begin.move_down(root, self.plane);
try sel_.end.move_down(root, self.plane);
}
return root;
}
pub fn pull_down(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.with_cursels_mut(b.root, pull_cursel_down, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
fn dupe_cursel_up(self: *Self, root_: Buffer.Root, cursel: *CurSel, a: 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);
cursel.selection = null;
var sfa = std.heap.stackFallback(4096, self.a);
const text = copy_selection(root, sel, sfa.get(), self.plane) catch return error.Stop;
defer a.free(text);
cursel.cursor = sel.begin;
root = self.insert(root, cursel, text, a) catch return error.Stop;
cursel.selection = .{ .begin = sel.begin, .end = sel.end };
cursel.cursor = sel.begin;
return root;
}
pub fn dupe_up(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.with_cursels_mut(b.root, dupe_cursel_up, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
fn dupe_cursel_down(self: *Self, root_: Buffer.Root, cursel: *CurSel, a: 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);
cursel.selection = null;
var sfa = std.heap.stackFallback(4096, self.a);
const text = copy_selection(root, sel, sfa.get(), self.plane) catch return error.Stop;
defer a.free(text);
cursel.cursor = sel.end;
root = self.insert(root, cursel, text, a) catch return error.Stop;
cursel.selection = .{ .begin = sel.end, .end = cursel.cursor };
return root;
}
pub fn dupe_down(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.with_cursels_mut(b.root, dupe_cursel_down, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
fn toggle_cursel_prefix(self: *Self, root_: Buffer.Root, cursel: *CurSel, a: 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.a);
const alloc = sfa.get();
const text = copy_selection(root, sel.*, alloc, self.plane) catch return error.Stop;
defer a.free(text);
root = try self.delete_selection(root, cursel, a);
const new_text = text_manip.toggle_prefix_in_text(self.prefix, text, alloc) catch return error.Stop;
root = self.insert(root, cursel, new_text, a) catch return error.Stop;
cursel.* = saved;
return root;
}
pub fn toggle_prefix(self: *Self, ctx: command.Context) tp.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 = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.with_cursels_mut(b.root, toggle_cursel_prefix, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
}
pub fn toggle_comment(self: *Self, _: command.Context) tp.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, a: 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], a) catch return error.Stop;
}
fn indent_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, a: 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, a);
if (sel.end.col > 0)
root = try self.indent_cursor(root, sel.end, a);
return root;
} else return try self.indent_cursor(root_, cursel.cursor, a);
}
pub fn indent(self: *Self, _: command.Context) tp.result {
defer self.reset_syntax();
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.with_cursels_mut(b.root, indent_cursel, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
}
fn unindent_cursor(self: *Self, root: Buffer.Root, cursel: *CurSel, cursor: Cursor, a: Allocator) error{Stop}!Buffer.Root {
const saved = cursel.*;
var newroot = root;
defer {
cursel.* = saved;
cursel.cursor.clamp_to_buffer(newroot, self.plane);
}
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);
if (cursel.cursor.col < cols) try cursel.cursor.move_to(root, cursel.cursor.row, cols, self.plane);
newroot = try self.delete_selection(root, cursel, a);
return newroot;
}
fn unindent_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, a: 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, a);
if (sel.end.col > 0)
root = try self.unindent_cursor(root, cursel, sel.end, a);
return root;
} else return self.unindent_cursor(root_, cursel, cursel.cursor, a);
}
pub fn unindent(self: *Self, _: command.Context) tp.result {
defer self.reset_syntax();
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.with_cursels_mut(b.root, unindent_cursel, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
}
pub fn move_scroll_up(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const(root, move_cursor_up) catch {};
self.view.move_up() catch {};
self.clamp();
}
pub fn move_scroll_down(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const(root, move_cursor_down) catch {};
self.view.move_down(root) catch {};
self.clamp();
}
pub fn move_scroll_left(self: *Self, _: command.Context) tp.result {
self.view.move_left() catch {};
}
pub fn move_scroll_right(self: *Self, _: command.Context) tp.result {
self.view.move_right() catch {};
}
pub fn move_scroll_page_up(self: *Self, _: command.Context) tp.result {
if (self.screen_cursor(&self.get_primary().cursor)) |cursor| {
const root = self.buf_root() catch |e| return tp.exit_error(e);
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, _: command.Context) tp.result {
if (self.screen_cursor(&self.get_primary().cursor)) |cursor| {
const root = self.buf_root() catch |e| return tp.exit_error(e);
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, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const(root, smart_move_cursor_begin) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn move_begin(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const(root, move_cursor_begin) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn move_end(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_const(root, move_cursor_end) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn move_page_up(self: *Self, _: command.Context) tp.result {
try self.send_editor_jump_source();
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_and_view_const(root, move_cursor_page_up, &self.view) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn move_page_down(self: *Self, _: command.Context) tp.result {
try self.send_editor_jump_source();
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursors_and_view_const(root, move_cursor_page_down, &self.view) catch |e| return tp.exit_error(e);
self.clamp();
try self.send_editor_jump_destination();
}
pub fn move_buffer_begin(self: *Self, _: command.Context) tp.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, _: command.Context) tp.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);
self.clamp();
try self.send_editor_jump_destination();
}
pub fn cancel(self: *Self, _: command.Context) tp.result {
self.cancel_all_selections();
self.cancel_all_matches();
}
pub fn select_up(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_up) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn select_down(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_down) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn select_scroll_up(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_up) catch |e| return tp.exit_error(e);
self.view.move_up() catch {};
self.clamp();
}
pub fn select_scroll_down(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_down) catch |e| return tp.exit_error(e);
self.view.move_down(root) catch {};
self.clamp();
}
pub fn select_left(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_left) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn select_right(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_right) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn select_word_left(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_word_left) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn select_word_right(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_word_right) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn select_word_begin(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_word_begin) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn select_word_end(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_word_end) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn select_to_char_left(self: *Self, ctx: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const_arg(root, move_cursor_to_char_left, ctx) catch {};
self.clamp();
}
pub fn select_to_char_right(self: *Self, ctx: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const_arg(root, move_cursor_to_char_right, ctx) catch {};
self.clamp();
}
pub fn select_begin(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_begin) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn smart_select_begin(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, smart_move_cursor_begin) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn select_end(self: *Self, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_end) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn select_buffer_begin(self: *Self, _: command.Context) tp.result {
try self.send_editor_jump_source();
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_buffer_begin) catch |e| return tp.exit_error(e);
self.clamp();
try self.send_editor_jump_destination();
}
pub fn select_buffer_end(self: *Self, _: command.Context) tp.result {
try self.send_editor_jump_source();
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_const(root, move_cursor_buffer_end) catch |e| return tp.exit_error(e);
self.clamp();
try self.send_editor_jump_destination();
}
pub fn select_page_up(self: *Self, _: command.Context) tp.result {
try self.send_editor_jump_source();
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_and_view_const(root, move_cursor_page_up, &self.view) catch |e| return tp.exit_error(e);
self.clamp();
try self.send_editor_jump_destination();
}
pub fn select_page_down(self: *Self, _: command.Context) tp.result {
try self.send_editor_jump_source();
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_selections_and_view_const(root, move_cursor_page_down, &self.view) catch |e| return tp.exit_error(e);
self.clamp();
try self.send_editor_jump_destination();
}
pub fn select_all(self: *Self, _: command.Context) tp.result {
try self.send_editor_jump_source();
self.cancel_all_selections();
const primary = self.get_primary();
const sel = primary.enable_selection();
const root = self.buf_root() catch |e| return tp.exit_error(e);
expand_selection_to_all(root, sel, self.plane) catch |e| tp.exit_error(e);
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 = self.buf_root() catch |e| return tp.exit_error(e);
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, _: command.Context) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.with_cursels_const(root, selection_reverse) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn insert_chars(self: *Self, ctx: command.Context) tp.result {
var chars: []const u8 = undefined;
if (!try ctx.args.match(.{tp.extract(&chars)}))
return tp.exit_error(error.InvalidArgument);
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
var root = b.root;
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
root = self.insert(root, cursel, chars, b.a) catch |e| return tp.exit_error(e);
};
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn insert_line(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
var root = b.root;
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
root = self.insert(root, cursel, "\n", b.a) catch |e| return tp.exit_error(e);
};
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn smart_insert_line(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
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.a);
const a = sfa.get();
var stream = std.ArrayList(u8).init(a);
defer stream.deinit();
var writer = stream.writer();
_ = writer.write("\n") catch |e| return tp.exit_error(e);
while (leading_ws > 0) : (leading_ws -= 1)
_ = writer.write(" ") catch |e| return tp.exit_error(e);
root = self.insert(root, cursel, stream.items, b.a) catch |e| return tp.exit_error(e);
};
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn insert_line_before(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
var root = b.root;
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
move_cursor_begin(root, &cursel.cursor, self.plane) catch |e| return tp.exit_error(e);
root = self.insert(root, cursel, "\n", b.a) catch |e| return tp.exit_error(e);
move_cursor_left(root, &cursel.cursor, self.plane) catch |e| return tp.exit_error(e);
};
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn smart_insert_line_before(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
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);
move_cursor_begin(root, &cursel.cursor, self.plane) catch |e| return tp.exit_error(e);
root = self.insert(root, cursel, "\n", b.a) catch |e| return tp.exit_error(e);
move_cursor_left(root, &cursel.cursor, self.plane) catch |e| return tp.exit_error(e);
var sfa = std.heap.stackFallback(512, self.a);
const a = sfa.get();
var stream = std.ArrayList(u8).init(a);
defer stream.deinit();
var writer = stream.writer();
while (leading_ws > 0) : (leading_ws -= 1)
_ = writer.write(" ") catch |e| return tp.exit_error(e);
if (stream.items.len > 0)
root = self.insert(root, cursel, stream.items, b.a) catch |e| return tp.exit_error(e);
};
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn insert_line_after(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
var root = b.root;
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
move_cursor_end(root, &cursel.cursor, self.plane) catch |e| return tp.exit_error(e);
root = self.insert(root, cursel, "\n", b.a) catch |e| return tp.exit_error(e);
};
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn smart_insert_line_after(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
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);
move_cursor_end(root, &cursel.cursor, self.plane) catch |e| return tp.exit_error(e);
var sfa = std.heap.stackFallback(512, self.a);
const a = sfa.get();
var stream = std.ArrayList(u8).init(a);
defer stream.deinit();
var writer = stream.writer();
_ = writer.write("\n") catch |e| return tp.exit_error(e);
while (leading_ws > 0) : (leading_ws -= 1)
_ = writer.write(" ") catch |e| return tp.exit_error(e);
if (stream.items.len > 0)
root = self.insert(root, cursel, stream.items, b.a) catch |e| return tp.exit_error(e);
};
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn enable_fast_scroll(self: *Self, _: command.Context) tp.result {
self.fast_scroll = true;
}
pub fn disable_fast_scroll(self: *Self, _: command.Context) tp.result {
self.fast_scroll = false;
}
pub fn enable_jump_mode(self: *Self, _: command.Context) tp.result {
self.jump_mode = true;
tui.current().rdr.request_mouse_cursor_pointer(true);
}
pub fn disable_jump_mode(self: *Self, _: command.Context) tp.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 (self.syntax_token == token)
return;
var content = std.ArrayList(u8).init(self.a);
defer content.deinit();
try root.store(content.writer());
if (self.syntax) |syn| {
try if (self.syntax_refresh_full)
syn.refresh_full(content.items)
else
syn.refresh(content.items);
self.syntax_refresh_full = false;
self.syntax_token = token;
} else {
self.syntax = syntax.create_guess_file_type(self.a, 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, _: command.Context) tp.result {
const root = self.buf_root() catch return;
const primary = self.get_primary();
var tree = std.ArrayList(u8).init(self.a);
defer tree.deinit();
root.debug_render_chunks(primary.cursor.row, &tree, self.plane) 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, _: command.Context) tp.result {
const root = self.buf_root() catch return;
const primary = self.get_primary();
var tree = std.ArrayList(u8).init(self.a);
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, _: command.Context) tp.result {
self.restore_undo() catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn redo(self: *Self, _: command.Context) tp.result {
self.restore_redo() catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn open_file(self: *Self, ctx: command.Context) tp.result {
var file_path: []const u8 = undefined;
if (ctx.args.match(.{tp.extract(&file_path)}) catch false) {
self.open(file_path) catch |e| return tp.exit_error(e);
self.clamp();
} else return tp.exit_error(error.InvalidArgument);
}
pub fn open_scratch_buffer(self: *Self, ctx: command.Context) tp.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) {
self.open_scratch(file_path, content) catch |e| return tp.exit_error(e);
self.clamp();
} else return tp.exit_error(error.InvalidArgument);
}
pub fn save_file(self: *Self, _: command.Context) tp.result {
self.save() catch |e| return tp.exit_error(e);
}
pub fn close_file(self: *Self, _: command.Context) tp.result {
self.cancel_all_selections();
self.close() catch |e| return tp.exit_error(e);
}
pub fn close_file_without_saving(self: *Self, _: command.Context) tp.result {
self.cancel_all_selections();
self.close_dirty() catch |e| return tp.exit_error(e);
}
pub fn find(self: *Self, ctx: command.Context) tp.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 tp.exit_error(error.InvalidArgument);
}
fn find_in(self: *Self, query: []const u8, comptime find_f: ripgrep.FindF, write_buffer: bool) tp.result {
const root = self.buf_root() catch |e| return tp.exit_error(e);
self.cancel_all_matches();
if (std.mem.indexOfScalar(u8, query, '\n')) |_| return;
self.logger.print("find:{s}", .{std.fmt.fmtSliceEscapeLower(query)});
var rg = find_f(self.a, query, "A") catch |e| return tp.exit_error(e);
defer rg.deinit();
if (write_buffer) {
var rg_buffer = rg.bufferedWriter();
root.store(rg_buffer.writer()) catch |e| return tp.exit_error(e);
rg_buffer.flush() catch |e| return tp.exit_error(e);
}
}
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.a);
break :ret &self.find_history.?;
};
for (history.items, 0..) |entry, i|
if (std.mem.eql(u8, entry, query))
self.a.free(history.orderedRemove(i));
const new = self.a.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.a.free(last);
self.last_find_query = self.a.dupe(u8, query) catch return;
}
} else self.last_find_query = self.a.dupe(u8, query) catch return;
}
pub fn find_in_buffer(self: *Self, query: []const u8) tp.result {
self.set_last_find_query(query);
return self.find_in_buffer_async(query);
}
fn find_in_buffer_sync(self: *Self, query: []const u8) tp.result {
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 = self.buf_root() catch |e| return tp.exit_error(e);
defer tp.self_pid().send(.{ "A", "done", self.match_token }) catch {};
var ctx: Ctx = .{ .self = self };
self.init_matches_update() catch {};
root.find_all_ranges(query, &ctx, Ctx.cb, self.a) catch |e| return tp.exit_error(e);
}
fn find_in_buffer_async(self: *Self, query: []const u8) tp.result {
const finder = struct {
a: 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) tp.result {
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.a.free(fdr.query);
var ctx: Ctx = .{ .fdr = fdr };
fdr.root.find_all_ranges(fdr.query, &ctx, Ctx.cb, fdr.a) catch |e| return tp.exit_error(e);
fdr.send_batch() catch |e| return tp.exit_error(e);
}
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() catch {};
const fdr = self.a.create(finder) catch |e| return tp.exit_error(e);
fdr.* = .{
.a = self.a,
.query = self.a.dupe(u8, query) catch |e| return tp.exit_error(e),
.parent = tp.self_pid(),
.root = self.buf_root() catch |e| return tp.exit_error(e),
.token = self.match_token,
.matches = Match.List.init(self.a),
};
const pid = tp.spawn_link(self.a, fdr, finder.start, "editor.find") catch |e| return tp.exit_error(e);
pid.deinit();
}
pub fn find_in_buffer_ext(self: *Self, query: []const u8) tp.result {
return self.find_in(query, ripgrep.find_in_stdin, true);
}
pub fn find_in_files(self: *Self, query: []const u8) tp.result {
return self.find_in(query, ripgrep.find_in_files, false);
}
pub fn add_match(self: *Self, m: tp.message) tp.result {
var path: []const u8 = undefined;
var begin_line: usize = undefined;
var begin_pos: usize = undefined;
var end_line: usize = undefined;
var end_pos: usize = undefined;
var lines: []const u8 = 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) })) {
self.add_match_batch(batch_cbor) catch |e| return tp.exit_error(e);
} 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);
} else if (try m.match(.{ tp.any, tp.extract(&path), tp.extract(&begin_line), tp.extract(&begin_pos), tp.extract(&end_line), tp.extract(&end_pos), tp.extract(&lines) })) {
self.logger.print("match: {s}:{d}:{d}:{d}:{d} {s}", .{ path, begin_line, begin_pos + 1, end_line, end_pos + 1, std.fmt.fmtSliceEscapeLower(lines) });
}
}
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) catch return;
const end_pos = root.pos_to_width(end_line, end_pos_, self.plane) 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 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);
return self.scan_prev_match(cursor_);
}
pub fn move_cursor_next_match(self: *Self, _: command.Context) tp.result {
const primary = self.get_primary();
if (self.get_next_match(primary.cursor)) |match| {
const root = self.buf_root() catch return;
primary.selection = match.to_selection();
primary.cursor.move_to(root, match.end.row, match.end.col, self.plane) catch return;
self.clamp();
}
}
pub fn goto_next_match(self: *Self, ctx: command.Context) tp.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, _: command.Context) tp.result {
const primary = self.get_primary();
if (self.get_prev_match(primary.cursor)) |match| {
const root = self.buf_root() catch return;
primary.selection = match.to_selection();
primary.selection.?.reverse();
primary.cursor.move_to(root, match.begin.row, match.begin.col, self.plane) catch return;
self.clamp();
}
}
pub fn goto_prev_match(self: *Self, ctx: command.Context) tp.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, _: command.Context) tp.result {
if (self.diagnostics.items.len == 0) return;
self.sort_diagnostics();
const primary = self.get_primary();
for (self.diagnostics.items) |*diag| {
if ((diag.sel.begin.row == primary.cursor.row and diag.sel.begin.col > primary.cursor.col) or diag.sel.begin.row > primary.cursor.row)
return self.goto_diagnostic(diag);
}
return self.goto_diagnostic(&self.diagnostics.items[0]);
}
pub fn goto_prev_diagnostic(self: *Self, _: command.Context) tp.result {
if (self.diagnostics.items.len == 0) return;
self.sort_diagnostics();
const primary = self.get_primary();
var i = self.diagnostics.items.len - 1;
while (true) : (i -= 1) {
const diag = &self.diagnostics.items[i];
if ((diag.sel.begin.row == primary.cursor.row and diag.sel.begin.col < primary.cursor.col) or diag.sel.begin.row < primary.cursor.row)
return self.goto_diagnostic(diag);
if (i == 0) return self.goto_diagnostic(&self.diagnostics.items[self.diagnostics.items.len - 1]);
}
}
fn goto_diagnostic(self: *Self, diag: *const Diagnostic) tp.result {
const root = self.buf_root() catch return;
const primary = self.get_primary();
try self.send_editor_jump_source();
self.cancel_all_selections();
primary.cursor.move_to(root, diag.sel.begin.row, diag.sel.begin.col, self.plane) catch |e| return tp.exit_error(e);
self.clamp();
try self.send_editor_jump_destination();
}
fn sort_diagnostics(self: *Self) void {
const less_fn = struct {
fn less_fn(_: void, lhs: Diagnostic, rhs: Diagnostic) bool {
return if (lhs.sel.begin.row == rhs.sel.begin.row)
lhs.sel.begin.col < rhs.sel.begin.col
else
lhs.sel.begin.row < rhs.sel.begin.row;
}
}.less_fn;
std.mem.sort(Diagnostic, self.diagnostics.items, {}, less_fn);
}
pub fn goto_line(self: *Self, ctx: command.Context) tp.result {
try self.send_editor_jump_source();
var line: usize = 0;
if (!try ctx.args.match(.{tp.extract(&line)}))
return tp.exit_error(error.InvalidArgument);
const root = self.buf_root() catch return;
self.cancel_all_selections();
const primary = self.get_primary();
primary.cursor.move_to(root, @intCast(if (line < 1) 0 else line - 1), primary.cursor.col, self.plane) catch |e| return tp.exit_error(e);
self.clamp();
try self.send_editor_jump_destination();
}
pub fn goto_column(self: *Self, ctx: command.Context) tp.result {
var column: usize = 0;
if (!try ctx.args.match(.{tp.extract(&column)}))
return tp.exit_error(error.InvalidArgument);
const root = self.buf_root() catch return;
const primary = self.get_primary();
primary.cursor.move_to(root, primary.cursor.row, @intCast(if (column < 1) 0 else column - 1), self.plane) catch |e| return tp.exit_error(e);
self.clamp();
}
pub fn goto(self: *Self, ctx: command.Context) tp.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 tp.exit_error(error.InvalidArgument);
self.cancel_all_selections();
const root = self.buf_root() catch return;
const primary = self.get_primary();
primary.cursor.move_to(
root,
@intCast(if (line < 1) 0 else line - 1),
@intCast(if (column < 1) 0 else column - 1),
self.plane,
) catch |e| return tp.exit_error(e);
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, _: command.Context) tp.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 clear_diagnostics(self: *Self, ctx: command.Context) tp.result {
var file_path: []const u8 = undefined;
if (!try ctx.args.match(.{tp.extract(&file_path)})) return tp.exit_error(error.InvalidArgument);
file_path = project_manager.normalize_file_path(file_path);
if (!std.mem.eql(u8, file_path, self.file_path orelse return)) return;
for (self.diagnostics.items) |*d| d.deinit(self.diagnostics.allocator);
self.diagnostics.clearRetainingCapacity();
self.diag_errors = 0;
self.diag_warnings = 0;
self.diag_info = 0;
self.diag_hints = 0;
self.send_editor_diagnostics() catch {};
self.need_render();
}
pub fn add_diagnostic(self: *Self, ctx: command.Context) tp.result {
var file_path: []const u8 = undefined;
var source: []const u8 = undefined;
var code: []const u8 = undefined;
var message: []const u8 = undefined;
var severity: i32 = 0;
var sel: Selection = .{};
if (!try ctx.args.match(.{
tp.extract(&file_path),
tp.extract(&source),
tp.extract(&code),
tp.extract(&message),
tp.extract(&severity),
tp.extract(&sel.begin.row),
tp.extract(&sel.begin.col),
tp.extract(&sel.end.row),
tp.extract(&sel.end.col),
})) return tp.exit_error(error.InvalidArgument);
file_path = project_manager.normalize_file_path(file_path);
if (!std.mem.eql(u8, file_path, self.file_path orelse return)) return;
(self.diagnostics.addOne() catch |e| return tp.exit_error(e)).* = .{
.source = self.diagnostics.allocator.dupe(u8, source) catch |e| return tp.exit_error(e),
.code = self.diagnostics.allocator.dupe(u8, code) catch |e| return tp.exit_error(e),
.message = self.diagnostics.allocator.dupe(u8, message) catch |e| return tp.exit_error(e),
.severity = severity,
.sel = sel,
};
switch (Diagnostic.to_severity(severity)) {
.Error => self.diag_errors += 1,
.Warning => self.diag_warnings += 1,
.Information => self.diag_info += 1,
.Hint => self.diag_hints += 1,
}
self.send_editor_diagnostics() catch {};
// self.logger.print("diag: {d} {d} {d}:{d} {s}", .{ self.diagnostics.items.len, severity, sel.begin.row, sel.begin.col, message });
self.need_render();
}
pub fn select(self: *Self, ctx: command.Context) tp.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 tp.exit_error(error.InvalidArgument);
self.get_primary().selection = sel;
}
pub fn format(self: *Self, ctx: command.Context) tp.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.syntax) |syn| if (syn.file_type.formatter) |fmtr| if (fmtr.len > 0) {
var args = std.ArrayList(u8).init(self.a);
const writer = args.writer();
cbor.writeArrayHeader(writer, fmtr.len) catch |e| return tp.exit_error(e);
for (fmtr) |arg| cbor.writeValue(writer, arg) catch |e| return tp.exit_error(e);
try self.filter_cmd(.{ .buf = args.items });
return;
};
return tp.exit("no formatter");
}
pub fn filter(self: *Self, ctx: command.Context) tp.result {
if (!try ctx.args.match(.{ tp.string, tp.more }))
return tp.exit_error(error.InvalidArgument);
try self.filter_cmd(ctx.args);
}
fn filter_cmd(self: *Self, cmd: tp.message) tp.result {
if (self.filter) |_| return tp.exit_error(error.Stop);
const root = self.buf_root() catch return;
const buf_a_ = self.buf_a() catch |e| return tp.exit_error(e);
const primary = self.get_primary();
var sel: Selection = if (primary.selection) |sel_| sel_ else val: {
var sel_: Selection = .{};
expand_selection_to_all(root, &sel_, self.plane) catch |e| tp.exit_error(e);
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.a),
};
errdefer self.filter_deinit();
const state = &self.filter.?;
var buf: [1024]u8 = undefined;
const json = cmd.to_json(&buf) catch |e| return tp.exit_error(e);
self.logger.print("filter: start {s}", .{json});
var sp = tp.subprocess.init(self.a, cmd, "filter", .Pipe) catch |e| return tp.exit_error(e);
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 = state.work_root.delete_range(sel, buf_a_, null, self.plane) catch |e| return tp.exit_error(e);
}
fn filter_stdout(self: *Self, bytes: []const u8) tp.result {
const state = if (self.filter) |*s| s else return tp.exit_error(error.Stop);
errdefer self.filter_deinit();
const buf_a_ = self.buf_a() catch |e| return tp.exit_error(e);
if (state.whole_file) |*buf| {
buf.appendSlice(bytes) catch |e| return tp.exit_error(e);
} else {
const cursor = &state.pos.cursor;
cursor.row, cursor.col, state.work_root = state.work_root.insert_chars(cursor.row, cursor.col, bytes, buf_a_, self.plane) catch |e| return tp.exit_error(e);
state.bytes += bytes.len;
state.chunks += 1;
}
}
fn filter_error(self: *Self, bytes: []const u8) tp.result {
defer self.filter_deinit();
self.logger.print("filter: ERR: {s}", .{bytes});
}
fn filter_done(self: *Self) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.buf_root() catch return;
const state = if (self.filter) |*s| s else return tp.exit_error(error.Stop);
if (state.before_root != root) return tp.exit_error(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 = b.load_from_string(buf.items) catch |e| return tp.exit_error(e);
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;
}
self.update_buf(state.work_root) catch |e| return tp.exit_error(e);
primary.cursor.clamp_to_buffer(state.work_root, self.plane);
self.logger.print("filter: done (bytes:{d} chunks:{d})", .{ state.bytes, state.chunks });
self.reset_syntax();
self.clamp();
self.need_render();
}
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, a: 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.a);
const cut_text = copy_selection(root, sel.*, sfa.get(), self.plane) catch return error.Stop;
defer a.free(cut_text);
const cd = CaseData.init(a) catch return error.Stop;
defer cd.deinit();
const ucased = cd.toUpperStr(a, cut_text) catch return error.Stop;
defer a.free(ucased);
root = try self.delete_selection(root, cursel, a);
root = self.insert(root, cursel, ucased, a) catch return error.Stop;
cursel.* = saved;
return root;
}
pub fn to_upper(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.with_cursels_mut(b.root, to_upper_cursel, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
fn to_lower_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, a: 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.a);
const cut_text = copy_selection(root, sel.*, sfa.get(), self.plane) catch return error.Stop;
defer a.free(cut_text);
const cd = CaseData.init(a) catch return error.Stop;
defer cd.deinit();
const ucased = cd.toLowerStr(a, cut_text) catch return error.Stop;
defer a.free(ucased);
root = try self.delete_selection(root, cursel, a);
root = self.insert(root, cursel, ucased, a) catch return error.Stop;
cursel.* = saved;
return root;
}
pub fn to_lower(self: *Self, _: command.Context) tp.result {
const b = self.buf_for_update() catch |e| return tp.exit_error(e);
const root = self.with_cursels_mut(b.root, to_lower_cursel, b.a) catch |e| return tp.exit_error(e);
self.update_buf(root) catch |e| return tp.exit_error(e);
self.clamp();
}
};
pub fn create(a: Allocator, parent: Widget) !Widget {
return EditorWidget.create(a, 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(a: Allocator, parent: Widget) !Widget {
const container = try WidgetList.createH(a, parent, "editor.container", .dynamic);
const self: *Self = try a.create(Self);
try self.init(a, container.widget());
try self.commands.init(&self.editor);
const editorWidget = Widget.to(self);
try container.add(try editor_gutter.create(a, container.widget(), editorWidget, &self.editor));
try container.add(editorWidget);
try container.add(try scrollbar_v.create(a, container.widget(), editorWidget, EventHandler.to_unowned(container)));
return container.widget();
}
fn init(self: *Self, a: 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(a, n);
errdefer self.editor.deinit();
try self.editor.push_cursor();
}
pub fn deinit(self: *Self, a: Allocator) void {
self.commands.deinit();
self.editor.deinit();
self.plane.deinit();
a.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 {
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) })) {
self.editor.filter_error(bytes) catch {};
} else if (try m.match(.{ "filter", "term", tp.more })) {
self.editor.filter_done() catch {};
} 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;
}
fn mouse_click_event(self: *Self, evtype: c_int, btn: c_int, y: c_int, x: c_int, ypx: c_int, xpx: c_int) tp.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) tp.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) tp.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) tp.result {
const y_, const x_ = self.editor.plane.abs_yx_to_rel(y, x);
try self.editor.primary_drag(y_, x_);
}
fn mouse_click_button2(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) tp.result {}
fn mouse_drag_button2(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) tp.result {}
fn mouse_click_button3(self: *Self, y: c_int, x: c_int, _: c_int, _: c_int) tp.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) tp.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) tp.result {
try self.editor.scroll_up_pageup(.{});
}
fn mouse_click_button5(self: *Self, _: c_int, _: c_int, _: c_int, _: c_int) tp.result {
try self.editor.scroll_down_pagedown(.{});
}
fn mouse_click_button8(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) tp.result {
try command.executeName("jump_back", .{});
}
fn mouse_click_button9(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) tp.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(u16),
cached_line: usize = std.math.maxInt(usize),
cached_root: ?Buffer.Root = null,
const Self = @This();
pub fn init(a: Allocator) !Self {
return .{
.cache = try std.ArrayList(u16).initCapacity(a, 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) 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) catch end.column;
return .{ .begin = .{ .row = start.row, .col = start_col }, .end = .{ .row = end.row, .col = end_col } };
}
};