4548 lines
187 KiB
Zig
4548 lines
187 KiB
Zig
const std = @import("std");
|
||
const builtin = @import("builtin");
|
||
const tp = @import("thespian");
|
||
const cbor = @import("cbor");
|
||
const log = @import("log");
|
||
const Buffer = @import("Buffer");
|
||
const ripgrep = @import("ripgrep");
|
||
const tracy = @import("tracy");
|
||
const text_manip = @import("text_manip");
|
||
const syntax = @import("syntax");
|
||
const project_manager = @import("project_manager");
|
||
const CaseData = @import("CaseData");
|
||
const root_mod = @import("root");
|
||
|
||
const Plane = @import("renderer").Plane;
|
||
const Cell = @import("renderer").Cell;
|
||
const input = @import("input");
|
||
const command = @import("command");
|
||
const EventHandler = @import("EventHandler");
|
||
|
||
const scrollbar_v = @import("scrollbar_v.zig");
|
||
const editor_gutter = @import("editor_gutter.zig");
|
||
const Widget = @import("Widget.zig");
|
||
const WidgetList = @import("WidgetList.zig");
|
||
const tui = @import("tui.zig");
|
||
|
||
pub const Cursor = Buffer.Cursor;
|
||
pub const View = Buffer.View;
|
||
pub const Selection = Buffer.Selection;
|
||
|
||
const Allocator = std.mem.Allocator;
|
||
const time = std.time;
|
||
|
||
const scroll_step_small = 3;
|
||
const scroll_cursor_min_border_distance = 5;
|
||
|
||
const double_click_time_ms = 350;
|
||
const syntax_full_reparse_time_limit = 0; // ms (0 = always use incremental)
|
||
|
||
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, metrics: Buffer.Metrics) *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, metrics);
|
||
sel.end.move_right(root, metrics) catch {};
|
||
}
|
||
return sel;
|
||
}
|
||
|
||
fn select_node(self: *Self, node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!void {
|
||
const range = node.getRange();
|
||
self.selection = .{
|
||
.begin = .{
|
||
.row = range.start_point.row,
|
||
.col = try root.pos_to_width(range.start_point.row, range.start_point.column, metrics),
|
||
},
|
||
.end = .{
|
||
.row = range.end_point.row,
|
||
.col = try root.pos_to_width(range.end_point.row, range.end_point.column, metrics),
|
||
},
|
||
};
|
||
self.cursor = self.selection.?.end;
|
||
}
|
||
|
||
fn write(self: *const Self, writer: Buffer.MetaWriter) !void {
|
||
try self.cursor.write(writer);
|
||
if (self.selection) |sel| {
|
||
try sel.write(writer);
|
||
} else {
|
||
try cbor.writeValue(writer, null);
|
||
}
|
||
}
|
||
|
||
fn extract(self: *Self, iter: *[]const u8) !bool {
|
||
if (!try self.cursor.extract(iter)) return false;
|
||
var iter2 = iter.*;
|
||
if (try cbor.matchValue(&iter2, cbor.null_)) {
|
||
iter.* = iter2;
|
||
} else {
|
||
var sel: Selection = .{};
|
||
if (!try sel.extract(iter)) return false;
|
||
self.selection = sel;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
fn nudge_insert(self: *Self, nudge: Selection) void {
|
||
if (self.selection) |*sel_| sel_.nudge_insert(nudge);
|
||
self.cursor.nudge_insert(nudge);
|
||
}
|
||
|
||
fn nudge_delete(self: *Self, nudge: Selection) bool {
|
||
if (self.selection) |*sel_|
|
||
if (!sel_.nudge_delete(nudge))
|
||
return false;
|
||
return self.cursor.nudge_delete(nudge);
|
||
}
|
||
};
|
||
|
||
pub const Diagnostic = struct {
|
||
source: []const u8,
|
||
code: []const u8,
|
||
message: []const u8,
|
||
severity: i32,
|
||
sel: Selection,
|
||
|
||
fn deinit(self: *Diagnostic, allocator: std.mem.Allocator) void {
|
||
allocator.free(self.source);
|
||
allocator.free(self.code);
|
||
allocator.free(self.message);
|
||
}
|
||
|
||
pub const Severity = enum { Error, Warning, Information, Hint };
|
||
pub fn get_severity(self: Diagnostic) Severity {
|
||
return to_severity(self.severity);
|
||
}
|
||
|
||
pub fn to_severity(sev: i32) Severity {
|
||
return switch (sev) {
|
||
1 => .Error,
|
||
2 => .Warning,
|
||
3 => .Information,
|
||
4 => .Hint,
|
||
else => .Error,
|
||
};
|
||
}
|
||
};
|
||
|
||
pub const Editor = struct {
|
||
const SelectMode = enum {
|
||
char,
|
||
word,
|
||
line,
|
||
};
|
||
const Self = @This();
|
||
pub const Target = Self;
|
||
|
||
allocator: Allocator,
|
||
plane: Plane,
|
||
metrics: Buffer.Metrics,
|
||
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,
|
||
eol_mode: Buffer.EolMode = .lf,
|
||
} = 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,
|
||
render_whitespace: WhitespaceMode,
|
||
indent_size: usize,
|
||
tab_width: usize,
|
||
|
||
last: struct {
|
||
root: ?Buffer.Root = null,
|
||
primary: CurSel = CurSel.invalid(),
|
||
view: View = View.invalid(),
|
||
matches: usize = 0,
|
||
cursels: usize = 0,
|
||
dirty: bool = false,
|
||
eol_mode: Buffer.EolMode = .lf,
|
||
} = .{},
|
||
|
||
syntax: ?*syntax = null,
|
||
syntax_no_render: bool = false,
|
||
syntax_report_timing: bool = false,
|
||
syntax_refresh_full: bool = false,
|
||
syntax_last_rendered_root: ?Buffer.Root = null,
|
||
syntax_incremental_reparse: bool = false,
|
||
|
||
style_cache: ?StyleCache = null,
|
||
style_cache_theme: []const u8 = "",
|
||
|
||
diagnostics: std.ArrayList(Diagnostic),
|
||
diag_errors: usize = 0,
|
||
diag_warnings: usize = 0,
|
||
diag_info: usize = 0,
|
||
diag_hints: usize = 0,
|
||
|
||
need_save_after_filter: ?struct {
|
||
then: ?struct {
|
||
cmd: []const u8,
|
||
args: []const u8,
|
||
} = null,
|
||
} = null,
|
||
|
||
case_data: ?CaseData = null,
|
||
|
||
const WhitespaceMode = enum { visible, indent, none };
|
||
const StyleCache = std.AutoHashMap(u32, ?Widget.Theme.Token);
|
||
|
||
const Context = command.Context;
|
||
const Result = command.Result;
|
||
|
||
pub fn write_state(self: *const Self, writer: Buffer.MetaWriter) !void {
|
||
try cbor.writeArrayHeader(writer, 6);
|
||
try cbor.writeValue(writer, self.file_path orelse "");
|
||
try cbor.writeValue(writer, self.clipboard orelse "");
|
||
try cbor.writeValue(writer, self.last_find_query orelse "");
|
||
if (self.find_history) |history| {
|
||
try cbor.writeArrayHeader(writer, history.items.len);
|
||
for (history.items) |item|
|
||
try cbor.writeValue(writer, item);
|
||
} else {
|
||
try cbor.writeArrayHeader(writer, 0);
|
||
}
|
||
try self.view.write(writer);
|
||
try self.get_primary().cursor.write(writer);
|
||
}
|
||
|
||
pub fn extract_state(self: *Self, buf: []const u8) !void {
|
||
var file_path: []const u8 = undefined;
|
||
var view_cbor: []const u8 = undefined;
|
||
var primary_cbor: []const u8 = undefined;
|
||
var clipboard: []const u8 = undefined;
|
||
var query: []const u8 = undefined;
|
||
var find_history: []const u8 = undefined;
|
||
if (!try cbor.match(buf, .{
|
||
tp.extract(&file_path),
|
||
tp.extract(&clipboard),
|
||
tp.extract(&query),
|
||
tp.extract_cbor(&find_history),
|
||
tp.extract_cbor(&view_cbor),
|
||
tp.extract_cbor(&primary_cbor),
|
||
}))
|
||
return error.RestoreStateMatch;
|
||
try self.open(file_path);
|
||
self.clipboard = if (clipboard.len > 0) try self.allocator.dupe(u8, clipboard) else null;
|
||
self.last_find_query = if (query.len > 0) try self.allocator.dupe(u8, clipboard) else null;
|
||
if (!try self.view.extract(&view_cbor))
|
||
return error.RestoreView;
|
||
self.scroll_dest = self.view.row;
|
||
if (!try self.get_primary().cursor.extract(&primary_cbor))
|
||
return error.RestoreCursor;
|
||
var len = cbor.decodeArrayHeader(&find_history) catch return error.RestoryFindHistory;
|
||
while (len > 0) : (len -= 1) {
|
||
var value: []const u8 = undefined;
|
||
if (!(cbor.matchValue(&find_history, cbor.extract(&value)) catch return error.RestoryFindHistory))
|
||
return error.RestoryFindHistory;
|
||
self.push_find_history(value);
|
||
}
|
||
self.clamp();
|
||
}
|
||
|
||
fn init(self: *Self, allocator: Allocator, n: Plane) void {
|
||
const logger = log.logger("editor");
|
||
const frame_rate = tp.env.get().num("frame-rate");
|
||
const indent_size = tui.current().config.indent_size;
|
||
const tab_width = tui.current().config.tab_width;
|
||
self.* = Self{
|
||
.allocator = allocator,
|
||
.plane = n,
|
||
.indent_size = indent_size,
|
||
.tab_width = tab_width,
|
||
.metrics = self.plane.metrics(tab_width),
|
||
.logger = logger,
|
||
.file_path = null,
|
||
.buffer = null,
|
||
.handlers = EventHandler.List.init(allocator),
|
||
.animation_lag = get_animation_max_lag(),
|
||
.animation_frame_rate = frame_rate,
|
||
.animation_last_time = time.microTimestamp(),
|
||
.cursels = CurSel.List.init(allocator),
|
||
.cursels_saved = CurSel.List.init(allocator),
|
||
.matches = Match.List.init(allocator),
|
||
.enable_terminal_cursor = tui.current().config.enable_terminal_cursor,
|
||
.render_whitespace = from_whitespace_mode(tui.current().config.whitespace_mode),
|
||
.diagnostics = std.ArrayList(Diagnostic).init(allocator),
|
||
};
|
||
}
|
||
|
||
fn deinit(self: *Self) void {
|
||
for (self.diagnostics.items) |*d| d.deinit(self.diagnostics.allocator);
|
||
self.diagnostics.deinit();
|
||
if (self.syntax) |syn| syn.destroy();
|
||
self.cursels.deinit();
|
||
self.matches.deinit();
|
||
self.handlers.deinit();
|
||
self.logger.deinit();
|
||
if (self.buffer) |p| p.deinit();
|
||
if (self.case_data) |cd| cd.deinit();
|
||
}
|
||
|
||
fn from_whitespace_mode(whitespace_mode: []const u8) WhitespaceMode {
|
||
return if (std.mem.eql(u8, whitespace_mode, "visible"))
|
||
.visible
|
||
else if (std.mem.eql(u8, whitespace_mode, "indent"))
|
||
.indent
|
||
else
|
||
.none;
|
||
}
|
||
|
||
fn need_render(_: *Self) void {
|
||
Widget.need_render();
|
||
}
|
||
|
||
fn get_case_data(self: *Self) *CaseData {
|
||
if (self.case_data) |*cd| return cd;
|
||
self.case_data = CaseData.init(self.allocator) catch @panic("CaseData.init");
|
||
return &self.case_data.?;
|
||
}
|
||
|
||
fn buf_for_update(self: *Self) !*const Buffer {
|
||
self.cursels_saved.clearAndFree();
|
||
self.cursels_saved = try self.cursels.clone();
|
||
return self.buffer orelse error.Stop;
|
||
}
|
||
|
||
fn buf_root(self: *const Self) !Buffer.Root {
|
||
return if (self.buffer) |p| p.root else error.Stop;
|
||
}
|
||
|
||
fn buf_eol_mode(self: *const Self) !Buffer.EolMode {
|
||
return if (self.buffer) |p| p.file_eol_mode else error.Stop;
|
||
}
|
||
|
||
fn buf_a(self: *const Self) !Allocator {
|
||
return if (self.buffer) |p| p.allocator else error.Stop;
|
||
}
|
||
|
||
pub fn get_current_root(self: *const Self) ?Buffer.Root {
|
||
return if (self.buffer) |p| p.root else null;
|
||
}
|
||
|
||
pub fn handle_resize(self: *Self, pos: Widget.Box) void {
|
||
self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return;
|
||
self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return;
|
||
self.view.rows = pos.h;
|
||
self.view.cols = pos.w;
|
||
}
|
||
|
||
pub fn is_dirty(self: *Self) bool {
|
||
const b = self.buffer orelse return false;
|
||
return b.is_dirty();
|
||
}
|
||
|
||
fn open(self: *Self, file_path: []const u8) !void {
|
||
var new_buf = try Buffer.create(self.allocator);
|
||
errdefer new_buf.deinit();
|
||
try new_buf.load_from_file_and_update(file_path);
|
||
return self.open_buffer(file_path, new_buf);
|
||
}
|
||
|
||
fn open_scratch(self: *Self, file_path: []const u8, content: []const u8) !void {
|
||
var new_buf = try Buffer.create(self.allocator);
|
||
errdefer new_buf.deinit();
|
||
try new_buf.load_from_string_and_update(file_path, content);
|
||
new_buf.file_exists = true;
|
||
return self.open_buffer(file_path, new_buf);
|
||
}
|
||
|
||
fn open_buffer(self: *Self, file_path: []const u8, new_buf: *Buffer) !void {
|
||
errdefer new_buf.deinit();
|
||
self.cancel_all_selections();
|
||
self.get_primary().reset();
|
||
self.file_path = try self.allocator.dupe(u8, file_path);
|
||
if (self.buffer) |_| try self.close();
|
||
self.buffer = new_buf;
|
||
|
||
if (new_buf.root.lines() > root_mod.max_syntax_lines) {
|
||
self.logger.print("large file threshold {d} lines < file size {d} lines", .{
|
||
root_mod.max_syntax_lines,
|
||
new_buf.root.lines(),
|
||
});
|
||
self.logger.print("syntax highlighting disabled", .{});
|
||
self.syntax_no_render = true;
|
||
}
|
||
self.syntax = syntax: {
|
||
const lang_override = tp.env.get().str("language");
|
||
var content = std.ArrayList(u8).init(self.allocator);
|
||
defer content.deinit();
|
||
try new_buf.root.store(content.writer(), new_buf.file_eol_mode);
|
||
const syn = if (lang_override.len > 0)
|
||
syntax.create_file_type(self.allocator, lang_override) catch null
|
||
else
|
||
syntax.create_guess_file_type(self.allocator, content.items, self.file_path) catch null;
|
||
if (syn) |syn_|
|
||
project_manager.did_open(file_path, syn_.file_type, self.lsp_version, try content.toOwnedSlice()) catch |e|
|
||
self.logger.print("project_manager.did_open failed: {any}", .{e});
|
||
break :syntax syn;
|
||
};
|
||
self.syntax_no_render = tp.env.get().is("no-syntax");
|
||
self.syntax_report_timing = tp.env.get().is("syntax-report-timing");
|
||
|
||
const ftn = if (self.syntax) |syn| syn.file_type.name else "text";
|
||
const fti = if (self.syntax) |syn| syn.file_type.icon else "🖹";
|
||
const ftc = if (self.syntax) |syn| syn.file_type.color else 0x000000;
|
||
try self.send_editor_open(file_path, new_buf.file_exists, ftn, fti, ftc);
|
||
}
|
||
|
||
fn close(self: *Self) !void {
|
||
return self.close_internal(false);
|
||
}
|
||
|
||
fn close_dirty(self: *Self) !void {
|
||
return self.close_internal(true);
|
||
}
|
||
|
||
fn close_internal(self: *Self, allow_dirty_close: bool) !void {
|
||
const b = self.buffer orelse return error.Stop;
|
||
if (!allow_dirty_close and b.is_dirty()) return tp.exit("unsaved changes");
|
||
if (self.buffer) |b_mut| b_mut.deinit();
|
||
self.buffer = null;
|
||
self.plane.erase();
|
||
self.plane.home();
|
||
tui.current().rdr.cursor_disable();
|
||
_ = try self.handlers.msg(.{ "E", "close" });
|
||
if (self.syntax) |_| if (self.file_path) |file_path|
|
||
project_manager.did_close(file_path) catch {};
|
||
}
|
||
|
||
fn save(self: *Self) !void {
|
||
const b = self.buffer orelse return error.Stop;
|
||
if (!b.is_dirty()) return self.logger.print("no changes to save", .{});
|
||
if (self.file_path) |file_path| {
|
||
if (self.buffer) |b_mut| try b_mut.store_to_file_and_clean(file_path);
|
||
} else return error.SaveNoFileName;
|
||
try self.send_editor_save(self.file_path.?);
|
||
self.last.dirty = false;
|
||
}
|
||
|
||
fn save_as(self: *Self, file_path: []const u8) !void {
|
||
if (self.buffer) |b_mut| try b_mut.store_to_file_and_clean(file_path);
|
||
if (self.file_path) |old_file_path| self.allocator.free(old_file_path);
|
||
self.file_path = try self.allocator.dupe(u8, file_path);
|
||
try self.send_editor_save(self.file_path.?);
|
||
self.last.dirty = false;
|
||
}
|
||
|
||
pub fn push_cursor(self: *Self) !void {
|
||
const primary = self.cursels.getLastOrNull() orelse CurSel{} orelse CurSel{};
|
||
(try self.cursels.addOne()).* = primary;
|
||
}
|
||
|
||
pub fn pop_cursor(self: *Self, _: Context) Result {
|
||
if (self.cursels.items.len > 1) {
|
||
const cursel = self.cursels.popOrNull() orelse return orelse return;
|
||
if (cursel.selection) |sel| if (self.find_selection_match(sel)) |match| {
|
||
match.has_selection = false;
|
||
};
|
||
}
|
||
self.clamp();
|
||
}
|
||
pub const pop_cursor_meta = .{ .description = "Remove last added cursor" };
|
||
|
||
pub fn get_primary(self: *const Self) *CurSel {
|
||
var idx = self.cursels.items.len;
|
||
while (idx > 0) : (idx -= 1)
|
||
if (self.cursels.items[idx - 1]) |*primary|
|
||
return primary;
|
||
if (idx == 0) {
|
||
self.logger.print("ERROR: no more cursors", .{});
|
||
(@constCast(self).cursels.addOne() catch |e| switch (e) {
|
||
error.OutOfMemory => @panic("get_primary error.OutOfMemory"),
|
||
}).* = CurSel{};
|
||
}
|
||
return self.get_primary();
|
||
}
|
||
|
||
fn store_undo_meta(self: *Self, allocator: Allocator) ![]u8 {
|
||
var meta = std.ArrayList(u8).init(allocator);
|
||
const writer = meta.writer();
|
||
for (self.cursels_saved.items) |*cursel_| if (cursel_.*) |*cursel|
|
||
try cursel.write(writer);
|
||
return meta.toOwnedSlice();
|
||
}
|
||
|
||
fn store_current_undo_meta(self: *Self, allocator: Allocator) ![]u8 {
|
||
var meta = std.ArrayList(u8).init(allocator);
|
||
const writer = meta.writer();
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel|
|
||
try cursel.write(writer);
|
||
return meta.toOwnedSlice();
|
||
}
|
||
|
||
fn update_buf(self: *Self, root: Buffer.Root) !void {
|
||
const b = self.buffer orelse return error.Stop;
|
||
return self.update_buf_and_eol_mode(root, b.file_eol_mode);
|
||
}
|
||
|
||
fn update_buf_and_eol_mode(self: *Self, root: Buffer.Root, eol_mode: Buffer.EolMode) !void {
|
||
const b = self.buffer orelse return error.Stop;
|
||
var sfa = std.heap.stackFallback(512, self.allocator);
|
||
const allocator = sfa.get();
|
||
const meta = try self.store_undo_meta(allocator);
|
||
defer allocator.free(meta);
|
||
try b.store_undo(meta);
|
||
b.update(root);
|
||
b.file_eol_mode = eol_mode;
|
||
try self.send_editor_modified();
|
||
}
|
||
|
||
fn restore_undo_redo_meta(self: *Self, meta: []const u8) !void {
|
||
if (meta.len > 0)
|
||
self.clear_all_cursors();
|
||
var iter = meta;
|
||
while (iter.len > 0) {
|
||
var cursel: CurSel = .{};
|
||
if (!try cursel.extract(&iter)) return error.SyntaxError;
|
||
(try self.cursels.addOne()).* = cursel;
|
||
}
|
||
}
|
||
|
||
fn restore_undo(self: *Self) !void {
|
||
if (self.buffer) |b_mut| {
|
||
try self.send_editor_jump_source();
|
||
self.cancel_all_matches();
|
||
var sfa = std.heap.stackFallback(512, self.allocator);
|
||
const allocator = sfa.get();
|
||
const redo_metadata = try self.store_current_undo_meta(allocator);
|
||
defer allocator.free(redo_metadata);
|
||
const meta = b_mut.undo(redo_metadata) 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();
|
||
}
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|
||
|
||
fn find_first_non_ws(root: Buffer.Root, row: usize, metrics: Buffer.Metrics) usize {
|
||
const Ctx = struct {
|
||
col: usize = 0,
|
||
fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize, _: Buffer.Metrics) Buffer.Walker {
|
||
const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_)));
|
||
if (egc[0] == ' ' or egc[0] == '\t') {
|
||
ctx.col += wcwidth;
|
||
return Buffer.Walker.keep_walking;
|
||
}
|
||
return Buffer.Walker.stop;
|
||
}
|
||
};
|
||
var ctx: Ctx = .{};
|
||
root.walk_egc_forward(row, Ctx.walker, &ctx, metrics) catch return 0;
|
||
return ctx.col;
|
||
}
|
||
|
||
fn write_range(
|
||
self: *const Self,
|
||
root: Buffer.Root,
|
||
sel: Selection,
|
||
writer: anytype,
|
||
map_error: fn (e: anyerror, stack_trace: ?*std.builtin.StackTrace) @TypeOf(writer).Error,
|
||
wcwidth_: ?*usize,
|
||
) @TypeOf(writer).Error!void {
|
||
const Writer = @TypeOf(writer);
|
||
const Ctx = struct {
|
||
col: usize = 0,
|
||
sel: Selection,
|
||
writer: Writer,
|
||
wcwidth: usize = 0,
|
||
fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize, _: Buffer.Metrics) Buffer.Walker {
|
||
const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_)));
|
||
if (ctx.col < ctx.sel.begin.col) {
|
||
ctx.col += wcwidth;
|
||
return Buffer.Walker.keep_walking;
|
||
}
|
||
_ = ctx.writer.write(egc) catch |e| return Buffer.Walker{ .err = e };
|
||
ctx.wcwidth += wcwidth;
|
||
if (egc[0] == '\n') {
|
||
ctx.col = 0;
|
||
ctx.sel.begin.col = 0;
|
||
ctx.sel.begin.row += 1;
|
||
} else {
|
||
ctx.col += wcwidth;
|
||
ctx.sel.begin.col += wcwidth;
|
||
}
|
||
return if (ctx.sel.begin.eql(ctx.sel.end))
|
||
Buffer.Walker.stop
|
||
else
|
||
Buffer.Walker.keep_walking;
|
||
}
|
||
};
|
||
|
||
var ctx: Ctx = .{ .sel = sel, .writer = writer };
|
||
ctx.sel.normalize();
|
||
if (sel.begin.eql(sel.end))
|
||
return;
|
||
root.walk_egc_forward(sel.begin.row, Ctx.walker, &ctx, self.metrics) catch |e| return map_error(e, @errorReturnTrace());
|
||
if (wcwidth_) |p| p.* = ctx.wcwidth;
|
||
}
|
||
|
||
pub fn update(self: *Self) void {
|
||
self.update_scroll();
|
||
self.update_event() catch {};
|
||
}
|
||
|
||
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor render" });
|
||
defer frame.deinit();
|
||
self.update_syntax() catch |e| switch (e) {
|
||
error.Stop => {},
|
||
else => self.logger.err("update_syntax", e),
|
||
};
|
||
if (self.style_cache) |*cache| {
|
||
if (!std.mem.eql(u8, self.style_cache_theme, theme.name)) {
|
||
cache.deinit();
|
||
self.style_cache = StyleCache.init(self.allocator);
|
||
// self.logger.print("style_cache reset {s} -> {s}", .{ self.style_cache_theme, theme.name });
|
||
}
|
||
} else {
|
||
self.style_cache = StyleCache.init(self.allocator);
|
||
}
|
||
self.style_cache_theme = theme.name;
|
||
const cache: *StyleCache = &self.style_cache.?;
|
||
self.render_screen(theme, cache);
|
||
return self.scroll_dest != self.view.row;
|
||
}
|
||
|
||
fn render_screen(self: *Self, theme: *const Widget.Theme, cache: *StyleCache) void {
|
||
const ctx = struct {
|
||
self: *Self,
|
||
buf_row: usize,
|
||
buf_col: usize = 0,
|
||
match_idx: usize = 0,
|
||
theme: *const Widget.Theme,
|
||
hl_row: ?usize,
|
||
leading: bool = true,
|
||
|
||
fn walker(ctx_: *anyopaque, leaf: *const Buffer.Leaf, _: Buffer.Metrics) Buffer.Walker {
|
||
const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_)));
|
||
const self_ = ctx.self;
|
||
const view = self_.view;
|
||
const n = &self_.plane;
|
||
|
||
if (ctx.buf_row > view.row + view.rows)
|
||
return Buffer.Walker.stop;
|
||
|
||
const bufsize = 4095;
|
||
var bufstatic: [bufsize:0]u8 = undefined;
|
||
const len = leaf.buf.len;
|
||
var chunk_alloc: ?[:0]u8 = null;
|
||
var chunk: [:0]u8 = if (len > bufsize) ret: {
|
||
const ptr = self_.allocator.allocSentinel(u8, len, 0) catch |e| return Buffer.Walker{ .err = e };
|
||
chunk_alloc = ptr;
|
||
break :ret ptr;
|
||
} else &bufstatic;
|
||
defer if (chunk_alloc) |p| self_.allocator.free(p);
|
||
|
||
@memcpy(chunk[0..leaf.buf.len], leaf.buf);
|
||
chunk[leaf.buf.len] = 0;
|
||
chunk.len = leaf.buf.len;
|
||
|
||
while (chunk.len > 0) {
|
||
if (ctx.buf_col >= view.col + view.cols)
|
||
break;
|
||
var cell = n.cell_init();
|
||
var end_cell: ?Cell = null;
|
||
const c = &cell;
|
||
switch (chunk[0]) {
|
||
0...8, 10...31, 32, 9 => {},
|
||
else => ctx.leading = false,
|
||
}
|
||
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.buf_col, ctx.leading, ctx.theme),
|
||
9 => ctx.self.render_tab(c, n, ctx.buf_col, ctx.theme, &end_cell),
|
||
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_ = if (end_cell) |ec| if (ctx.buf_col == new_col - 1) ec else c.* else 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;
|
||
ctx.leading = true;
|
||
}
|
||
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.metrics) catch {};
|
||
}
|
||
self.render_syntax(theme, cache, root) catch {};
|
||
self.render_diagnostics(theme, hl_row) catch {};
|
||
self.render_cursors(theme) catch {};
|
||
}
|
||
|
||
fn render_terminal_cursor(self: *const Self, cursor_: *const Cursor) !void {
|
||
if (self.screen_cursor(cursor_)) |cursor| {
|
||
const y, const x = self.plane.rel_yx_to_abs(@intCast(cursor.row), @intCast(cursor.col));
|
||
const shape = if (tui.current().input_mode) |mode|
|
||
mode.cursor_shape
|
||
else
|
||
.block;
|
||
tui.current().rdr.cursor_enable(y, x, tui.translate_cursor_shape(shape)) catch {};
|
||
} else {
|
||
tui.current().rdr.cursor_disable();
|
||
}
|
||
}
|
||
|
||
fn render_cursors(self: *Self, theme: *const Widget.Theme) !void {
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor render cursors" });
|
||
defer frame.deinit();
|
||
if (self.cursels.items.len == 1 and self.enable_terminal_cursor)
|
||
return self.render_terminal_cursor(&self.get_primary().cursor);
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel|
|
||
try self.render_cursor(&cursel.cursor, theme);
|
||
if (self.enable_terminal_cursor)
|
||
try self.render_terminal_cursor(&self.get_primary().cursor);
|
||
}
|
||
|
||
fn render_cursor(self: *Self, cursor: *const Cursor, theme: *const Widget.Theme) !void {
|
||
if (self.screen_cursor(cursor)) |pos| {
|
||
self.plane.cursor_move_yx(@intCast(pos.row), @intCast(pos.col)) catch return;
|
||
self.render_cursor_cell(theme);
|
||
}
|
||
}
|
||
|
||
fn render_line_highlight(self: *Self, cursor: *const Cursor, theme: *const Widget.Theme) !void {
|
||
const row_min = self.view.row;
|
||
const row_max = row_min + self.view.rows;
|
||
if (cursor.row < row_min or row_max < cursor.row)
|
||
return;
|
||
const row = cursor.row - self.view.row;
|
||
for (0..self.view.cols) |i| {
|
||
self.plane.cursor_move_yx(@intCast(row), @intCast(i)) catch return;
|
||
var cell = self.plane.cell_init();
|
||
_ = self.plane.at_cursor_cell(&cell) catch return;
|
||
self.render_line_highlight_cell(theme, &cell);
|
||
_ = self.plane.putc(&cell) catch {};
|
||
}
|
||
}
|
||
|
||
fn render_matches(self: *const Self, last_idx: *usize, theme: *const Widget.Theme, cell: *Cell) void {
|
||
var y: c_uint = undefined;
|
||
var x: c_uint = undefined;
|
||
self.plane.cursor_yx(&y, &x);
|
||
while (true) {
|
||
if (last_idx.* >= self.matches.items.len)
|
||
return;
|
||
const sel = if (self.matches.items[last_idx.*]) |sel_| sel_ else {
|
||
last_idx.* += 1;
|
||
continue;
|
||
};
|
||
if (self.is_point_before_selection(sel, y, x))
|
||
return;
|
||
if (self.is_point_in_selection(sel, y, x))
|
||
return self.render_match_cell(theme, cell, sel);
|
||
last_idx.* += 1;
|
||
}
|
||
}
|
||
|
||
fn render_selections(self: *const Self, theme: *const Widget.Theme, cell: *Cell) void {
|
||
var y: c_uint = undefined;
|
||
var x: c_uint = undefined;
|
||
self.plane.cursor_yx(&y, &x);
|
||
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel|
|
||
if (cursel.selection) |sel_| {
|
||
var sel = sel_;
|
||
sel.normalize();
|
||
if (self.is_point_in_selection(sel, y, x))
|
||
return self.render_selection_cell(theme, cell);
|
||
};
|
||
}
|
||
|
||
fn render_diagnostics(self: *Self, theme: *const Widget.Theme, hl_row: ?usize) !void {
|
||
for (self.diagnostics.items) |*diag| self.render_diagnostic(diag, theme, hl_row);
|
||
}
|
||
|
||
fn render_diagnostic(self: *Self, diag: *const Diagnostic, theme: *const Widget.Theme, hl_row: ?usize) void {
|
||
const screen_width = self.view.cols;
|
||
const pos = self.screen_cursor(&diag.sel.begin) orelse return;
|
||
var style = switch (diag.get_severity()) {
|
||
.Error => theme.editor_error,
|
||
.Warning => theme.editor_warning,
|
||
.Information => theme.editor_information,
|
||
.Hint => theme.editor_hint,
|
||
};
|
||
if (hl_row) |hlr| if (hlr == diag.sel.begin.row) {
|
||
style = .{ .fg = style.fg, .bg = theme.editor_line_highlight.bg };
|
||
};
|
||
|
||
self.plane.cursor_move_yx(@intCast(pos.row), @intCast(pos.col)) catch return;
|
||
self.render_diagnostic_cell(style);
|
||
if (diag.sel.begin.row == diag.sel.end.row) {
|
||
var col = pos.col;
|
||
while (col < diag.sel.end.col) : (col += 1) {
|
||
self.plane.cursor_move_yx(@intCast(pos.row), @intCast(col)) catch return;
|
||
self.render_diagnostic_cell(style);
|
||
}
|
||
}
|
||
const space_begin = get_line_end_space_begin(&self.plane, screen_width, pos.row);
|
||
if (space_begin < screen_width) {
|
||
self.render_diagnostic_message(diag.message, pos.row, screen_width - space_begin, style);
|
||
}
|
||
}
|
||
|
||
fn get_line_end_space_begin(plane: *Plane, screen_width: usize, screen_row: usize) usize {
|
||
var pos = screen_width;
|
||
var cell = plane.cell_init();
|
||
while (pos > 0) : (pos -= 1) {
|
||
plane.cursor_move_yx(@intCast(screen_row), @intCast(pos - 1)) catch return pos;
|
||
const cell_egc_bytes = plane.at_cursor_cell(&cell) catch return pos;
|
||
if (cell_egc_bytes > 0) return pos;
|
||
}
|
||
return pos;
|
||
}
|
||
|
||
fn render_diagnostic_message(self: *Self, message_: []const u8, y: usize, max_space: usize, style: Widget.Theme.Style) void {
|
||
self.plane.set_style(style);
|
||
var iter = std.mem.splitScalar(u8, message_, '\n');
|
||
if (iter.next()) |message|
|
||
_ = self.plane.print_aligned_right(@intCast(y), "{s}", .{message[0..@min(max_space, message.len)]}) catch {};
|
||
}
|
||
|
||
inline fn render_diagnostic_cell(self: *Self, style: Widget.Theme.Style) void {
|
||
var cell = self.plane.cell_init();
|
||
_ = self.plane.at_cursor_cell(&cell) catch return;
|
||
cell.set_style(.{ .fs = .undercurl });
|
||
if (style.fg) |ul_col| cell.set_under_color(ul_col.color);
|
||
_ = 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_opaque(theme.editor);
|
||
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.render_whitespace == .visible)
|
||
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.render_whitespace == .visible) {
|
||
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, buf_col: usize, leading: bool, theme: *const Widget.Theme) struct { usize, usize } {
|
||
switch (self.render_whitespace) {
|
||
.visible => {
|
||
c.set_style(theme.editor_whitespace);
|
||
_ = n.cell_load(c, "·") catch {};
|
||
},
|
||
.indent => {
|
||
c.set_style(theme.editor_whitespace);
|
||
_ = n.cell_load(c, if (leading) if (buf_col % self.indent_size == 0) "│" else " " else " ") 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, end_cell: *?Cell) struct { usize, usize } {
|
||
const begin_char = "-";
|
||
const end_char = ">";
|
||
const colcount = 1 + self.tab_width - (abs_col % self.tab_width);
|
||
switch (self.render_whitespace) {
|
||
.visible => {
|
||
c.set_style(theme.editor_whitespace);
|
||
_ = n.cell_load(c, if (colcount == 2) end_char else begin_char) catch {};
|
||
if (colcount > 1) {
|
||
end_cell.* = n.cell_init();
|
||
const c_: *Cell = &end_cell.*.?;
|
||
_ = n.cell_load(c_, end_char) catch {};
|
||
c_.set_style(theme.editor_whitespace);
|
||
}
|
||
},
|
||
else => {
|
||
_ = n.cell_load(c, " ") catch {};
|
||
},
|
||
}
|
||
return .{ 1, colcount };
|
||
}
|
||
|
||
inline fn render_egc(c: *Cell, n: *Plane, egc: [:0]const u8) struct { usize, usize } {
|
||
const bytes = n.cell_load(c, egc) catch return .{ 1, 1 };
|
||
const colcount = c.columns();
|
||
return .{ bytes, colcount };
|
||
}
|
||
|
||
fn render_syntax(self: *Self, theme: *const Widget.Theme, cache: *StyleCache, root: Buffer.Root) !void {
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor render syntax" });
|
||
defer frame.deinit();
|
||
const syn = self.syntax orelse return;
|
||
const Ctx = struct {
|
||
self: *Self,
|
||
theme: *const Widget.Theme,
|
||
cache: *StyleCache,
|
||
root: Buffer.Root,
|
||
pos_cache: PosToWidthCache,
|
||
last_begin: Cursor = Cursor.invalid(),
|
||
fn cb(ctx: *@This(), range: syntax.Range, scope: []const u8, id: u32, idx: usize, _: *const syntax.Node) error{Stop}!void {
|
||
const sel_ = ctx.pos_cache.range_to_selection(range, ctx.root, ctx.self.metrics) orelse return;
|
||
|
||
if (idx > 0) return;
|
||
if (sel_.begin.eql(ctx.last_begin)) return;
|
||
ctx.last_begin = sel_.begin;
|
||
const style_ = style_cache_lookup(ctx.theme, ctx.cache, scope, id);
|
||
const style = if (style_) |sty| sty.style else return;
|
||
var sel = sel_;
|
||
|
||
if (sel.end.row < ctx.self.view.row) return;
|
||
if (sel.begin.row > ctx.self.view.row + ctx.self.view.rows) return;
|
||
if (sel.begin.row < ctx.self.view.row) sel.begin.row = ctx.self.view.row;
|
||
if (sel.end.row > ctx.self.view.row + ctx.self.view.rows) sel.end.row = ctx.self.view.row + ctx.self.view.rows;
|
||
|
||
if (sel.end.col < ctx.self.view.col) return;
|
||
if (sel.begin.col > ctx.self.view.col + ctx.self.view.cols) return;
|
||
if (sel.begin.col < ctx.self.view.col) sel.begin.col = ctx.self.view.col;
|
||
if (sel.end.col > ctx.self.view.col + ctx.self.view.cols) sel.end.col = ctx.self.view.col + ctx.self.view.cols;
|
||
|
||
for (sel.begin.row..sel.end.row + 1) |row| {
|
||
const begin_col = if (row == sel.begin.row) sel.begin.col else 0;
|
||
const end_col = if (row == sel.end.row) sel.end.col else ctx.self.view.col + ctx.self.view.cols;
|
||
const y = @max(ctx.self.view.row, row) - ctx.self.view.row;
|
||
const x = @max(ctx.self.view.col, begin_col) - ctx.self.view.col;
|
||
const end_x = @max(ctx.self.view.col, end_col) - ctx.self.view.col;
|
||
if (x >= end_x) return;
|
||
for (x..end_x) |x_|
|
||
try ctx.render_cell(y, x_, style);
|
||
}
|
||
}
|
||
fn render_cell(ctx: *@This(), y: usize, x: usize, style: Widget.Theme.Style) !void {
|
||
ctx.self.plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return;
|
||
var cell = ctx.self.plane.cell_init();
|
||
_ = ctx.self.plane.at_cursor_cell(&cell) catch return;
|
||
cell.set_style(style);
|
||
_ = ctx.self.plane.putc(&cell) catch {};
|
||
}
|
||
};
|
||
var ctx: Ctx = .{
|
||
.self = self,
|
||
.theme = theme,
|
||
.cache = cache,
|
||
.root = root,
|
||
.pos_cache = try PosToWidthCache.init(self.allocator),
|
||
};
|
||
defer ctx.pos_cache.deinit();
|
||
const range: syntax.Range = .{
|
||
.start_point = .{ .row = @intCast(self.view.row), .column = 0 },
|
||
.end_point = .{ .row = @intCast(self.view.row + self.view.rows), .column = 0 },
|
||
.start_byte = 0,
|
||
.end_byte = 0,
|
||
};
|
||
return syn.render(&ctx, Ctx.cb, range);
|
||
}
|
||
|
||
fn style_cache_lookup(theme: *const Widget.Theme, cache: *StyleCache, scope: []const u8, id: u32) ?Widget.Theme.Token {
|
||
return if (cache.get(id)) |sty| ret: {
|
||
break :ret sty;
|
||
} else ret: {
|
||
const sty = tui.find_scope_style(theme, scope) orelse null;
|
||
cache.put(id, sty) catch {};
|
||
break :ret sty;
|
||
};
|
||
}
|
||
|
||
pub fn style_lookup(self: *Self, theme_: ?*const Widget.Theme, scope: []const u8, id: u32) ?Widget.Theme.Token {
|
||
const theme = theme_ orelse return null;
|
||
const cache = &(self.style_cache orelse return null);
|
||
return style_cache_lookup(theme, cache, scope, id);
|
||
}
|
||
|
||
inline fn is_point_in_selection(self: *const Self, sel_: anytype, y: c_uint, x: c_uint) bool {
|
||
const sel = sel_;
|
||
const row = self.view.row + y;
|
||
const col = self.view.col + x;
|
||
const b_col: usize = if (sel.begin.row < row) 0 else sel.begin.col;
|
||
const e_col: usize = if (row < sel.end.row) std.math.maxInt(u32) else sel.end.col;
|
||
return sel.begin.row <= row and row <= sel.end.row and b_col <= col and col < e_col;
|
||
}
|
||
|
||
inline fn is_point_before_selection(self: *const Self, sel_: anytype, y: c_uint, x: c_uint) bool {
|
||
const sel = sel_;
|
||
const row = self.view.row + y;
|
||
const col = self.view.col + x;
|
||
return row < sel.begin.row or (row == sel.begin.row and col < sel.begin.col);
|
||
}
|
||
|
||
inline fn screen_cursor(self: *const Self, cursor: *const Cursor) ?Cursor {
|
||
return if (self.view.is_visible(cursor)) .{
|
||
.row = cursor.row - self.view.row,
|
||
.col = cursor.col - self.view.col,
|
||
} else null;
|
||
}
|
||
|
||
inline fn screen_pos_y(self: *Self) usize {
|
||
return self.primary.row - self.view.row;
|
||
}
|
||
|
||
inline fn screen_pos_x(self: *Self) usize {
|
||
return self.primary.col - self.view.col;
|
||
}
|
||
|
||
fn update_event(self: *Self) !void {
|
||
const primary = self.get_primary();
|
||
const dirty = if (self.buffer) |buf| buf.is_dirty() else false;
|
||
|
||
const root: ?Buffer.Root = self.buf_root() catch null;
|
||
const eol_mode = self.buf_eol_mode() catch .lf;
|
||
if (token_from(self.last.root) != token_from(root)) {
|
||
try self.send_editor_update(self.last.root, root, eol_mode);
|
||
self.lsp_version += 1;
|
||
}
|
||
|
||
if (self.last.eol_mode != eol_mode)
|
||
try self.send_editor_eol_mode(eol_mode);
|
||
|
||
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;
|
||
self.last.eol_mode = eol_mode;
|
||
}
|
||
|
||
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, eol_mode: Buffer.EolMode) !void {
|
||
_ = try self.handlers.msg(.{ "E", "update", token_from(new_root), token_from(old_root), @intFromEnum(eol_mode) });
|
||
if (self.syntax) |_| if (self.file_path) |file_path| if (old_root != null and new_root != null)
|
||
project_manager.did_change(file_path, self.lsp_version, token_from(new_root), token_from(old_root), eol_mode) catch {};
|
||
}
|
||
|
||
fn send_editor_eol_mode(self: *const Self, eol_mode: Buffer.EolMode) !void {
|
||
_ = try self.handlers.msg(.{ "E", "eol_mode", @intFromEnum(eol_mode) });
|
||
}
|
||
|
||
fn clamp_abs(self: *Self, abs: bool) void {
|
||
var dest: View = self.view;
|
||
dest.clamp(&self.get_primary().cursor, abs);
|
||
self.update_scroll_dest_abs(dest.row);
|
||
self.view.col = dest.col;
|
||
}
|
||
|
||
inline fn clamp(self: *Self) void {
|
||
self.clamp_abs(false);
|
||
}
|
||
|
||
fn clamp_mouse(self: *Self) void {
|
||
self.clamp_abs(true);
|
||
}
|
||
|
||
fn clear_all_cursors(self: *Self) void {
|
||
self.cursels.clearRetainingCapacity();
|
||
}
|
||
|
||
fn collapse_cursors(self: *Self) void {
|
||
const frame = tracy.initZone(@src(), .{ .name = "collapse cursors" });
|
||
defer frame.deinit();
|
||
var old = self.cursels;
|
||
defer old.deinit();
|
||
self.cursels = CurSel.List.initCapacity(self.allocator, old.items.len) catch return;
|
||
for (old.items[0 .. old.items.len - 1], 0..) |*a_, i| if (a_.*) |*a| {
|
||
for (old.items[i + 1 ..], i + 1..) |*b_, j| if (b_.*) |*b| {
|
||
if (a.cursor.eql(b.cursor))
|
||
old.items[j] = null;
|
||
};
|
||
};
|
||
for (old.items) |*item_| if (item_.*) |*item| {
|
||
(self.cursels.addOne() catch return).* = item.*;
|
||
};
|
||
}
|
||
|
||
fn cancel_all_selections(self: *Self) void {
|
||
var primary = self.cursels.getLast() orelse CurSel{};
|
||
primary.selection = null;
|
||
self.cursels.clearRetainingCapacity();
|
||
self.cursels.addOneAssumeCapacity().* = primary;
|
||
for (self.matches.items) |*match_| if (match_.*) |*match| {
|
||
match.has_selection = false;
|
||
};
|
||
}
|
||
|
||
fn cancel_all_matches(self: *Self) void {
|
||
self.matches.clearAndFree();
|
||
}
|
||
|
||
pub fn clear_matches(self: *Self) void {
|
||
self.cancel_all_matches();
|
||
self.match_token += 1;
|
||
self.match_done_token = self.match_token;
|
||
}
|
||
|
||
pub fn init_matches_update(self: *Self) void {
|
||
self.cancel_all_matches();
|
||
self.match_token += 1;
|
||
}
|
||
|
||
fn with_cursor_const(root: Buffer.Root, move: cursor_operator_const, cursel: *CurSel, metrics: Buffer.Metrics) error{Stop}!void {
|
||
try move(root, &cursel.cursor, metrics);
|
||
}
|
||
|
||
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.metrics);
|
||
};
|
||
self.collapse_cursors();
|
||
}
|
||
|
||
fn with_cursor_const_arg(root: Buffer.Root, move: cursor_operator_const_arg, cursel: *CurSel, ctx: Context, metrics: Buffer.Metrics) error{Stop}!void {
|
||
try move(root, &cursel.cursor, ctx, metrics);
|
||
}
|
||
|
||
fn with_cursors_const_arg(self: *Self, root: Buffer.Root, move: cursor_operator_const_arg, ctx: Context) error{Stop}!void {
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
cursel.selection = null;
|
||
try with_cursor_const_arg(root, move, cursel, ctx, self.metrics);
|
||
};
|
||
self.collapse_cursors();
|
||
}
|
||
|
||
fn with_cursor_and_view_const(root: Buffer.Root, move: cursor_view_operator_const, cursel: *CurSel, view: *const View, metrics: Buffer.Metrics) error{Stop}!void {
|
||
try move(root, &cursel.cursor, view, metrics);
|
||
}
|
||
|
||
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.metrics) catch {
|
||
someone_stopped = true;
|
||
};
|
||
self.collapse_cursors();
|
||
return if (someone_stopped) error.Stop else {};
|
||
}
|
||
|
||
fn with_cursor(root: Buffer.Root, move: cursor_operator, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
return try move(root, &cursel.cursor, allocator);
|
||
}
|
||
|
||
fn with_cursors(self: *Self, root_: Buffer.Root, move: cursor_operator, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var root = root_;
|
||
for (self.cursels.items) |*cursel| {
|
||
cursel.selection = null;
|
||
root = try with_cursor(root, move, cursel, allocator);
|
||
}
|
||
self.collapse_cursors();
|
||
return root;
|
||
}
|
||
|
||
fn with_selection_const(root: Buffer.Root, move: cursor_operator_const, cursel: *CurSel, metrics: Buffer.Metrics) error{Stop}!void {
|
||
const sel = cursel.enable_selection();
|
||
try move(root, &sel.end, metrics);
|
||
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.metrics) catch {
|
||
someone_stopped = true;
|
||
};
|
||
self.collapse_cursors();
|
||
return if (someone_stopped) error.Stop else {};
|
||
}
|
||
|
||
fn with_selection_const_arg(root: Buffer.Root, move: cursor_operator_const_arg, cursel: *CurSel, ctx: Context, metrics: Buffer.Metrics) error{Stop}!void {
|
||
const sel = cursel.enable_selection();
|
||
try move(root, &sel.end, ctx, metrics);
|
||
cursel.cursor = sel.end;
|
||
cursel.check_selection();
|
||
}
|
||
|
||
fn with_selections_const_arg(self: *Self, root: Buffer.Root, move: cursor_operator_const_arg, ctx: Context) error{Stop}!void {
|
||
var someone_stopped = false;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel|
|
||
with_selection_const_arg(root, move, cursel, ctx, self.metrics) 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, metrics: Buffer.Metrics) error{Stop}!void {
|
||
const sel = cursel.enable_selection();
|
||
try move(root, &sel.end, view, metrics);
|
||
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.metrics) catch {
|
||
someone_stopped = true;
|
||
};
|
||
self.collapse_cursors();
|
||
return if (someone_stopped) error.Stop else {};
|
||
}
|
||
|
||
fn with_cursel(root: Buffer.Root, op: cursel_operator, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
return op(root, cursel, allocator);
|
||
}
|
||
|
||
fn with_cursels(self: *Self, root_: Buffer.Root, move: cursel_operator, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var root = root_;
|
||
var someone_stopped = false;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
root = with_cursel(root, move, cursel, allocator) catch ret: {
|
||
someone_stopped = true;
|
||
break :ret root;
|
||
};
|
||
};
|
||
self.collapse_cursors();
|
||
return if (someone_stopped) error.Stop else root;
|
||
}
|
||
|
||
fn with_cursel_mut(self: *Self, root: Buffer.Root, op: cursel_operator_mut, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
return op(self, root, cursel, allocator);
|
||
}
|
||
|
||
fn with_cursels_mut(self: *Self, root_: Buffer.Root, move: cursel_operator_mut, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var root = root_;
|
||
var someone_stopped = false;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
root = self.with_cursel_mut(root, move, cursel, allocator) catch ret: {
|
||
someone_stopped = true;
|
||
break :ret root;
|
||
};
|
||
};
|
||
self.collapse_cursors();
|
||
return if (someone_stopped) error.Stop else root;
|
||
}
|
||
|
||
fn with_cursel_const(root: Buffer.Root, op: cursel_operator_const, cursel: *CurSel) error{Stop}!void {
|
||
return op(root, cursel);
|
||
}
|
||
|
||
fn with_cursels_const(self: *Self, root: Buffer.Root, move: cursel_operator_const) error{Stop}!void {
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel|
|
||
with_cursel_const(root, move, cursel) catch return error.Stop;
|
||
self.collapse_cursors();
|
||
}
|
||
|
||
fn nudge_insert(self: *Self, nudge: Selection, exclude: *const CurSel, _: 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);
|
||
}
|
||
|
||
fn nudge_delete(self: *Self, nudge: Selection, exclude: *const CurSel, _: usize) void {
|
||
for (self.cursels.items, 0..) |*cursel_, i| if (cursel_.*) |*cursel|
|
||
if (cursel != exclude)
|
||
if (!cursel.nudge_delete(nudge)) {
|
||
self.cursels.items[i] = null;
|
||
};
|
||
for (self.matches.items, 0..) |*match_, i| if (match_.*) |*match|
|
||
if (!match.nudge_delete(nudge)) {
|
||
self.matches.items[i] = null;
|
||
};
|
||
}
|
||
|
||
fn delete_selection(self: *Self, root: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var sel: Selection = cursel.selection orelse return error.Stop;
|
||
sel.normalize();
|
||
cursel.cursor = sel.begin;
|
||
cursel.selection = null;
|
||
var size: usize = 0;
|
||
const root_ = try root.delete_range(sel, allocator, &size, self.metrics);
|
||
self.nudge_delete(sel, cursel, size);
|
||
return root_;
|
||
}
|
||
|
||
fn delete_to(self: *Self, move: cursor_operator_const, root_: Buffer.Root, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var all_stop = true;
|
||
var root = root_;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
if (cursel.selection) |_| {
|
||
root = self.delete_selection(root, cursel, allocator) catch continue;
|
||
all_stop = false;
|
||
continue;
|
||
}
|
||
with_selection_const(root, move, cursel, self.metrics) catch continue;
|
||
root = self.delete_selection(root, cursel, allocator) catch continue;
|
||
all_stop = false;
|
||
};
|
||
|
||
if (all_stop)
|
||
return error.Stop;
|
||
return root;
|
||
}
|
||
|
||
const cursor_predicate = *const fn (root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) bool;
|
||
const cursor_operator_const = *const fn (root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void;
|
||
const cursor_operator_const_arg = *const fn (root: Buffer.Root, cursor: *Cursor, ctx: Context, metrics: Buffer.Metrics) error{Stop}!void;
|
||
const cursor_view_operator_const = *const fn (root: Buffer.Root, cursor: *Cursor, view: *const View, metrics: Buffer.Metrics) error{Stop}!void;
|
||
const cursel_operator_const = *const fn (root: Buffer.Root, cursel: *CurSel) error{Stop}!void;
|
||
const cursor_operator = *const fn (root: Buffer.Root, cursor: *Cursor, allocator: Allocator) error{Stop}!Buffer.Root;
|
||
const cursel_operator = *const fn (root: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root;
|
||
const cursel_operator_mut = *const fn (self: *Self, root: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root;
|
||
|
||
fn is_not_word_char(c: []const u8) bool {
|
||
if (c.len == 0) return true;
|
||
return switch (c[0]) {
|
||
' ' => true,
|
||
'=' => true,
|
||
'"' => true,
|
||
'\'' => true,
|
||
'\t' => true,
|
||
'\n' => true,
|
||
'/' => true,
|
||
'\\' => true,
|
||
'*' => true,
|
||
':' => true,
|
||
'.' => true,
|
||
',' => true,
|
||
'(' => true,
|
||
')' => true,
|
||
'{' => true,
|
||
'}' => true,
|
||
'[' => true,
|
||
']' => true,
|
||
';' => true,
|
||
'|' => true,
|
||
'!' => true,
|
||
'?' => true,
|
||
'&' => 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, metrics: Buffer.Metrics) bool {
|
||
return cursor.test_at(root, is_word_char, metrics);
|
||
}
|
||
|
||
fn is_non_word_char_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
|
||
return cursor.test_at(root, is_not_word_char, metrics);
|
||
}
|
||
|
||
fn is_word_boundary_left(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
|
||
if (cursor.col == 0)
|
||
return true;
|
||
if (is_non_word_char_at_cursor(root, cursor, metrics))
|
||
return false;
|
||
var next = cursor.*;
|
||
next.move_left(root, metrics) catch return true;
|
||
if (is_non_word_char_at_cursor(root, &next, metrics))
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
fn is_non_word_boundary_left(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
|
||
if (cursor.col == 0)
|
||
return true;
|
||
if (is_word_char_at_cursor(root, cursor, metrics))
|
||
return false;
|
||
var next = cursor.*;
|
||
next.move_left(root, metrics) catch return true;
|
||
if (is_word_char_at_cursor(root, &next, metrics))
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
fn is_word_boundary_right(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
|
||
const line_width = root.line_width(cursor.row, metrics) catch return true;
|
||
if (cursor.col >= line_width)
|
||
return true;
|
||
if (is_non_word_char_at_cursor(root, cursor, metrics))
|
||
return false;
|
||
var next = cursor.*;
|
||
next.move_right(root, metrics) catch return true;
|
||
if (is_non_word_char_at_cursor(root, &next, metrics))
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
fn is_non_word_boundary_right(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
|
||
const line_width = root.line_width(cursor.row, metrics) catch return true;
|
||
if (cursor.col >= line_width)
|
||
return true;
|
||
if (is_word_char_at_cursor(root, cursor, metrics))
|
||
return false;
|
||
var next = cursor.*;
|
||
next.move_right(root, metrics) catch return true;
|
||
if (is_word_char_at_cursor(root, &next, metrics))
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
fn is_eol_left(_: Buffer.Root, cursor: *const Cursor, _: Buffer.Metrics) bool {
|
||
if (cursor.col == 0)
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
fn is_eol_right(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
|
||
const line_width = root.line_width(cursor.row, metrics) catch return true;
|
||
if (cursor.col >= line_width)
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
fn is_eol_right_vim(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
|
||
const line_width = root.line_width(cursor.row, metrics) catch return true;
|
||
if (line_width == 0) return true;
|
||
if (cursor.col >= line_width - 1)
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
fn move_cursor_left(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
|
||
try cursor.move_left(root, metrics);
|
||
}
|
||
|
||
fn move_cursor_left_until(root: Buffer.Root, cursor: *Cursor, pred: cursor_predicate, metrics: Buffer.Metrics) void {
|
||
while (!pred(root, cursor, metrics))
|
||
move_cursor_left(root, cursor, metrics) catch return;
|
||
}
|
||
|
||
fn move_cursor_left_unless(root: Buffer.Root, cursor: *Cursor, pred: cursor_predicate, metrics: Buffer.Metrics) void {
|
||
if (!pred(root, cursor, metrics))
|
||
move_cursor_left(root, cursor, metrics) catch return;
|
||
}
|
||
|
||
fn move_cursor_begin(_: Buffer.Root, cursor: *Cursor, _: Buffer.Metrics) !void {
|
||
cursor.move_begin();
|
||
}
|
||
|
||
fn smart_move_cursor_begin(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) !void {
|
||
const first = find_first_non_ws(root, cursor.row, metrics);
|
||
return if (cursor.col == first) cursor.move_begin() else cursor.move_to(root, cursor.row, first, metrics);
|
||
}
|
||
|
||
fn move_cursor_right(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
|
||
try cursor.move_right(root, metrics);
|
||
}
|
||
|
||
fn move_cursor_right_until(root: Buffer.Root, cursor: *Cursor, pred: cursor_predicate, metrics: Buffer.Metrics) void {
|
||
while (!pred(root, cursor, metrics))
|
||
move_cursor_right(root, cursor, metrics) catch return;
|
||
}
|
||
|
||
fn move_cursor_right_unless(root: Buffer.Root, cursor: *Cursor, pred: cursor_predicate, metrics: Buffer.Metrics) void {
|
||
if (!pred(root, cursor, metrics))
|
||
move_cursor_right(root, cursor, metrics) catch return;
|
||
}
|
||
|
||
fn move_cursor_end(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) !void {
|
||
cursor.move_end(root, metrics);
|
||
}
|
||
|
||
fn move_cursor_up(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) !void {
|
||
try cursor.move_up(root, metrics);
|
||
}
|
||
|
||
fn move_cursor_down(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) !void {
|
||
try cursor.move_down(root, metrics);
|
||
}
|
||
|
||
fn move_cursor_buffer_begin(_: Buffer.Root, cursor: *Cursor, _: Buffer.Metrics) !void {
|
||
cursor.move_buffer_begin();
|
||
}
|
||
|
||
fn move_cursor_buffer_end(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) !void {
|
||
cursor.move_buffer_end(root, metrics);
|
||
}
|
||
|
||
fn move_cursor_page_up(root: Buffer.Root, cursor: *Cursor, view: *const View, metrics: Buffer.Metrics) !void {
|
||
cursor.move_page_up(root, view, metrics);
|
||
}
|
||
|
||
fn move_cursor_page_down(root: Buffer.Root, cursor: *Cursor, view: *const View, metrics: Buffer.Metrics) !void {
|
||
cursor.move_page_down(root, view, metrics);
|
||
}
|
||
|
||
pub fn primary_click(self: *Self, y: c_int, x: c_int) !void {
|
||
if (self.fast_scroll)
|
||
try self.push_cursor()
|
||
else
|
||
self.cancel_all_selections();
|
||
const primary = self.get_primary();
|
||
primary.selection = null;
|
||
self.selection_mode = .char;
|
||
try self.send_editor_jump_source();
|
||
const root = self.buf_root() catch return;
|
||
primary.cursor.move_abs(root, &self.view, @intCast(y), @intCast(x), self.metrics) catch return;
|
||
self.clamp_mouse();
|
||
try self.send_editor_jump_destination();
|
||
if (self.jump_mode) try self.goto_definition(.{});
|
||
}
|
||
|
||
pub fn primary_double_click(self: *Self, y: c_int, x: c_int) !void {
|
||
const primary = self.get_primary();
|
||
primary.selection = null;
|
||
self.selection_mode = .word;
|
||
const root = self.buf_root() catch return;
|
||
primary.cursor.move_abs(root, &self.view, @intCast(y), @intCast(x), self.metrics) catch return;
|
||
_ = try self.select_word_at_cursor(primary);
|
||
self.clamp_mouse();
|
||
}
|
||
|
||
pub fn primary_triple_click(self: *Self, y: c_int, x: c_int) !void {
|
||
const primary = self.get_primary();
|
||
primary.selection = null;
|
||
self.selection_mode = .line;
|
||
const root = self.buf_root() catch return;
|
||
primary.cursor.move_abs(root, &self.view, @intCast(y), @intCast(x), self.metrics) catch return;
|
||
try self.select_line_at_cursor(primary);
|
||
self.clamp_mouse();
|
||
}
|
||
|
||
pub fn primary_drag(self: *Self, y: c_int, x: c_int) void {
|
||
const y_ = if (y < 0) 0 else y;
|
||
const x_ = if (x < 0) 0 else x;
|
||
const primary = self.get_primary();
|
||
const sel = primary.enable_selection();
|
||
const root = self.buf_root() catch return;
|
||
sel.end.move_abs(root, &self.view, @intCast(y_), @intCast(x_), self.metrics) catch return;
|
||
switch (self.selection_mode) {
|
||
.char => {},
|
||
.word => if (sel.begin.right_of(sel.end))
|
||
with_selection_const(root, move_cursor_word_begin, primary, self.metrics) catch return
|
||
else
|
||
with_selection_const(root, move_cursor_word_end, primary, self.metrics) catch return,
|
||
.line => if (sel.begin.right_of(sel.end))
|
||
with_selection_const(root, move_cursor_begin, primary, self.metrics) catch return
|
||
else {
|
||
with_selection_const(root, move_cursor_end, primary, self.metrics) catch return;
|
||
with_selection_const(root, move_cursor_right, primary, self.metrics) catch return;
|
||
},
|
||
}
|
||
primary.cursor = sel.end;
|
||
primary.check_selection();
|
||
self.clamp_mouse();
|
||
}
|
||
|
||
pub fn drag_to(self: *Self, ctx: Context) Result {
|
||
var y: i32 = 0;
|
||
var x: i32 = 0;
|
||
if (!try ctx.args.match(.{ tp.extract(&y), tp.extract(&x) }))
|
||
return error.InvalidArgument;
|
||
return self.primary_drag(y, x);
|
||
}
|
||
pub const drag_to_meta = .{ .arguments = &.{ .integer, .integer } };
|
||
|
||
pub fn secondary_click(self: *Self, y: c_int, x: c_int) !void {
|
||
return self.primary_drag(y, x);
|
||
}
|
||
|
||
pub fn secondary_drag(self: *Self, y: c_int, x: c_int) !void {
|
||
return self.primary_drag(y, x);
|
||
}
|
||
|
||
fn get_animation_min_lag() f64 {
|
||
const ms: f64 = @floatFromInt(tui.current().config.animation_min_lag);
|
||
return @max(ms * 0.001, 0.001); // to seconds
|
||
}
|
||
|
||
fn get_animation_max_lag() f64 {
|
||
const ms: f64 = @floatFromInt(tui.current().config.animation_max_lag);
|
||
return @max(ms * 0.001, 0.001); // to seconds
|
||
}
|
||
|
||
fn update_animation_lag(self: *Self) void {
|
||
const ts = time.microTimestamp();
|
||
const tdiff = ts - self.animation_last_time;
|
||
const lag: f64 = @as(f64, @floatFromInt(tdiff)) / time.us_per_s;
|
||
self.animation_lag = @max(@min(lag, get_animation_max_lag()), get_animation_min_lag());
|
||
self.animation_last_time = ts;
|
||
// self.logger.print("update_lag: {d} {d:.2}", .{ lag, self.animation_lag }) catch {};
|
||
}
|
||
|
||
fn update_animation_step(self: *Self, dest: usize) void {
|
||
const steps_ = @max(dest, self.view.row) - @min(dest, self.view.row);
|
||
self.update_animation_lag();
|
||
const steps: f64 = @floatFromInt(steps_);
|
||
const frame_rate: f64 = @floatFromInt(self.animation_frame_rate);
|
||
const frame_time: f64 = 1.0 / frame_rate;
|
||
const step_frames = self.animation_lag / frame_time;
|
||
const step: f64 = steps / step_frames;
|
||
self.animation_step = @intFromFloat(step);
|
||
if (self.animation_step == 0) self.animation_step = 1;
|
||
}
|
||
|
||
fn update_scroll(self: *Self) void {
|
||
const step = self.animation_step;
|
||
const view = self.view.row;
|
||
const dest = self.scroll_dest;
|
||
if (view == dest) return;
|
||
var row = view;
|
||
if (view < dest) {
|
||
row += step;
|
||
if (dest < row) row = dest;
|
||
} else if (dest < view) {
|
||
row -= if (row < step) row else step;
|
||
if (row < dest) row = dest;
|
||
}
|
||
self.view.row = row;
|
||
}
|
||
|
||
fn update_scroll_dest_abs(self: *Self, dest: usize) void {
|
||
const root = self.buf_root() catch return;
|
||
const max_view = if (root.lines() <= scroll_cursor_min_border_distance) 0 else root.lines() - scroll_cursor_min_border_distance;
|
||
self.scroll_dest = @min(dest, max_view);
|
||
self.update_animation_step(dest);
|
||
}
|
||
|
||
fn scroll_up(self: *Self) void {
|
||
var dest: View = self.view;
|
||
dest.row = if (dest.row > scroll_step_small) dest.row - scroll_step_small else 0;
|
||
self.update_scroll_dest_abs(dest.row);
|
||
}
|
||
|
||
fn scroll_down(self: *Self) void {
|
||
var dest: View = self.view;
|
||
dest.row += scroll_step_small;
|
||
self.update_scroll_dest_abs(dest.row);
|
||
}
|
||
|
||
fn scroll_pageup(self: *Self) void {
|
||
var dest: View = self.view;
|
||
dest.row = if (dest.row > dest.rows) dest.row - dest.rows else 0;
|
||
self.update_scroll_dest_abs(dest.row);
|
||
}
|
||
|
||
fn scroll_pagedown(self: *Self) void {
|
||
var dest: View = self.view;
|
||
dest.row += dest.rows;
|
||
self.update_scroll_dest_abs(dest.row);
|
||
}
|
||
|
||
pub fn scroll_up_pageup(self: *Self, _: Context) Result {
|
||
if (self.fast_scroll)
|
||
self.scroll_pageup()
|
||
else
|
||
self.scroll_up();
|
||
}
|
||
pub const scroll_up_pageup_meta = .{};
|
||
|
||
pub fn scroll_down_pagedown(self: *Self, _: Context) Result {
|
||
if (self.fast_scroll)
|
||
self.scroll_pagedown()
|
||
else
|
||
self.scroll_down();
|
||
}
|
||
pub const scroll_down_pagedown_meta = .{};
|
||
|
||
pub fn scroll_to(self: *Self, row: usize) void {
|
||
self.update_scroll_dest_abs(row);
|
||
}
|
||
|
||
fn scroll_view_offset(self: *Self, offset: usize) void {
|
||
const primary = self.get_primary();
|
||
const row = if (primary.cursor.row > offset) primary.cursor.row - offset else 0;
|
||
self.update_scroll_dest_abs(row);
|
||
}
|
||
|
||
pub fn scroll_view_center(self: *Self, _: Context) Result {
|
||
return self.scroll_view_offset(self.view.rows / 2);
|
||
}
|
||
pub const scroll_view_center_meta = .{ .description = "Scroll cursor to center of view" };
|
||
|
||
pub fn scroll_view_center_cycle(self: *Self, _: Context) Result {
|
||
const cursor_row = self.get_primary().cursor.row;
|
||
return if (cursor_row == self.view.row + scroll_cursor_min_border_distance)
|
||
self.scroll_view_bottom(.{})
|
||
else if (cursor_row == self.view.row + self.view.rows / 2)
|
||
self.scroll_view_top(.{})
|
||
else
|
||
self.scroll_view_offset(self.view.rows / 2);
|
||
}
|
||
pub const scroll_view_center_cycle_meta = .{ .description = "Scroll cursor to center/top/bottom of view" };
|
||
|
||
pub fn scroll_view_top(self: *Self, _: Context) Result {
|
||
return self.scroll_view_offset(scroll_cursor_min_border_distance);
|
||
}
|
||
pub const scroll_view_top_meta = .{};
|
||
|
||
pub fn scroll_view_bottom(self: *Self, _: Context) Result {
|
||
return self.scroll_view_offset(if (self.view.rows > scroll_cursor_min_border_distance) self.view.rows - scroll_cursor_min_border_distance else 0);
|
||
}
|
||
pub const scroll_view_bottom_meta = .{};
|
||
|
||
fn set_clipboard(self: *Self, text: []const u8) void {
|
||
if (self.clipboard) |old|
|
||
self.allocator.free(old);
|
||
self.clipboard = text;
|
||
tui.current().rdr.copy_to_system_clipboard(text);
|
||
}
|
||
|
||
fn copy_selection(root: Buffer.Root, sel: Selection, text_allocator: Allocator, metrics: Buffer.Metrics) ![]const u8 {
|
||
var size: usize = 0;
|
||
_ = try root.get_range(sel, null, &size, null, metrics);
|
||
const buf__ = try text_allocator.alloc(u8, size);
|
||
return (try root.get_range(sel, buf__, null, null, metrics)).?;
|
||
}
|
||
|
||
pub fn get_selection(self: *const Self, sel: Selection, text_allocator: Allocator) ![]const u8 {
|
||
return copy_selection(try self.buf_root(), sel, text_allocator, self.metrics);
|
||
}
|
||
|
||
fn copy_word_at_cursor(self: *Self, text_allocator: Allocator) ![]const u8 {
|
||
const root = try self.buf_root();
|
||
const primary = self.get_primary();
|
||
const sel = if (primary.selection) |*sel| sel else try self.select_word_at_cursor(primary);
|
||
return try copy_selection(root, sel.*, text_allocator, self.metrics);
|
||
}
|
||
|
||
pub fn cut_selection(self: *Self, root: Buffer.Root, cursel: *CurSel) !struct { []const u8, Buffer.Root } {
|
||
return if (cursel.selection) |sel| ret: {
|
||
var old_selection: Selection = sel;
|
||
old_selection.normalize();
|
||
const cut_text = try copy_selection(root, sel, self.allocator, self.metrics);
|
||
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, metrics: Buffer.Metrics) !void {
|
||
try move_cursor_buffer_begin(root, &sel.begin, metrics);
|
||
try move_cursor_buffer_end(root, &sel.end, metrics);
|
||
}
|
||
|
||
fn insert(self: *Self, root: Buffer.Root, cursel: *CurSel, s: []const u8, allocator: Allocator) !Buffer.Root {
|
||
var root_ = if (cursel.selection) |_| try self.delete_selection(root, cursel, allocator) else root;
|
||
const cursor = &cursel.cursor;
|
||
const begin = cursel.cursor;
|
||
cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, self.metrics);
|
||
cursor.target = cursor.col;
|
||
self.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len);
|
||
return root_;
|
||
}
|
||
|
||
pub fn cut(self: *Self, _: Context) Result {
|
||
const primary = self.get_primary();
|
||
const b = self.buf_for_update() catch return;
|
||
var root = b.root;
|
||
if (self.cursels.items.len == 1)
|
||
if (primary.selection) |_| {} else {
|
||
const sel = primary.enable_selection();
|
||
try move_cursor_begin(root, &sel.begin, self.metrics);
|
||
try move_cursor_end(root, &sel.end, self.metrics);
|
||
try move_cursor_right(root, &sel.end, self.metrics);
|
||
};
|
||
var first = true;
|
||
var text = std.ArrayList(u8).init(self.allocator);
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
const cut_text, root = try self.cut_selection(root, cursel);
|
||
if (first) {
|
||
first = false;
|
||
} else {
|
||
try text.appendSlice("\n");
|
||
}
|
||
try text.appendSlice(cut_text);
|
||
};
|
||
try self.update_buf(root);
|
||
self.set_clipboard(text.items);
|
||
self.clamp();
|
||
}
|
||
pub const cut_meta = .{ .description = "Cut selection or current line to clipboard" };
|
||
|
||
pub fn copy(self: *Self, _: Context) Result {
|
||
const root = self.buf_root() catch return;
|
||
var first = true;
|
||
var text = std.ArrayList(u8).init(self.allocator);
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
if (cursel.selection) |sel| {
|
||
const copy_text = try copy_selection(root, sel, self.allocator, self.metrics);
|
||
if (first) {
|
||
first = false;
|
||
} else {
|
||
try text.appendSlice("\n");
|
||
}
|
||
try text.appendSlice(copy_text);
|
||
}
|
||
};
|
||
if (text.items.len > 0) {
|
||
if (text.items.len > 100) {
|
||
self.logger.print("copy:{s}...", .{std.fmt.fmtSliceEscapeLower(text.items[0..100])});
|
||
} else {
|
||
self.logger.print("copy:{s}", .{std.fmt.fmtSliceEscapeLower(text.items)});
|
||
}
|
||
self.set_clipboard(text.items);
|
||
}
|
||
}
|
||
pub const copy_meta = .{ .description = "Copy selection to clipboard" };
|
||
|
||
pub fn paste(self: *Self, ctx: Context) Result {
|
||
var text: []const u8 = undefined;
|
||
if (!(ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text)}))) {
|
||
if (self.clipboard) |text_| text = text_ else return;
|
||
}
|
||
self.logger.print("paste: {d} bytes", .{text.len});
|
||
const b = try self.buf_for_update();
|
||
var root = b.root;
|
||
if (self.cursels.items.len == 1) {
|
||
const primary = self.get_primary();
|
||
root = try self.insert(root, primary, text, b.allocator);
|
||
} else {
|
||
if (std.mem.indexOfScalar(u8, text, '\n')) |_| {
|
||
var pos: usize = 0;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
if (std.mem.indexOfScalarPos(u8, text, pos, '\n')) |next| {
|
||
root = try self.insert(root, cursel, text[pos..next], b.allocator);
|
||
pos = next + 1;
|
||
} else {
|
||
root = try self.insert(root, cursel, text[pos..], b.allocator);
|
||
pos = 0;
|
||
}
|
||
};
|
||
} else {
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
root = try self.insert(root, cursel, text, b.allocator);
|
||
};
|
||
}
|
||
}
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
self.need_render();
|
||
}
|
||
pub const paste_meta = .{ .description = "Paste from internal clipboard" };
|
||
|
||
pub fn delete_forward(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.delete_to(move_cursor_right, b.root, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const delete_forward_meta = .{ .description = "Delete next character" };
|
||
|
||
pub fn delete_backward(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.delete_to(move_cursor_left, b.root, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const delete_backward_meta = .{ .description = "Delete previous character" };
|
||
|
||
pub fn delete_word_left(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.delete_to(move_cursor_word_left_space, b.root, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const delete_word_left_meta = .{ .description = "Delete previous word" };
|
||
|
||
pub fn delete_word_right(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.delete_to(move_cursor_word_right_space, b.root, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const delete_word_right_meta = .{ .description = "Delete next word" };
|
||
|
||
pub fn delete_to_begin(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.delete_to(move_cursor_begin, b.root, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const delete_to_begin_meta = .{ .description = "Delete to beginning of line" };
|
||
|
||
pub fn delete_to_end(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.delete_to(move_cursor_end, b.root, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const delete_to_end_meta = .{ .description = "Delete to end of line" };
|
||
|
||
pub fn join_next_line(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
try self.with_cursors_const(b.root, move_cursor_end);
|
||
const root = try self.delete_to(move_cursor_right, b.root, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const join_next_line_meta = .{ .description = "Join next line" };
|
||
|
||
pub fn move_left(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const(root, move_cursor_left) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_left_meta = .{ .description = "Move cursor left" };
|
||
|
||
pub fn move_right(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const(root, move_cursor_right) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_right_meta = .{ .description = "Move cursor right" };
|
||
|
||
fn move_cursor_left_vim(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
|
||
move_cursor_left_unless(root, cursor, is_eol_left, metrics);
|
||
}
|
||
|
||
fn move_cursor_right_vim(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
|
||
move_cursor_right_unless(root, cursor, is_eol_right_vim, metrics);
|
||
}
|
||
|
||
pub fn move_left_vim(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const(root, move_cursor_left_vim) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_left_vim_meta = .{ .description = "Move cursor left (vim)" };
|
||
|
||
pub fn move_right_vim(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const(root, move_cursor_right_vim) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_right_vim_meta = .{ .description = "Move cursor right (vim)" };
|
||
|
||
fn move_cursor_word_begin(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
|
||
if (is_non_word_char_at_cursor(root, cursor, metrics)) {
|
||
move_cursor_left_until(root, cursor, is_word_boundary_right, metrics);
|
||
try move_cursor_right(root, cursor, metrics);
|
||
} else {
|
||
move_cursor_left_until(root, cursor, is_word_boundary_left, metrics);
|
||
}
|
||
}
|
||
|
||
fn move_cursor_word_end(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
|
||
if (is_non_word_char_at_cursor(root, cursor, metrics)) {
|
||
move_cursor_right_until(root, cursor, is_word_boundary_left, metrics);
|
||
try move_cursor_left(root, cursor, metrics);
|
||
} else {
|
||
move_cursor_right_until(root, cursor, is_word_boundary_right, metrics);
|
||
}
|
||
try move_cursor_right(root, cursor, metrics);
|
||
}
|
||
|
||
fn move_cursor_word_left(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
|
||
try move_cursor_left(root, cursor, metrics);
|
||
move_cursor_left_until(root, cursor, is_word_boundary_left, metrics);
|
||
}
|
||
|
||
fn move_cursor_word_left_space(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
|
||
try move_cursor_left(root, cursor, metrics);
|
||
var next = cursor.*;
|
||
next.move_left(root, metrics) catch
|
||
return move_cursor_left_until(root, cursor, is_word_boundary_left, metrics);
|
||
if (is_non_word_char_at_cursor(root, cursor, metrics) and is_non_word_char_at_cursor(root, &next, metrics))
|
||
move_cursor_left_until(root, cursor, is_non_word_boundary_left, metrics)
|
||
else
|
||
move_cursor_left_until(root, cursor, is_word_boundary_left, metrics);
|
||
}
|
||
|
||
pub fn move_cursor_word_right(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
|
||
move_cursor_right_until(root, cursor, is_word_boundary_right, metrics);
|
||
try move_cursor_right(root, cursor, metrics);
|
||
}
|
||
|
||
pub fn move_cursor_word_right_vim(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
|
||
try move_cursor_right(root, cursor, metrics);
|
||
move_cursor_right_until(root, cursor, is_word_boundary_left, metrics);
|
||
}
|
||
|
||
pub fn move_cursor_word_right_space(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
|
||
var next = cursor.*;
|
||
next.move_right(root, metrics) catch {
|
||
move_cursor_right_until(root, cursor, is_word_boundary_right, metrics);
|
||
try move_cursor_right(root, cursor, metrics);
|
||
return;
|
||
};
|
||
if (is_non_word_char_at_cursor(root, cursor, metrics) and is_non_word_char_at_cursor(root, &next, metrics))
|
||
move_cursor_right_until(root, cursor, is_non_word_boundary_right, metrics)
|
||
else
|
||
move_cursor_right_until(root, cursor, is_word_boundary_right, metrics);
|
||
try move_cursor_right(root, cursor, metrics);
|
||
}
|
||
|
||
pub fn move_word_left(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const(root, move_cursor_word_left) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_word_left_meta = .{ .description = "Move cursor left by word" };
|
||
|
||
pub fn move_word_right(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const(root, move_cursor_word_right) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_word_right_meta = .{ .description = "Move cursor right by word" };
|
||
|
||
pub fn move_word_right_vim(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const(root, move_cursor_word_right_vim) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_word_right_vim_meta = .{ .description = "Move cursor right by word (vim)" };
|
||
|
||
fn move_cursor_to_char_left(root: Buffer.Root, cursor: *Cursor, ctx: Context, metrics: Buffer.Metrics) 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, metrics);
|
||
while (true) {
|
||
const curr_egc, _, _ = root.ecg_at(cursor.row, cursor.col, metrics) catch return error.Stop;
|
||
if (std.mem.eql(u8, curr_egc, egc))
|
||
return;
|
||
if (is_eol_left(root, cursor, metrics))
|
||
return;
|
||
move_cursor_left(root, cursor, metrics) catch return error.Stop;
|
||
}
|
||
}
|
||
|
||
pub fn move_cursor_to_char_right(root: Buffer.Root, cursor: *Cursor, ctx: Context, metrics: Buffer.Metrics) 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, metrics);
|
||
while (true) {
|
||
const curr_egc, _, _ = root.ecg_at(cursor.row, cursor.col, metrics) catch return error.Stop;
|
||
if (std.mem.eql(u8, curr_egc, egc))
|
||
return;
|
||
if (is_eol_right(root, cursor, metrics))
|
||
return;
|
||
move_cursor_right(root, cursor, metrics) catch return error.Stop;
|
||
}
|
||
}
|
||
|
||
pub fn move_to_char_left(self: *Self, ctx: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const_arg(root, move_cursor_to_char_left, ctx) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_to_char_left_meta = .{ .arguments = &.{.integer} };
|
||
|
||
pub fn move_to_char_right(self: *Self, ctx: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const_arg(root, move_cursor_to_char_right, ctx) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_to_char_right_meta = .{ .arguments = &.{.integer} };
|
||
|
||
pub fn move_up(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const(root, move_cursor_up) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_up_meta = .{ .description = "Move cursor up" };
|
||
|
||
pub fn add_cursor_up(self: *Self, _: Context) Result {
|
||
try self.push_cursor();
|
||
const primary = self.get_primary();
|
||
const root = try self.buf_root();
|
||
move_cursor_up(root, &primary.cursor, self.metrics) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const add_cursor_up_meta = .{ .description = "Add cursor up" };
|
||
|
||
pub fn move_down(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const(root, move_cursor_down) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_down_meta = .{ .description = "Move cursor down" };
|
||
|
||
pub fn add_cursor_down(self: *Self, _: Context) Result {
|
||
try self.push_cursor();
|
||
const primary = self.get_primary();
|
||
const root = try self.buf_root();
|
||
move_cursor_down(root, &primary.cursor, self.metrics) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const add_cursor_down_meta = .{ .description = "Add cursor down" };
|
||
|
||
pub fn add_cursor_next_match(self: *Self, _: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
if (self.matches.items.len == 0) {
|
||
const root = self.buf_root() catch return;
|
||
self.with_cursors_const(root, move_cursor_word_begin) catch {};
|
||
try self.with_selections_const(root, move_cursor_word_end);
|
||
} else if (self.get_next_match(self.get_primary().cursor)) |match| {
|
||
try self.push_cursor();
|
||
const primary = self.get_primary();
|
||
const root = self.buf_root() catch return;
|
||
primary.selection = match.to_selection();
|
||
match.has_selection = true;
|
||
primary.cursor.move_to(root, match.end.row, match.end.col, self.metrics) catch return;
|
||
}
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const add_cursor_next_match_meta = .{ .description = "Add cursor at next highlighted match" };
|
||
|
||
pub fn add_cursor_all_matches(self: *Self, _: Context) Result {
|
||
if (self.matches.items.len == 0) return;
|
||
try self.send_editor_jump_source();
|
||
while (self.get_next_match(self.get_primary().cursor)) |match| {
|
||
try self.push_cursor();
|
||
const primary = self.get_primary();
|
||
const root = self.buf_root() catch return;
|
||
primary.selection = match.to_selection();
|
||
match.has_selection = true;
|
||
primary.cursor.move_to(root, match.end.row, match.end.col, self.metrics) catch return;
|
||
}
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const add_cursor_all_matches_meta = .{ .description = "Add cursors to all highlighted matches" };
|
||
|
||
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.metrics);
|
||
}
|
||
}
|
||
|
||
pub fn add_cursors_to_line_ends(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
const cursels = try self.cursels.toOwnedSlice();
|
||
defer self.cursels.allocator.free(cursels);
|
||
for (cursels) |*cursel_| if (cursel_.*) |*cursel|
|
||
try self.add_cursors_to_cursel_line_ends(root, cursel);
|
||
self.collapse_cursors();
|
||
self.clamp();
|
||
}
|
||
pub const add_cursors_to_line_ends_meta = .{ .description = "Add cursors to all lines in selection" };
|
||
|
||
fn pull_cursel_up(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var root = root_;
|
||
const saved = cursel.*;
|
||
const sel = cursel.expand_selection_to_line(root, self.metrics);
|
||
var sfa = std.heap.stackFallback(4096, self.allocator);
|
||
const cut_text = copy_selection(root, sel.*, sfa.get(), self.metrics) catch return error.Stop;
|
||
defer allocator.free(cut_text);
|
||
root = try self.delete_selection(root, cursel, allocator);
|
||
try cursel.cursor.move_up(root, self.metrics);
|
||
root = self.insert(root, cursel, cut_text, allocator) catch return error.Stop;
|
||
cursel.* = saved;
|
||
try cursel.cursor.move_up(root, self.metrics);
|
||
if (cursel.selection) |*sel_| {
|
||
try sel_.begin.move_up(root, self.metrics);
|
||
try sel_.end.move_up(root, self.metrics);
|
||
}
|
||
return root;
|
||
}
|
||
|
||
pub fn pull_up(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.with_cursels_mut(b.root, pull_cursel_up, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const pull_up_meta = .{ .description = "Pull line up" };
|
||
|
||
fn pull_cursel_down(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var root = root_;
|
||
const saved = cursel.*;
|
||
const sel = cursel.expand_selection_to_line(root, self.metrics);
|
||
var sfa = std.heap.stackFallback(4096, self.allocator);
|
||
const cut_text = copy_selection(root, sel.*, sfa.get(), self.metrics) catch return error.Stop;
|
||
defer allocator.free(cut_text);
|
||
root = try self.delete_selection(root, cursel, allocator);
|
||
try cursel.cursor.move_down(root, self.metrics);
|
||
root = self.insert(root, cursel, cut_text, allocator) catch return error.Stop;
|
||
cursel.* = saved;
|
||
try cursel.cursor.move_down(root, self.metrics);
|
||
if (cursel.selection) |*sel_| {
|
||
try sel_.begin.move_down(root, self.metrics);
|
||
try sel_.end.move_down(root, self.metrics);
|
||
}
|
||
return root;
|
||
}
|
||
|
||
pub fn pull_down(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.with_cursels_mut(b.root, pull_cursel_down, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const pull_down_meta = .{ .description = "Pull line down" };
|
||
|
||
fn dupe_cursel_up(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var root = root_;
|
||
const sel: Selection = if (cursel.selection) |sel_| sel_ else Selection.line_from_cursor(cursel.cursor, root, self.metrics);
|
||
cursel.selection = null;
|
||
var sfa = std.heap.stackFallback(4096, self.allocator);
|
||
const text = copy_selection(root, sel, sfa.get(), self.metrics) catch return error.Stop;
|
||
defer allocator.free(text);
|
||
cursel.cursor = sel.begin;
|
||
root = self.insert(root, cursel, text, allocator) catch return error.Stop;
|
||
cursel.selection = .{ .begin = sel.begin, .end = sel.end };
|
||
cursel.cursor = sel.begin;
|
||
return root;
|
||
}
|
||
|
||
pub fn dupe_up(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.with_cursels_mut(b.root, dupe_cursel_up, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const dupe_up_meta = .{ .description = "Duplicate line or selection up/backwards" };
|
||
|
||
fn dupe_cursel_down(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var root = root_;
|
||
const sel: Selection = if (cursel.selection) |sel_| sel_ else Selection.line_from_cursor(cursel.cursor, root, self.metrics);
|
||
cursel.selection = null;
|
||
var sfa = std.heap.stackFallback(4096, self.allocator);
|
||
const text = copy_selection(root, sel, sfa.get(), self.metrics) catch return error.Stop;
|
||
defer allocator.free(text);
|
||
cursel.cursor = sel.end;
|
||
root = self.insert(root, cursel, text, allocator) catch return error.Stop;
|
||
cursel.selection = .{ .begin = sel.end, .end = cursel.cursor };
|
||
return root;
|
||
}
|
||
|
||
pub fn dupe_down(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.with_cursels_mut(b.root, dupe_cursel_down, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const dupe_down_meta = .{ .description = "Duplicate line or selection down/forwards" };
|
||
|
||
fn toggle_cursel_prefix(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var root = root_;
|
||
const saved = cursel.*;
|
||
const sel = cursel.expand_selection_to_line(root, self.metrics);
|
||
var sfa = std.heap.stackFallback(4096, self.allocator);
|
||
const alloc = sfa.get();
|
||
const text = copy_selection(root, sel.*, alloc, self.metrics) catch return error.Stop;
|
||
defer allocator.free(text);
|
||
root = try self.delete_selection(root, cursel, allocator);
|
||
const new_text = text_manip.toggle_prefix_in_text(self.prefix, text, alloc) catch return error.Stop;
|
||
root = self.insert(root, cursel, new_text, allocator) catch return error.Stop;
|
||
cursel.* = saved;
|
||
return root;
|
||
}
|
||
|
||
pub fn toggle_prefix(self: *Self, ctx: Context) Result {
|
||
var prefix: []const u8 = undefined;
|
||
if (!try ctx.args.match(.{tp.extract(&prefix)}))
|
||
return;
|
||
@memcpy(self.prefix_buf[0..prefix.len], prefix);
|
||
self.prefix = self.prefix_buf[0..prefix.len];
|
||
const b = try self.buf_for_update();
|
||
const root = try self.with_cursels_mut(b.root, toggle_cursel_prefix, b.allocator);
|
||
try self.update_buf(root);
|
||
}
|
||
pub const toggle_prefix_meta = .{ .arguments = &.{.string} };
|
||
|
||
pub fn toggle_comment(self: *Self, _: Context) Result {
|
||
const comment = if (self.syntax) |syn| syn.file_type.comment else "//";
|
||
return self.toggle_prefix(command.fmt(.{comment}));
|
||
}
|
||
pub const toggle_comment_meta = .{ .description = "Toggle comment" };
|
||
|
||
fn indent_cursor(self: *Self, root: Buffer.Root, cursor: Cursor, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
const space = " ";
|
||
var cursel: CurSel = .{};
|
||
cursel.cursor = cursor;
|
||
const cols = self.indent_size - find_first_non_ws(root, cursel.cursor.row, self.metrics) % self.indent_size;
|
||
try move_cursor_begin(root, &cursel.cursor, self.metrics);
|
||
return self.insert(root, &cursel, space[0..cols], allocator) catch return error.Stop;
|
||
}
|
||
|
||
fn indent_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
if (cursel.selection) |*sel_| {
|
||
var root = root_;
|
||
var sel = sel_.*;
|
||
const sel_from_start = sel_.begin.col == 0;
|
||
sel.normalize();
|
||
while (sel.begin.row < sel.end.row) : (sel.begin.row += 1)
|
||
root = try self.indent_cursor(root, sel.begin, allocator);
|
||
if (sel.end.col > 0)
|
||
root = try self.indent_cursor(root, sel.end, allocator);
|
||
if (sel_from_start)
|
||
sel_.begin.col = 0;
|
||
return root;
|
||
} else return try self.indent_cursor(root_, cursel.cursor, allocator);
|
||
}
|
||
|
||
pub fn indent(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.with_cursels_mut(b.root, indent_cursel, b.allocator);
|
||
try self.update_buf(root);
|
||
}
|
||
pub const indent_meta = .{ .description = "Indent current line" };
|
||
|
||
fn unindent_cursor(self: *Self, root: Buffer.Root, cursor: *Cursor, cursor_protect: ?*Cursor, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var newroot = root;
|
||
var cursel: CurSel = .{};
|
||
cursel.cursor = cursor.*;
|
||
const first = find_first_non_ws(root, cursel.cursor.row, self.metrics);
|
||
if (first == 0) return error.Stop;
|
||
const off = first % self.indent_size;
|
||
const cols = if (off == 0) self.indent_size else off;
|
||
const sel = cursel.enable_selection();
|
||
sel.begin.move_begin();
|
||
try sel.end.move_to(root, sel.end.row, cols, self.metrics);
|
||
var saved = false;
|
||
if (cursor_protect) |cp| if (cp.row == cursor.row and cp.col < cols) {
|
||
cp.col = cols + 1;
|
||
saved = true;
|
||
};
|
||
newroot = try self.delete_selection(root, &cursel, allocator);
|
||
if (cursor_protect) |cp| if (saved) {
|
||
try cp.move_to(root, cp.row, 0, self.metrics);
|
||
cp.clamp_to_buffer(newroot, self.metrics);
|
||
};
|
||
return newroot;
|
||
}
|
||
|
||
fn unindent_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
if (cursel.selection) |sel_| {
|
||
var root = root_;
|
||
var sel = sel_;
|
||
sel.normalize();
|
||
while (sel.begin.row < sel.end.row) : (sel.begin.row += 1)
|
||
root = try self.unindent_cursor(root, &sel.begin, &cursel.cursor, allocator);
|
||
if (sel.end.col > 0)
|
||
root = try self.unindent_cursor(root, &sel.end, &cursel.cursor, allocator);
|
||
return root;
|
||
} else return self.unindent_cursor(root_, &cursel.cursor, &cursel.cursor, allocator);
|
||
}
|
||
|
||
fn restore_cursels(self: *Self) void {
|
||
self.cursels.clearAndFree();
|
||
self.cursels = self.cursels_saved.clone() catch return;
|
||
}
|
||
|
||
pub fn unindent(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
errdefer self.restore_cursels();
|
||
const cursor_count = self.cursels.items.len;
|
||
const root = try self.with_cursels_mut(b.root, unindent_cursel, b.allocator);
|
||
if (self.cursels.items.len != cursor_count)
|
||
self.restore_cursels();
|
||
try self.update_buf(root);
|
||
}
|
||
pub const unindent_meta = .{ .description = "Unindent current line" };
|
||
|
||
pub fn move_scroll_up(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const(root, move_cursor_up) catch {};
|
||
self.view.move_up() catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_scroll_up_meta = .{ .description = "Move and scroll up" };
|
||
|
||
pub fn move_scroll_down(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_const(root, move_cursor_down) catch {};
|
||
self.view.move_down(root) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const move_scroll_down_meta = .{ .description = "Move and scroll down" };
|
||
|
||
pub fn move_scroll_left(self: *Self, _: Context) Result {
|
||
self.view.move_left() catch {};
|
||
}
|
||
pub const move_scroll_left_meta = .{ .description = "Scroll left" };
|
||
|
||
pub fn move_scroll_right(self: *Self, _: Context) Result {
|
||
self.view.move_right() catch {};
|
||
}
|
||
pub const move_scroll_right_meta = .{ .description = "Scroll right" };
|
||
|
||
pub fn move_scroll_page_up(self: *Self, _: Context) Result {
|
||
if (self.screen_cursor(&self.get_primary().cursor)) |cursor| {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_and_view_const(root, move_cursor_page_up, &self.view) catch {};
|
||
const new_cursor_row = self.get_primary().cursor.row;
|
||
self.update_scroll_dest_abs(if (cursor.row > new_cursor_row) 0 else new_cursor_row - cursor.row);
|
||
} else {
|
||
return self.move_page_up(.{});
|
||
}
|
||
}
|
||
pub const move_scroll_page_up_meta = .{ .description = "Move and scroll page up" };
|
||
|
||
pub fn move_scroll_page_down(self: *Self, _: Context) Result {
|
||
if (self.screen_cursor(&self.get_primary().cursor)) |cursor| {
|
||
const root = try self.buf_root();
|
||
self.with_cursors_and_view_const(root, move_cursor_page_down, &self.view) catch {};
|
||
const new_cursor_row = self.get_primary().cursor.row;
|
||
self.update_scroll_dest_abs(if (cursor.row > new_cursor_row) 0 else new_cursor_row - cursor.row);
|
||
} else {
|
||
return self.move_page_down(.{});
|
||
}
|
||
}
|
||
pub const move_scroll_page_down_meta = .{ .description = "Move and scroll page down" };
|
||
|
||
pub fn smart_move_begin(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_cursors_const(root, smart_move_cursor_begin);
|
||
self.clamp();
|
||
}
|
||
pub const smart_move_begin_meta = .{ .description = "Move cursor to beginning of line (smart)" };
|
||
|
||
pub fn move_begin(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_cursors_const(root, move_cursor_begin);
|
||
self.clamp();
|
||
}
|
||
pub const move_begin_meta = .{ .description = "Move cursor to beginning of line" };
|
||
|
||
pub fn move_end(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_cursors_const(root, move_cursor_end);
|
||
self.clamp();
|
||
}
|
||
pub const move_end_meta = .{ .description = "Move cursor to end of line" };
|
||
|
||
pub fn move_page_up(self: *Self, _: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
const root = try self.buf_root();
|
||
try self.with_cursors_and_view_const(root, move_cursor_page_up, &self.view);
|
||
self.clamp();
|
||
}
|
||
pub const move_page_up_meta = .{ .description = "Move cursor page up" };
|
||
|
||
pub fn move_page_down(self: *Self, _: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
const root = try self.buf_root();
|
||
try self.with_cursors_and_view_const(root, move_cursor_page_down, &self.view);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const move_page_down_meta = .{ .description = "Move cursor page down" };
|
||
|
||
pub fn move_buffer_begin(self: *Self, _: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
self.cancel_all_selections();
|
||
self.get_primary().cursor.move_buffer_begin();
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const move_buffer_begin_meta = .{ .description = "Move cursor to start of file" };
|
||
|
||
pub fn move_buffer_end(self: *Self, _: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
self.cancel_all_selections();
|
||
const root = self.buf_root() catch return;
|
||
self.get_primary().cursor.move_buffer_end(root, self.metrics);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const move_buffer_end_meta = .{ .description = "Move cursor to end of file" };
|
||
|
||
pub fn cancel(self: *Self, _: Context) Result {
|
||
self.cancel_all_selections();
|
||
self.cancel_all_matches();
|
||
}
|
||
pub const cancel_meta = .{ .description = "Cancel current action" };
|
||
|
||
pub fn select_up(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_up);
|
||
self.clamp();
|
||
}
|
||
pub const select_up_meta = .{ .description = "Select up" };
|
||
|
||
pub fn select_down(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_down);
|
||
self.clamp();
|
||
}
|
||
pub const select_down_meta = .{ .description = "Select down" };
|
||
|
||
pub fn select_scroll_up(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_up);
|
||
self.view.move_up() catch {};
|
||
self.clamp();
|
||
}
|
||
pub const select_scroll_up_meta = .{ .description = "Select and scroll up" };
|
||
|
||
pub fn select_scroll_down(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_down);
|
||
self.view.move_down(root) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const select_scroll_down_meta = .{ .description = "Select and scroll down" };
|
||
|
||
pub fn select_left(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_left);
|
||
self.clamp();
|
||
}
|
||
pub const select_left_meta = .{ .description = "Select left" };
|
||
|
||
pub fn select_right(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_right);
|
||
self.clamp();
|
||
}
|
||
pub const select_right_meta = .{ .description = "Select right" };
|
||
|
||
pub fn select_word_left(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_word_left);
|
||
self.clamp();
|
||
}
|
||
pub const select_word_left_meta = .{ .description = "Select left by word" };
|
||
|
||
pub fn select_word_right(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_word_right);
|
||
self.clamp();
|
||
}
|
||
pub const select_word_right_meta = .{ .description = "Select right by word" };
|
||
|
||
pub fn select_word_begin(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_word_begin);
|
||
self.clamp();
|
||
}
|
||
pub const select_word_begin_meta = .{ .description = "Select to beginning of word" };
|
||
|
||
pub fn select_word_end(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_word_end);
|
||
self.clamp();
|
||
}
|
||
pub const select_word_end_meta = .{ .description = "Select to end of word" };
|
||
|
||
pub fn select_to_char_left(self: *Self, ctx: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_selections_const_arg(root, move_cursor_to_char_left, ctx) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const select_to_char_left_meta = .{ .arguments = &.{.integer} };
|
||
|
||
pub fn select_to_char_right(self: *Self, ctx: Context) Result {
|
||
const root = try self.buf_root();
|
||
self.with_selections_const_arg(root, move_cursor_to_char_right, ctx) catch {};
|
||
self.clamp();
|
||
}
|
||
pub const select_to_char_right_meta = .{ .arguments = &.{.integer} };
|
||
|
||
pub fn select_begin(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_begin);
|
||
self.clamp();
|
||
}
|
||
pub const select_begin_meta = .{ .description = "Select to beginning of line" };
|
||
|
||
pub fn smart_select_begin(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, smart_move_cursor_begin);
|
||
self.clamp();
|
||
}
|
||
pub const smart_select_begin_meta = .{ .description = "Select to beginning of line (smart)" };
|
||
|
||
pub fn select_end(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_end);
|
||
self.clamp();
|
||
}
|
||
pub const select_end_meta = .{ .description = "Select to end of line" };
|
||
|
||
pub fn select_buffer_begin(self: *Self, _: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_buffer_begin);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const select_buffer_begin_meta = .{ .description = "Select to start of file" };
|
||
|
||
pub fn select_buffer_end(self: *Self, _: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
const root = try self.buf_root();
|
||
try self.with_selections_const(root, move_cursor_buffer_end);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const select_buffer_end_meta = .{ .description = "Select to end of file" };
|
||
|
||
pub fn select_page_up(self: *Self, _: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
const root = try self.buf_root();
|
||
try self.with_selections_and_view_const(root, move_cursor_page_up, &self.view);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const select_page_up_meta = .{ .description = "Select page up" };
|
||
|
||
pub fn select_page_down(self: *Self, _: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
const root = try self.buf_root();
|
||
try self.with_selections_and_view_const(root, move_cursor_page_down, &self.view);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const select_page_down_meta = .{ .description = "Select page down" };
|
||
|
||
pub fn select_all(self: *Self, _: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
self.cancel_all_selections();
|
||
const primary = self.get_primary();
|
||
const sel = primary.enable_selection();
|
||
const root = try self.buf_root();
|
||
try expand_selection_to_all(root, sel, self.metrics);
|
||
primary.cursor = sel.end;
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const select_all_meta = .{ .description = "Select all" };
|
||
|
||
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.metrics);
|
||
try move_cursor_word_end(root, &sel.end, self.metrics);
|
||
cursel.cursor = sel.end;
|
||
return sel;
|
||
}
|
||
|
||
fn select_line_at_cursor(self: *Self, cursel: *CurSel) !void {
|
||
const root = try self.buf_root();
|
||
const sel = cursel.enable_selection();
|
||
sel.normalize();
|
||
try move_cursor_begin(root, &sel.begin, self.metrics);
|
||
try move_cursor_end(root, &sel.end, self.metrics);
|
||
cursel.cursor = sel.end;
|
||
}
|
||
|
||
fn selection_reverse(_: Buffer.Root, cursel: *CurSel) !void {
|
||
if (cursel.selection) |*sel| {
|
||
sel.reverse();
|
||
cursel.cursor = sel.end;
|
||
}
|
||
}
|
||
|
||
pub fn selections_reverse(self: *Self, _: Context) Result {
|
||
const root = try self.buf_root();
|
||
try self.with_cursels_const(root, selection_reverse);
|
||
self.clamp();
|
||
}
|
||
pub const selections_reverse_meta = .{ .description = "Reverse selection" };
|
||
|
||
fn node_at_selection(self: *Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node {
|
||
const syn = self.syntax orelse return error.Stop;
|
||
const node = try syn.node_at_point_range(.{
|
||
.start_point = .{
|
||
.row = @intCast(sel.begin.row),
|
||
.column = @intCast(try root.get_line_width_to_pos(sel.begin.row, sel.begin.col, metrics)),
|
||
},
|
||
.end_point = .{
|
||
.row = @intCast(sel.end.row),
|
||
.column = @intCast(try root.get_line_width_to_pos(sel.end.row, sel.end.col, metrics)),
|
||
},
|
||
.start_byte = 0,
|
||
.end_byte = 0,
|
||
});
|
||
if (node.isNull()) return error.Stop;
|
||
return node;
|
||
}
|
||
|
||
fn select_node_at_cursor(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
|
||
cursel.selection = null;
|
||
const sel = cursel.enable_selection().*;
|
||
return cursel.select_node(try self.node_at_selection(sel, root, metrics), root, metrics);
|
||
}
|
||
|
||
fn expand_selection_to_parent_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
|
||
const sel = cursel.enable_selection().*;
|
||
const node = try self.node_at_selection(sel, root, metrics);
|
||
if (node.isNull()) return error.Stop;
|
||
const parent = node.getParent();
|
||
if (parent.isNull()) return error.Stop;
|
||
return cursel.select_node(parent, root, metrics);
|
||
}
|
||
|
||
pub fn expand_selection(self: *Self, _: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
const root = try self.buf_root();
|
||
const cursel = self.get_primary();
|
||
cursel.check_selection();
|
||
try if (cursel.selection) |_|
|
||
self.expand_selection_to_parent_node(root, cursel, self.metrics)
|
||
else
|
||
self.select_node_at_cursor(root, cursel, self.metrics);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const expand_selection_meta = .{ .description = "Expand selection to AST parent node" };
|
||
|
||
fn shrink_selection_to_child_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
|
||
const sel = cursel.enable_selection().*;
|
||
const node = try self.node_at_selection(sel, root, metrics);
|
||
if (node.isNull() or node.getChildCount() == 0) return error.Stop;
|
||
const child = node.getChild(0);
|
||
if (child.isNull()) return error.Stop;
|
||
return cursel.select_node(child, root, metrics);
|
||
}
|
||
|
||
fn shrink_selection_to_named_child_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
|
||
const sel = cursel.enable_selection().*;
|
||
const node = try self.node_at_selection(sel, root, metrics);
|
||
if (node.isNull() or node.getNamedChildCount() == 0) return error.Stop;
|
||
const child = node.getNamedChild(0);
|
||
if (child.isNull()) return error.Stop;
|
||
return cursel.select_node(child, root, metrics);
|
||
}
|
||
|
||
pub fn shrink_selection(self: *Self, ctx: Context) Result {
|
||
var unnamed: bool = false;
|
||
_ = ctx.args.match(.{tp.extract(&unnamed)}) catch false;
|
||
try self.send_editor_jump_source();
|
||
const root = try self.buf_root();
|
||
const cursel = self.get_primary();
|
||
cursel.check_selection();
|
||
if (cursel.selection) |_|
|
||
try if (unnamed)
|
||
self.shrink_selection_to_child_node(root, cursel, self.metrics)
|
||
else
|
||
self.shrink_selection_to_named_child_node(root, cursel, self.metrics);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const shrink_selection_meta = .{ .description = "Shrink selection to first AST child node" };
|
||
|
||
fn select_next_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
|
||
const sel = cursel.enable_selection().*;
|
||
const node = try self.node_at_selection(sel, root, metrics);
|
||
if (node.isNull()) return error.Stop;
|
||
const sibling = syntax.Node.externs.ts_node_next_sibling(node);
|
||
if (sibling.isNull()) return error.Stop;
|
||
return cursel.select_node(sibling, root, metrics);
|
||
}
|
||
|
||
fn select_next_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
|
||
const sel = cursel.enable_selection().*;
|
||
const node = try self.node_at_selection(sel, root, metrics);
|
||
if (node.isNull()) return error.Stop;
|
||
const sibling = syntax.Node.externs.ts_node_next_named_sibling(node);
|
||
if (sibling.isNull()) return error.Stop;
|
||
return cursel.select_node(sibling, root, metrics);
|
||
}
|
||
|
||
pub fn select_next_sibling(self: *Self, ctx: Context) Result {
|
||
var unnamed: bool = false;
|
||
_ = ctx.args.match(.{tp.extract(&unnamed)}) catch false;
|
||
try self.send_editor_jump_source();
|
||
const root = try self.buf_root();
|
||
const cursel = self.get_primary();
|
||
cursel.check_selection();
|
||
if (cursel.selection) |_|
|
||
try if (unnamed)
|
||
self.select_next_sibling_node(root, cursel, self.metrics)
|
||
else
|
||
self.select_next_named_sibling_node(root, cursel, self.metrics);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const select_next_sibling_meta = .{ .description = "Move selection to next AST sibling node" };
|
||
|
||
fn select_prev_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
|
||
const sel = cursel.enable_selection().*;
|
||
const node = try self.node_at_selection(sel, root, metrics);
|
||
if (node.isNull()) return error.Stop;
|
||
const sibling = syntax.Node.externs.ts_node_prev_sibling(node);
|
||
if (sibling.isNull()) return error.Stop;
|
||
return cursel.select_node(sibling, root, metrics);
|
||
}
|
||
|
||
fn select_prev_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
|
||
const sel = cursel.enable_selection().*;
|
||
const node = try self.node_at_selection(sel, root, metrics);
|
||
if (node.isNull()) return error.Stop;
|
||
const sibling = syntax.Node.externs.ts_node_prev_named_sibling(node);
|
||
if (sibling.isNull()) return error.Stop;
|
||
return cursel.select_node(sibling, root, metrics);
|
||
}
|
||
|
||
pub fn select_prev_sibling(self: *Self, ctx: Context) Result {
|
||
var unnamed: bool = false;
|
||
_ = ctx.args.match(.{tp.extract(&unnamed)}) catch false;
|
||
try self.send_editor_jump_source();
|
||
const root = try self.buf_root();
|
||
const cursel = self.get_primary();
|
||
cursel.check_selection();
|
||
if (cursel.selection) |_|
|
||
try if (unnamed)
|
||
self.select_prev_sibling_node(root, cursel, self.metrics)
|
||
else
|
||
self.select_prev_named_sibling_node(root, cursel, self.metrics);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const select_prev_sibling_meta = .{ .description = "Move selection to previous AST sibling node" };
|
||
|
||
pub fn insert_chars(self: *Self, ctx: Context) Result {
|
||
var chars: []const u8 = undefined;
|
||
if (!try ctx.args.match(.{tp.extract(&chars)}))
|
||
return error.InvalidArgument;
|
||
const b = try self.buf_for_update();
|
||
var root = b.root;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
root = try self.insert(root, cursel, chars, b.allocator);
|
||
};
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const insert_chars_meta = .{ .arguments = &.{.string} };
|
||
|
||
pub fn insert_line(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
var root = b.root;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
root = try self.insert(root, cursel, "\n", b.allocator);
|
||
};
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const insert_line_meta = .{ .description = "Insert line" };
|
||
|
||
pub fn smart_insert_line(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
var root = b.root;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
var leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.metrics), cursel.cursor.col);
|
||
var sfa = std.heap.stackFallback(512, self.allocator);
|
||
const allocator = sfa.get();
|
||
var stream = std.ArrayList(u8).init(allocator);
|
||
defer stream.deinit();
|
||
var writer = stream.writer();
|
||
_ = try writer.write("\n");
|
||
while (leading_ws > 0) : (leading_ws -= 1)
|
||
_ = try writer.write(" ");
|
||
root = try self.insert(root, cursel, stream.items, b.allocator);
|
||
};
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const smart_insert_line_meta = .{ .description = "Insert line (smart)" };
|
||
|
||
pub fn insert_line_before(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
var root = b.root;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
try move_cursor_begin(root, &cursel.cursor, self.metrics);
|
||
root = try self.insert(root, cursel, "\n", b.allocator);
|
||
try move_cursor_left(root, &cursel.cursor, self.metrics);
|
||
};
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const insert_line_before_meta = .{ .description = "Insert line before" };
|
||
|
||
pub fn smart_insert_line_before(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
var root = b.root;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
var leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.metrics), cursel.cursor.col);
|
||
try move_cursor_begin(root, &cursel.cursor, self.metrics);
|
||
root = try self.insert(root, cursel, "\n", b.allocator);
|
||
try move_cursor_left(root, &cursel.cursor, self.metrics);
|
||
var sfa = std.heap.stackFallback(512, self.allocator);
|
||
const allocator = sfa.get();
|
||
var stream = std.ArrayList(u8).init(allocator);
|
||
defer stream.deinit();
|
||
var writer = stream.writer();
|
||
while (leading_ws > 0) : (leading_ws -= 1)
|
||
_ = try writer.write(" ");
|
||
if (stream.items.len > 0)
|
||
root = try self.insert(root, cursel, stream.items, b.allocator);
|
||
};
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const smart_insert_line_before_meta = .{ .description = "Insert line before (smart)" };
|
||
|
||
pub fn insert_line_after(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
var root = b.root;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
try move_cursor_end(root, &cursel.cursor, self.metrics);
|
||
root = try self.insert(root, cursel, "\n", b.allocator);
|
||
};
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const insert_line_after_meta = .{ .description = "Insert line after" };
|
||
|
||
pub fn smart_insert_line_after(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
var root = b.root;
|
||
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
|
||
var leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.metrics), cursel.cursor.col);
|
||
try move_cursor_end(root, &cursel.cursor, self.metrics);
|
||
var sfa = std.heap.stackFallback(512, self.allocator);
|
||
const allocator = sfa.get();
|
||
var stream = std.ArrayList(u8).init(allocator);
|
||
defer stream.deinit();
|
||
var writer = stream.writer();
|
||
_ = try writer.write("\n");
|
||
while (leading_ws > 0) : (leading_ws -= 1)
|
||
_ = try writer.write(" ");
|
||
if (stream.items.len > 0)
|
||
root = try self.insert(root, cursel, stream.items, b.allocator);
|
||
};
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const smart_insert_line_after_meta = .{ .description = "Insert line after (smart)" };
|
||
|
||
pub fn enable_fast_scroll(self: *Self, _: Context) Result {
|
||
self.fast_scroll = true;
|
||
}
|
||
pub const enable_fast_scroll_meta = .{ .description = "Enable fast scroll mode" };
|
||
|
||
pub fn disable_fast_scroll(self: *Self, _: Context) Result {
|
||
self.fast_scroll = false;
|
||
}
|
||
pub const disable_fast_scroll_meta = .{};
|
||
|
||
pub fn enable_jump_mode(self: *Self, _: Context) Result {
|
||
self.jump_mode = true;
|
||
tui.current().rdr.request_mouse_cursor_pointer(true);
|
||
}
|
||
pub const enable_jump_mode_meta = .{ .description = "Enable jump/hover mode" };
|
||
|
||
pub fn disable_jump_mode(self: *Self, _: Context) Result {
|
||
self.jump_mode = false;
|
||
tui.current().rdr.request_mouse_cursor_text(true);
|
||
}
|
||
pub const disable_jump_mode_meta = .{};
|
||
|
||
fn update_syntax(self: *Self) !void {
|
||
const root = try self.buf_root();
|
||
const eol_mode = try self.buf_eol_mode();
|
||
if (self.syntax_last_rendered_root == root)
|
||
return;
|
||
var kind: enum { full, incremental, none } = .none;
|
||
var edit_count: usize = 0;
|
||
const start_time = std.time.milliTimestamp();
|
||
if (self.syntax) |syn| {
|
||
if (self.syntax_no_render) {
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor reset syntax" });
|
||
defer frame.deinit();
|
||
syn.reset();
|
||
self.syntax_last_rendered_root = null;
|
||
return;
|
||
}
|
||
if (!self.syntax_incremental_reparse)
|
||
self.syntax_refresh_full = true;
|
||
if (self.syntax_last_rendered_root == null)
|
||
self.syntax_refresh_full = true;
|
||
var content_ = std.ArrayList(u8).init(self.allocator);
|
||
defer content_.deinit();
|
||
{
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor store syntax" });
|
||
defer frame.deinit();
|
||
try root.store(content_.writer(), eol_mode);
|
||
}
|
||
const content = try content_.toOwnedSliceSentinel(0);
|
||
defer self.allocator.free(content);
|
||
if (self.syntax_refresh_full) {
|
||
{
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor reset syntax" });
|
||
defer frame.deinit();
|
||
syn.reset();
|
||
}
|
||
{
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor refresh_full syntax" });
|
||
defer frame.deinit();
|
||
try syn.refresh_full(content);
|
||
}
|
||
kind = .full;
|
||
self.syntax_last_rendered_root = root;
|
||
self.syntax_refresh_full = false;
|
||
} else {
|
||
if (self.syntax_last_rendered_root) |root_src| {
|
||
self.syntax_last_rendered_root = null;
|
||
var old_content = std.ArrayList(u8).init(self.allocator);
|
||
defer old_content.deinit();
|
||
{
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor store syntax" });
|
||
defer frame.deinit();
|
||
try root_src.store(old_content.writer(), eol_mode);
|
||
}
|
||
{
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor diff syntax" });
|
||
defer frame.deinit();
|
||
const diff = @import("diff");
|
||
const edits = try diff.diff(self.allocator, content, old_content.items);
|
||
defer self.allocator.free(edits);
|
||
for (edits) |edit|
|
||
syntax_process_edit(syn, edit);
|
||
edit_count = edits.len;
|
||
}
|
||
{
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor refresh syntax" });
|
||
defer frame.deinit();
|
||
try syn.refresh_from_string(content);
|
||
}
|
||
self.syntax_last_rendered_root = root;
|
||
kind = .incremental;
|
||
}
|
||
}
|
||
} else {
|
||
var content = std.ArrayList(u8).init(self.allocator);
|
||
defer content.deinit();
|
||
try root.store(content.writer(), eol_mode);
|
||
self.syntax = syntax.create_guess_file_type(self.allocator, content.items, self.file_path) catch |e| switch (e) {
|
||
error.NotFound => null,
|
||
else => return e,
|
||
};
|
||
if (!self.syntax_no_render) {
|
||
if (self.syntax) |syn| {
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor parse syntax" });
|
||
defer frame.deinit();
|
||
try syn.refresh_full(content.items);
|
||
self.syntax_last_rendered_root = root;
|
||
}
|
||
}
|
||
}
|
||
const end_time = std.time.milliTimestamp();
|
||
if (kind == .full or kind == .incremental) {
|
||
const update_time = end_time - start_time;
|
||
self.syntax_incremental_reparse = end_time - start_time > syntax_full_reparse_time_limit;
|
||
if (self.syntax_report_timing)
|
||
self.logger.print("syntax update {s} time: {d}ms ({d} edits)", .{ @tagName(kind), update_time, edit_count });
|
||
}
|
||
}
|
||
|
||
fn syntax_process_edit(syn: *syntax, edit: @import("diff").Diff) void {
|
||
switch (edit.kind) {
|
||
.insert => syn.edit(.{
|
||
.start_byte = @intCast(edit.start),
|
||
.old_end_byte = @intCast(edit.start),
|
||
.new_end_byte = @intCast(edit.start + edit.bytes.len),
|
||
.start_point = .{ .row = 0, .column = 0 },
|
||
.old_end_point = .{ .row = 0, .column = 0 },
|
||
.new_end_point = .{ .row = 0, .column = 0 },
|
||
}),
|
||
.delete => syn.edit(.{
|
||
.start_byte = @intCast(edit.start),
|
||
.old_end_byte = @intCast(edit.start + edit.bytes.len),
|
||
.new_end_byte = @intCast(edit.start),
|
||
.start_point = .{ .row = 0, .column = 0 },
|
||
.old_end_point = .{ .row = 0, .column = 0 },
|
||
.new_end_point = .{ .row = 0, .column = 0 },
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn reset_syntax(self: *Self) void {
|
||
if (self.syntax) |_| self.syntax_refresh_full = true;
|
||
}
|
||
|
||
pub fn dump_current_line(self: *Self, _: Context) Result {
|
||
const root = self.buf_root() catch return;
|
||
const primary = self.get_primary();
|
||
var tree = std.ArrayList(u8).init(self.allocator);
|
||
defer tree.deinit();
|
||
root.debug_render_chunks(primary.cursor.row, &tree, self.metrics) catch |e|
|
||
return self.logger.print("line {d}: {any}", .{ primary.cursor.row, e });
|
||
self.logger.print("line {d}:{s}", .{ primary.cursor.row, std.fmt.fmtSliceEscapeLower(tree.items) });
|
||
}
|
||
pub const dump_current_line_meta = .{ .description = "Debug: dump current line" };
|
||
|
||
pub fn dump_current_line_tree(self: *Self, _: Context) Result {
|
||
const root = self.buf_root() catch return;
|
||
const primary = self.get_primary();
|
||
var tree = std.ArrayList(u8).init(self.allocator);
|
||
defer tree.deinit();
|
||
root.debug_line_render_tree(primary.cursor.row, &tree) catch |e|
|
||
return self.logger.print("line {d} ast: {any}", .{ primary.cursor.row, e });
|
||
self.logger.print("line {d} ast:{s}", .{ primary.cursor.row, std.fmt.fmtSliceEscapeLower(tree.items) });
|
||
}
|
||
pub const dump_current_line_tree_meta = .{ .description = "Debug: dump current line (tree)" };
|
||
|
||
pub fn undo(self: *Self, _: Context) Result {
|
||
try self.restore_undo();
|
||
self.clamp();
|
||
}
|
||
pub const undo_meta = .{ .description = "Undo" };
|
||
|
||
pub fn redo(self: *Self, _: Context) Result {
|
||
try self.restore_redo();
|
||
self.clamp();
|
||
}
|
||
pub const redo_meta = .{ .description = "Redo" };
|
||
|
||
pub fn open_buffer_from_file(self: *Self, ctx: Context) Result {
|
||
var file_path: []const u8 = undefined;
|
||
if (ctx.args.match(.{tp.extract(&file_path)}) catch false) {
|
||
try self.open(file_path);
|
||
self.clamp();
|
||
} else return error.InvalidArgument;
|
||
}
|
||
pub const open_buffer_from_file_meta = .{ .arguments = &.{.string} };
|
||
|
||
pub fn open_scratch_buffer(self: *Self, ctx: Context) Result {
|
||
var file_path: []const u8 = undefined;
|
||
var content: []const u8 = undefined;
|
||
if (ctx.args.match(.{ tp.extract(&file_path), tp.extract(&content) }) catch false) {
|
||
try self.open_scratch(file_path, content);
|
||
self.clamp();
|
||
} else return error.InvalidArgument;
|
||
}
|
||
pub const open_scratch_buffer_meta = .{ .arguments = &.{ .string, .string } };
|
||
|
||
pub fn save_file(self: *Self, ctx: Context) Result {
|
||
var then = false;
|
||
var cmd: []const u8 = undefined;
|
||
var args: []const u8 = undefined;
|
||
if (ctx.args.match(.{ "then", .{ tp.extract(&cmd), tp.extract_cbor(&args) } }) catch false) {
|
||
then = true;
|
||
}
|
||
if (tui.current().config.enable_format_on_save) if (self.get_formatter()) |_| {
|
||
self.need_save_after_filter = .{ .then = if (then) .{ .cmd = cmd, .args = args } else null };
|
||
const primary = self.get_primary();
|
||
const sel = primary.selection;
|
||
primary.selection = null;
|
||
defer primary.selection = sel;
|
||
try self.format(.{});
|
||
return;
|
||
};
|
||
try self.save();
|
||
if (then)
|
||
return command.executeName(cmd, .{ .args = .{ .buf = args } });
|
||
}
|
||
pub const save_file_meta = .{ .description = "Save file" };
|
||
|
||
pub fn save_file_as(self: *Self, ctx: Context) Result {
|
||
var file_path: []const u8 = undefined;
|
||
if (ctx.args.match(.{tp.extract(&file_path)}) catch false) {
|
||
try self.save_as(file_path);
|
||
} else return error.InvalidArgument;
|
||
}
|
||
pub const save_file_as_meta = .{ .arguments = &.{.string} };
|
||
|
||
pub fn close_file(self: *Self, _: Context) Result {
|
||
self.cancel_all_selections();
|
||
try self.close();
|
||
}
|
||
pub const close_file_meta = .{ .description = "Close file" };
|
||
|
||
pub fn close_file_without_saving(self: *Self, _: Context) Result {
|
||
self.cancel_all_selections();
|
||
try self.close_dirty();
|
||
}
|
||
pub const close_file_without_saving_meta = .{ .description = "Close file without saving" };
|
||
|
||
pub fn find_query(self: *Self, ctx: Context) Result {
|
||
var query: []const u8 = undefined;
|
||
if (ctx.args.match(.{tp.extract(&query)}) catch false) {
|
||
try self.find_in_buffer(query);
|
||
self.clamp();
|
||
} else return error.InvalidArgument;
|
||
}
|
||
pub const find_query_meta = .{ .arguments = &.{.string} };
|
||
|
||
fn find_in(self: *Self, query: []const u8, comptime find_f: ripgrep.FindF, write_buffer: bool) !void {
|
||
const root = try self.buf_root();
|
||
self.cancel_all_matches();
|
||
if (std.mem.indexOfScalar(u8, query, '\n')) |_| return;
|
||
self.logger.print("find:{s}", .{std.fmt.fmtSliceEscapeLower(query)});
|
||
var rg = try find_f(self.allocator, query, "A");
|
||
defer rg.deinit();
|
||
if (write_buffer) {
|
||
var rg_buffer = rg.bufferedWriter();
|
||
try root.store(rg_buffer.writer());
|
||
try rg_buffer.flush();
|
||
}
|
||
}
|
||
|
||
pub fn push_find_history(self: *Self, query: []const u8) void {
|
||
if (query.len == 0) return;
|
||
const history = if (self.find_history) |*hist| hist else ret: {
|
||
self.find_history = std.ArrayList([]const u8).init(self.allocator);
|
||
break :ret &self.find_history.?;
|
||
};
|
||
for (history.items, 0..) |entry, i|
|
||
if (std.mem.eql(u8, entry, query))
|
||
self.allocator.free(history.orderedRemove(i));
|
||
const new = self.allocator.dupe(u8, query) catch return;
|
||
(history.addOne() catch return).* = new;
|
||
}
|
||
|
||
fn set_last_find_query(self: *Self, query: []const u8) void {
|
||
if (self.last_find_query) |last| {
|
||
if (query.ptr != last.ptr) {
|
||
self.allocator.free(last);
|
||
self.last_find_query = self.allocator.dupe(u8, query) catch return;
|
||
}
|
||
} else self.last_find_query = self.allocator.dupe(u8, query) catch return;
|
||
}
|
||
|
||
pub fn find_in_buffer(self: *Self, query: []const u8) !void {
|
||
self.set_last_find_query(query);
|
||
return self.find_in_buffer_sync(query);
|
||
}
|
||
|
||
fn find_in_buffer_sync(self: *Self, query: []const u8) !void {
|
||
const Ctx = struct {
|
||
matches: usize = 0,
|
||
self: *Self,
|
||
fn cb(ctx_: *anyopaque, begin_row: usize, begin_col: usize, end_row: usize, end_col: usize) error{Stop}!void {
|
||
const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_)));
|
||
ctx.matches += 1;
|
||
ctx.self.add_match_internal(begin_row, begin_col, end_row, end_col);
|
||
if (ctx.matches >= max_matches)
|
||
return error.Stop;
|
||
}
|
||
};
|
||
const root = try self.buf_root();
|
||
defer self.add_match_done();
|
||
var ctx: Ctx = .{ .self = self };
|
||
self.init_matches_update();
|
||
try root.find_all_ranges(query, &ctx, Ctx.cb, self.allocator);
|
||
}
|
||
|
||
fn find_in_buffer_async(self: *Self, query: []const u8) !void {
|
||
const finder = struct {
|
||
allocator: Allocator,
|
||
query: []const u8,
|
||
parent: tp.pid_ref,
|
||
root: Buffer.Root,
|
||
token: usize,
|
||
matches: Match.List,
|
||
|
||
const finder = @This();
|
||
|
||
fn start(fdr: *finder) tp.result {
|
||
fdr.find() catch {};
|
||
return tp.exit_normal();
|
||
}
|
||
|
||
fn find(fdr: *finder) !void {
|
||
const Ctx = struct {
|
||
matches: usize = 0,
|
||
fdr: *finder,
|
||
const Ctx = @This();
|
||
fn cb(ctx_: *anyopaque, begin_row: usize, begin_col: usize, end_row: usize, end_col: usize) error{Stop}!void {
|
||
const ctx = @as(*Ctx, @ptrCast(@alignCast(ctx_)));
|
||
ctx.matches += 1;
|
||
const match: Match = .{ .begin = .{ .row = begin_row, .col = begin_col }, .end = .{ .row = end_row, .col = end_col } };
|
||
(ctx.fdr.matches.addOne() catch return).* = match;
|
||
if (ctx.fdr.matches.items.len >= max_match_batch)
|
||
ctx.fdr.send_batch() catch return error.Stop;
|
||
if (ctx.matches >= max_matches)
|
||
return error.Stop;
|
||
}
|
||
};
|
||
defer fdr.parent.send(.{ "A", "done", fdr.token }) catch {};
|
||
defer fdr.allocator.free(fdr.query);
|
||
var ctx: Ctx = .{ .fdr = fdr };
|
||
try fdr.root.find_all_ranges(fdr.query, &ctx, Ctx.cb, fdr.a);
|
||
try fdr.send_batch();
|
||
}
|
||
|
||
fn send_batch(fdr: *finder) !void {
|
||
if (fdr.matches.items.len == 0)
|
||
return;
|
||
var buf: [max_match_batch * @sizeOf(Match)]u8 = undefined;
|
||
var stream = std.io.fixedBufferStream(&buf);
|
||
const writer = stream.writer();
|
||
try cbor.writeArrayHeader(writer, 4);
|
||
try cbor.writeValue(writer, "A");
|
||
try cbor.writeValue(writer, "batch");
|
||
try cbor.writeValue(writer, fdr.token);
|
||
try cbor.writeArrayHeader(writer, fdr.matches.items.len);
|
||
for (fdr.matches.items) |m_| if (m_) |m| {
|
||
try cbor.writeArray(writer, .{ m.begin.row, m.begin.col, m.end.row, m.end.col });
|
||
};
|
||
try fdr.parent.send_raw(.{ .buf = stream.getWritten() });
|
||
fdr.matches.clearRetainingCapacity();
|
||
}
|
||
};
|
||
self.init_matches_update();
|
||
const fdr = try self.allocator.create(finder);
|
||
fdr.* = .{
|
||
.a = self.allocator,
|
||
.query = try self.allocator.dupe(u8, query),
|
||
.parent = tp.self_pid(),
|
||
.root = try self.buf_root(),
|
||
.token = self.match_token,
|
||
.matches = Match.List.init(self.allocator),
|
||
};
|
||
const pid = try tp.spawn_link(self.allocator, fdr, finder.start, "editor.find");
|
||
pid.deinit();
|
||
}
|
||
|
||
pub fn find_in_buffer_ext(self: *Self, query: []const u8) !void {
|
||
return self.find_in(query, ripgrep.find_in_stdin, true);
|
||
}
|
||
|
||
pub fn add_match(self: *Self, m: tp.message) !void {
|
||
var begin_line: usize = undefined;
|
||
var begin_pos: usize = undefined;
|
||
var end_line: usize = undefined;
|
||
var end_pos: usize = undefined;
|
||
var batch_cbor: []const u8 = undefined;
|
||
if (try m.match(.{ "A", "done", self.match_token })) {
|
||
self.add_match_done();
|
||
} else if (try m.match(.{ tp.any, "batch", self.match_token, tp.extract_cbor(&batch_cbor) })) {
|
||
try self.add_match_batch(batch_cbor);
|
||
} else if (try m.match(.{ tp.any, self.match_token, tp.extract(&begin_line), tp.extract(&begin_pos), tp.extract(&end_line), tp.extract(&end_pos) })) {
|
||
self.add_match_internal(begin_line, begin_pos, end_line, end_pos);
|
||
} else if (try m.match(.{ tp.any, tp.extract(&begin_line), tp.extract(&begin_pos), tp.extract(&end_line), tp.extract(&end_pos) })) {
|
||
self.add_match_internal(begin_line, begin_pos, end_line, end_pos);
|
||
}
|
||
}
|
||
|
||
pub fn add_match_batch(self: *Self, cb: []const u8) !void {
|
||
var iter = cb;
|
||
var begin_line: usize = undefined;
|
||
var begin_pos: usize = undefined;
|
||
var end_line: usize = undefined;
|
||
var end_pos: usize = undefined;
|
||
var len = try cbor.decodeArrayHeader(&iter);
|
||
while (len > 0) : (len -= 1)
|
||
if (try cbor.matchValue(&iter, .{ tp.extract(&begin_line), tp.extract(&begin_pos), tp.extract(&end_line), tp.extract(&end_pos) })) {
|
||
self.add_match_internal(begin_line, begin_pos, end_line, end_pos);
|
||
} else return;
|
||
}
|
||
|
||
fn add_match_done(self: *Self) void {
|
||
if (self.matches.items.len > 0) {
|
||
if (self.find_operation) |op| {
|
||
self.find_operation = null;
|
||
switch (op) {
|
||
.goto_next_match => self.goto_next_match(.{}) catch {},
|
||
.goto_prev_match => self.goto_prev_match(.{}) catch {},
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
self.match_done_token = self.match_token;
|
||
self.need_render();
|
||
}
|
||
|
||
fn add_match_internal(self: *Self, begin_line_: usize, begin_pos_: usize, end_line_: usize, end_pos_: usize) void {
|
||
const root = self.buf_root() catch return;
|
||
const begin_line = begin_line_ - 1;
|
||
const end_line = end_line_ - 1;
|
||
const begin_pos = root.pos_to_width(begin_line, begin_pos_, self.metrics) catch return;
|
||
const end_pos = root.pos_to_width(end_line, end_pos_, self.metrics) catch return;
|
||
var match: Match = .{ .begin = .{ .row = begin_line, .col = begin_pos }, .end = .{ .row = end_line, .col = end_pos } };
|
||
if (match.end.eql(self.get_primary().cursor))
|
||
match.has_selection = true;
|
||
(self.matches.addOne() catch return).* = match;
|
||
}
|
||
|
||
fn find_selection_match(self: *const Self, sel: Selection) ?*Match {
|
||
for (self.matches.items) |*match_| if (match_.*) |*match| {
|
||
if (match.to_selection().eql(sel))
|
||
return match;
|
||
};
|
||
return null;
|
||
}
|
||
|
||
fn scan_first_match(self: *const Self) ?*Match {
|
||
for (self.matches.items) |*match_| if (match_.*) |*match| {
|
||
if (match.has_selection) continue;
|
||
return match;
|
||
};
|
||
return null;
|
||
}
|
||
|
||
fn scan_next_match(self: *const Self, cursor: Cursor) ?*Match {
|
||
const row = cursor.row;
|
||
const col = cursor.col;
|
||
const multi_cursor = self.cursels.items.len > 1;
|
||
for (self.matches.items) |*match_| if (match_.*) |*match|
|
||
if ((!multi_cursor or !match.has_selection) and (row < match.begin.row or (row == match.begin.row and col < match.begin.col)))
|
||
return match;
|
||
return null;
|
||
}
|
||
|
||
fn get_next_match(self: *const Self, cursor: Cursor) ?*Match {
|
||
if (self.scan_next_match(cursor)) |match| return match;
|
||
var cursor_ = cursor;
|
||
cursor_.move_buffer_begin();
|
||
return self.scan_first_match();
|
||
}
|
||
|
||
fn scan_prev_match(self: *const Self, cursor: Cursor) ?*Match {
|
||
const row = cursor.row;
|
||
const col = cursor.col;
|
||
const count = self.matches.items.len;
|
||
for (0..count) |i| {
|
||
const match = if (self.matches.items[count - 1 - i]) |*m| m else continue;
|
||
if (!match.has_selection and (row > match.end.row or (row == match.end.row and col > match.end.col)))
|
||
return match;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
fn get_prev_match(self: *const Self, cursor: Cursor) ?*Match {
|
||
if (self.scan_prev_match(cursor)) |match| return match;
|
||
const root = self.buf_root() catch return null;
|
||
var cursor_ = cursor;
|
||
cursor_.move_buffer_end(root, self.metrics);
|
||
return self.scan_prev_match(cursor_);
|
||
}
|
||
|
||
pub fn move_cursor_next_match(self: *Self, _: Context) Result {
|
||
const primary = self.get_primary();
|
||
if (self.get_next_match(primary.cursor)) |match| {
|
||
const root = self.buf_root() catch return;
|
||
if (primary.selection) |sel| if (self.find_selection_match(sel)) |match_| {
|
||
match_.has_selection = false;
|
||
};
|
||
primary.selection = match.to_selection();
|
||
primary.cursor.move_to(root, match.end.row, match.end.col, self.metrics) catch return;
|
||
self.clamp();
|
||
}
|
||
}
|
||
pub const move_cursor_next_match_meta = .{ .description = "Move cursor to next hightlighted match" };
|
||
|
||
pub fn goto_next_match(self: *Self, ctx: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
self.cancel_all_selections();
|
||
if (self.matches.items.len == 0) {
|
||
if (self.last_find_query) |last| {
|
||
self.find_operation = .goto_next_match;
|
||
try self.find_in_buffer(last);
|
||
}
|
||
}
|
||
try self.move_cursor_next_match(ctx);
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const goto_next_match_meta = .{ .description = "Goto to next hightlighted match" };
|
||
|
||
pub fn move_cursor_prev_match(self: *Self, _: Context) Result {
|
||
const primary = self.get_primary();
|
||
if (self.get_prev_match(primary.cursor)) |match| {
|
||
const root = self.buf_root() catch return;
|
||
if (primary.selection) |sel| if (self.find_selection_match(sel)) |match_| {
|
||
match_.has_selection = false;
|
||
};
|
||
primary.selection = match.to_selection();
|
||
primary.selection.?.reverse();
|
||
primary.cursor.move_to(root, match.begin.row, match.begin.col, self.metrics) catch return;
|
||
self.clamp();
|
||
}
|
||
}
|
||
pub const move_cursor_prev_match_meta = .{ .description = "Move cursor to previous hightlighted match" };
|
||
|
||
pub fn goto_prev_match(self: *Self, ctx: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
self.cancel_all_selections();
|
||
if (self.matches.items.len == 0) {
|
||
if (self.last_find_query) |last| {
|
||
self.find_operation = .goto_prev_match;
|
||
try self.find_in_buffer(last);
|
||
}
|
||
}
|
||
try self.move_cursor_prev_match(ctx);
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const goto_prev_match_meta = .{ .description = "Goto to previous hightlighted match" };
|
||
|
||
pub fn goto_next_diagnostic(self: *Self, _: Context) Result {
|
||
if (self.diagnostics.items.len == 0) {
|
||
if (command.get_id("goto_next_file")) |id|
|
||
return command.execute(id, .{});
|
||
return;
|
||
}
|
||
self.sort_diagnostics();
|
||
const primary = self.get_primary();
|
||
for (self.diagnostics.items) |*diag| {
|
||
if ((diag.sel.begin.row == primary.cursor.row and diag.sel.begin.col > primary.cursor.col) or diag.sel.begin.row > primary.cursor.row)
|
||
return self.goto_diagnostic(diag);
|
||
}
|
||
return self.goto_diagnostic(&self.diagnostics.items[0]);
|
||
}
|
||
pub const goto_next_diagnostic_meta = .{ .description = "Goto to next diagnostic" };
|
||
|
||
pub fn goto_prev_diagnostic(self: *Self, _: Context) Result {
|
||
if (self.diagnostics.items.len == 0) {
|
||
if (command.get_id("goto_prev_file")) |id|
|
||
return command.execute(id, .{});
|
||
return;
|
||
}
|
||
self.sort_diagnostics();
|
||
const primary = self.get_primary();
|
||
var i = self.diagnostics.items.len - 1;
|
||
while (true) : (i -= 1) {
|
||
const diag = &self.diagnostics.items[i];
|
||
if ((diag.sel.begin.row == primary.cursor.row and diag.sel.begin.col < primary.cursor.col) or diag.sel.begin.row < primary.cursor.row)
|
||
return self.goto_diagnostic(diag);
|
||
if (i == 0) return self.goto_diagnostic(&self.diagnostics.items[self.diagnostics.items.len - 1]);
|
||
}
|
||
}
|
||
pub const goto_prev_diagnostic_meta = .{ .description = "Goto to previous diagnostic" };
|
||
|
||
fn goto_diagnostic(self: *Self, diag: *const Diagnostic) !void {
|
||
const root = self.buf_root() catch return;
|
||
const primary = self.get_primary();
|
||
try self.send_editor_jump_source();
|
||
self.cancel_all_selections();
|
||
try primary.cursor.move_to(root, diag.sel.begin.row, diag.sel.begin.col, self.metrics);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
|
||
fn sort_diagnostics(self: *Self) void {
|
||
const less_fn = struct {
|
||
fn less_fn(_: void, lhs: Diagnostic, rhs: Diagnostic) bool {
|
||
return if (lhs.sel.begin.row == rhs.sel.begin.row)
|
||
lhs.sel.begin.col < rhs.sel.begin.col
|
||
else
|
||
lhs.sel.begin.row < rhs.sel.begin.row;
|
||
}
|
||
}.less_fn;
|
||
std.mem.sort(Diagnostic, self.diagnostics.items, {}, less_fn);
|
||
}
|
||
|
||
pub fn goto_line(self: *Self, ctx: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
var line: usize = 0;
|
||
if (!try ctx.args.match(.{tp.extract(&line)}))
|
||
return error.InvalidArgument;
|
||
const root = self.buf_root() catch return;
|
||
self.cancel_all_selections();
|
||
const primary = self.get_primary();
|
||
try primary.cursor.move_to(root, @intCast(if (line < 1) 0 else line - 1), primary.cursor.col, self.metrics);
|
||
self.clamp();
|
||
try self.send_editor_jump_destination();
|
||
}
|
||
pub const goto_line_meta = .{ .arguments = &.{.integer} };
|
||
|
||
pub fn goto_column(self: *Self, ctx: Context) Result {
|
||
var column: usize = 0;
|
||
if (!try ctx.args.match(.{tp.extract(&column)}))
|
||
return error.InvalidArgument;
|
||
const root = self.buf_root() catch return;
|
||
const primary = self.get_primary();
|
||
try primary.cursor.move_to(root, primary.cursor.row, @intCast(if (column < 1) 0 else column - 1), self.metrics);
|
||
self.clamp();
|
||
}
|
||
pub const goto_column_meta = .{ .arguments = &.{.integer} };
|
||
|
||
pub fn goto_line_and_column(self: *Self, ctx: Context) Result {
|
||
try self.send_editor_jump_source();
|
||
var line: usize = 0;
|
||
var column: usize = 0;
|
||
var have_sel: bool = false;
|
||
var sel: Selection = .{};
|
||
if (try ctx.args.match(.{
|
||
tp.extract(&line),
|
||
tp.extract(&column),
|
||
})) {
|
||
// self.logger.print("goto: l:{d} c:{d}", .{ line, column });
|
||
} else if (try ctx.args.match(.{
|
||
tp.extract(&line),
|
||
tp.extract(&column),
|
||
tp.extract(&sel.begin.row),
|
||
tp.extract(&sel.begin.col),
|
||
tp.extract(&sel.end.row),
|
||
tp.extract(&sel.end.col),
|
||
})) {
|
||
// self.logger.print("goto: l:{d} c:{d} {any}", .{ line, column, sel });
|
||
have_sel = true;
|
||
} else return error.InvalidArgument;
|
||
self.cancel_all_selections();
|
||
const root = self.buf_root() catch return;
|
||
const primary = self.get_primary();
|
||
try primary.cursor.move_to(
|
||
root,
|
||
@intCast(if (line < 1) 0 else line - 1),
|
||
@intCast(if (column < 1) 0 else column - 1),
|
||
self.metrics,
|
||
);
|
||
if (have_sel) primary.selection = sel;
|
||
if (self.view.is_visible(&primary.cursor))
|
||
self.clamp()
|
||
else
|
||
try self.scroll_view_center(.{});
|
||
try self.send_editor_jump_destination();
|
||
self.need_render();
|
||
}
|
||
pub const goto_line_and_column_meta = .{ .arguments = &.{ .integer, .integer } };
|
||
|
||
pub fn goto_definition(self: *Self, _: Context) Result {
|
||
const file_path = self.file_path orelse return;
|
||
const primary = self.get_primary();
|
||
return project_manager.goto_definition(file_path, primary.cursor.row, primary.cursor.col);
|
||
}
|
||
pub const goto_definition_meta = .{ .description = "Language: Goto definition" };
|
||
|
||
pub fn goto_declaration(self: *Self, _: Context) Result {
|
||
const file_path = self.file_path orelse return;
|
||
const primary = self.get_primary();
|
||
return project_manager.goto_declaration(file_path, primary.cursor.row, primary.cursor.col);
|
||
}
|
||
pub const goto_declaration_meta = .{ .description = "Language: Goto declaration" };
|
||
|
||
pub fn goto_implementation(self: *Self, _: Context) Result {
|
||
const file_path = self.file_path orelse return;
|
||
const primary = self.get_primary();
|
||
return project_manager.goto_implementation(file_path, primary.cursor.row, primary.cursor.col);
|
||
}
|
||
pub const goto_implementation_meta = .{ .description = "Language: Goto implementation" };
|
||
|
||
pub fn goto_type_definition(self: *Self, _: Context) Result {
|
||
const file_path = self.file_path orelse return;
|
||
const primary = self.get_primary();
|
||
return project_manager.goto_type_definition(file_path, primary.cursor.row, primary.cursor.col);
|
||
}
|
||
pub const goto_type_definition_meta = .{ .description = "Language: Goto type definition" };
|
||
|
||
pub fn references(self: *Self, _: Context) Result {
|
||
const file_path = self.file_path orelse return;
|
||
const primary = self.get_primary();
|
||
return project_manager.references(file_path, primary.cursor.row, primary.cursor.col);
|
||
}
|
||
pub const references_meta = .{ .description = "Language: Find all references" };
|
||
|
||
pub fn completion(self: *Self, _: Context) Result {
|
||
const file_path = self.file_path orelse return;
|
||
const primary = self.get_primary();
|
||
return project_manager.completion(file_path, primary.cursor.row, primary.cursor.col);
|
||
}
|
||
pub const completion_meta = .{ .description = "Language: Show completions at cursor" };
|
||
|
||
pub fn hover(self: *Self, _: Context) Result {
|
||
const primary = self.get_primary();
|
||
return self.hover_at(primary.cursor.row, primary.cursor.col);
|
||
}
|
||
pub const hover_meta = .{ .description = "Language: Show documentation for symbol (hover)" };
|
||
|
||
pub fn hover_at_abs(self: *Self, y: usize, x: usize) Result {
|
||
const row: usize = self.view.row + y;
|
||
const col: usize = self.view.col + x;
|
||
return self.hover_at(row, col);
|
||
}
|
||
|
||
pub fn hover_at(self: *Self, row: usize, col: usize) Result {
|
||
const file_path = self.file_path orelse return;
|
||
return project_manager.hover(file_path, row, col);
|
||
}
|
||
|
||
pub fn add_diagnostic(
|
||
self: *Self,
|
||
file_path: []const u8,
|
||
source: []const u8,
|
||
code: []const u8,
|
||
message: []const u8,
|
||
severity: i32,
|
||
sel_: Selection,
|
||
) Result {
|
||
if (!std.mem.eql(u8, file_path, self.file_path orelse return)) return;
|
||
|
||
const root = self.buf_root() catch return;
|
||
const sel: Selection = .{
|
||
.begin = .{
|
||
.row = sel_.begin.row,
|
||
.col = root.pos_to_width(sel_.begin.row, sel_.begin.col, self.metrics) catch return,
|
||
},
|
||
.end = .{
|
||
.row = sel_.end.row,
|
||
.col = root.pos_to_width(sel_.end.row, sel_.end.col, self.metrics) catch return,
|
||
},
|
||
};
|
||
|
||
(try self.diagnostics.addOne()).* = .{
|
||
.source = try self.diagnostics.allocator.dupe(u8, source),
|
||
.code = try self.diagnostics.allocator.dupe(u8, code),
|
||
.message = try self.diagnostics.allocator.dupe(u8, message),
|
||
.severity = severity,
|
||
.sel = sel,
|
||
};
|
||
|
||
switch (Diagnostic.to_severity(severity)) {
|
||
.Error => self.diag_errors += 1,
|
||
.Warning => self.diag_warnings += 1,
|
||
.Information => self.diag_info += 1,
|
||
.Hint => self.diag_hints += 1,
|
||
}
|
||
self.send_editor_diagnostics() catch {};
|
||
self.need_render();
|
||
}
|
||
|
||
pub fn clear_diagnostics(self: *Self) void {
|
||
self.diagnostics.clearRetainingCapacity();
|
||
self.diag_errors = 0;
|
||
self.diag_warnings = 0;
|
||
self.diag_info = 0;
|
||
self.diag_hints = 0;
|
||
self.send_editor_diagnostics() catch {};
|
||
self.need_render();
|
||
}
|
||
|
||
pub fn select(self: *Self, ctx: Context) Result {
|
||
var sel: Selection = .{};
|
||
if (!try ctx.args.match(.{ tp.extract(&sel.begin.row), tp.extract(&sel.begin.col), tp.extract(&sel.end.row), tp.extract(&sel.end.col) }))
|
||
return error.InvalidArgument;
|
||
self.get_primary().selection = sel;
|
||
}
|
||
pub const select_meta = .{ .arguments = &.{ .integer, .integer, .integer, .integer } };
|
||
|
||
fn get_formatter(self: *Self) ?[]const []const u8 {
|
||
if (self.syntax) |syn| if (syn.file_type.formatter) |fmtr| if (fmtr.len > 0) return fmtr;
|
||
return null;
|
||
}
|
||
|
||
pub fn format(self: *Self, ctx: Context) Result {
|
||
if (ctx.args.buf.len > 0 and try ctx.args.match(.{ tp.string, tp.more })) {
|
||
try self.filter_cmd(ctx.args);
|
||
return;
|
||
}
|
||
if (self.get_formatter()) |fmtr| {
|
||
var args = std.ArrayList(u8).init(self.allocator);
|
||
const writer = args.writer();
|
||
try cbor.writeArrayHeader(writer, fmtr.len);
|
||
for (fmtr) |arg| try cbor.writeValue(writer, arg);
|
||
try self.filter_cmd(.{ .buf = args.items });
|
||
return;
|
||
}
|
||
return tp.exit("no formatter");
|
||
}
|
||
pub const format_meta = .{ .description = "Language: Format file or selection" };
|
||
|
||
pub fn filter(self: *Self, ctx: Context) Result {
|
||
if (!try ctx.args.match(.{ tp.string, tp.more }))
|
||
return error.InvalidArgument;
|
||
try self.filter_cmd(ctx.args);
|
||
}
|
||
pub const filter_meta = .{ .arguments = &.{.string} };
|
||
|
||
fn filter_cmd(self: *Self, cmd: tp.message) !void {
|
||
if (self.filter) |_| return error.Stop;
|
||
const root = self.buf_root() catch return;
|
||
const buf_a_ = try self.buf_a();
|
||
const primary = self.get_primary();
|
||
var sel: Selection = if (primary.selection) |sel_| sel_ else val: {
|
||
var sel_: Selection = .{};
|
||
try expand_selection_to_all(root, &sel_, self.metrics);
|
||
break :val sel_;
|
||
};
|
||
const reversed = sel.begin.right_of(sel.end);
|
||
sel.normalize();
|
||
self.filter = .{
|
||
.before_root = root,
|
||
.work_root = root,
|
||
.begin = sel.begin,
|
||
.pos = .{ .cursor = sel.begin },
|
||
.old_primary = primary.*,
|
||
.old_primary_reversed = reversed,
|
||
.whole_file = if (primary.selection) |_| null else std.ArrayList(u8).init(self.allocator),
|
||
};
|
||
errdefer self.filter_deinit();
|
||
const state = &self.filter.?;
|
||
var buf: [1024]u8 = undefined;
|
||
const json = try cmd.to_json(&buf);
|
||
self.logger.print("filter: start {s}", .{json});
|
||
var sp = try tp.subprocess.init(self.allocator, cmd, "filter", .Pipe);
|
||
defer {
|
||
sp.close() catch {};
|
||
sp.deinit();
|
||
}
|
||
var buffer = sp.bufferedWriter();
|
||
try self.write_range(state.before_root, sel, buffer.writer(), tp.exit_error, null);
|
||
try buffer.flush();
|
||
self.logger.print("filter: sent", .{});
|
||
state.work_root = try state.work_root.delete_range(sel, buf_a_, null, self.metrics);
|
||
}
|
||
|
||
fn filter_stdout(self: *Self, bytes: []const u8) !void {
|
||
const state = if (self.filter) |*s| s else return error.Stop;
|
||
errdefer self.filter_deinit();
|
||
const buf_a_ = try self.buf_a();
|
||
if (state.whole_file) |*buf| {
|
||
try buf.appendSlice(bytes);
|
||
} else {
|
||
const cursor = &state.pos.cursor;
|
||
cursor.row, cursor.col, state.work_root = try state.work_root.insert_chars(cursor.row, cursor.col, bytes, buf_a_, self.metrics);
|
||
state.bytes += bytes.len;
|
||
state.chunks += 1;
|
||
}
|
||
}
|
||
|
||
fn filter_error(self: *Self, bytes: []const u8) !void {
|
||
defer self.filter_deinit();
|
||
self.logger.print("filter: ERR: {s}", .{bytes});
|
||
if (self.need_save_after_filter) |info| {
|
||
try self.save();
|
||
if (info.then) |then|
|
||
return command.executeName(then.cmd, .{ .args = .{ .buf = then.args } });
|
||
}
|
||
}
|
||
|
||
fn filter_done(self: *Self) !void {
|
||
const b = try self.buf_for_update();
|
||
const root = self.buf_root() catch return;
|
||
const state = if (self.filter) |*s| s else return error.Stop;
|
||
if (state.before_root != root) return error.Stop;
|
||
defer self.filter_deinit();
|
||
const primary = self.get_primary();
|
||
self.cancel_all_selections();
|
||
self.cancel_all_matches();
|
||
if (state.whole_file) |buf| {
|
||
state.work_root = try b.load_from_string(buf.items, &state.eol_mode);
|
||
state.bytes = buf.items.len;
|
||
state.chunks = 1;
|
||
primary.cursor = state.old_primary.cursor;
|
||
} else {
|
||
const sel = primary.enable_selection();
|
||
sel.begin = state.begin;
|
||
sel.end = state.pos.cursor;
|
||
if (state.old_primary_reversed) sel.reverse();
|
||
primary.cursor = sel.end;
|
||
}
|
||
try self.update_buf_and_eol_mode(state.work_root, state.eol_mode);
|
||
primary.cursor.clamp_to_buffer(state.work_root, self.metrics);
|
||
self.logger.print("filter: done (bytes:{d} chunks:{d})", .{ state.bytes, state.chunks });
|
||
self.reset_syntax();
|
||
self.clamp();
|
||
self.need_render();
|
||
if (self.need_save_after_filter) |info| {
|
||
try self.save();
|
||
if (info.then) |then|
|
||
return command.executeName(then.cmd, .{ .args = .{ .buf = then.args } });
|
||
}
|
||
}
|
||
|
||
fn filter_deinit(self: *Self) void {
|
||
const state = if (self.filter) |*s| s else return;
|
||
if (state.whole_file) |*buf| buf.deinit();
|
||
self.filter = null;
|
||
}
|
||
|
||
fn to_upper_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var root = root_;
|
||
const saved = cursel.*;
|
||
const sel = if (cursel.selection) |*sel| sel else ret: {
|
||
var sel = cursel.enable_selection();
|
||
move_cursor_word_begin(root, &sel.begin, self.metrics) catch return error.Stop;
|
||
move_cursor_word_end(root, &sel.end, self.metrics) catch return error.Stop;
|
||
break :ret sel;
|
||
};
|
||
var sfa = std.heap.stackFallback(4096, self.allocator);
|
||
const cut_text = copy_selection(root, sel.*, sfa.get(), self.metrics) catch return error.Stop;
|
||
defer allocator.free(cut_text);
|
||
const ucased = self.get_case_data().toUpperStr(allocator, cut_text) catch return error.Stop;
|
||
defer allocator.free(ucased);
|
||
root = try self.delete_selection(root, cursel, allocator);
|
||
root = self.insert(root, cursel, ucased, allocator) catch return error.Stop;
|
||
cursel.* = saved;
|
||
return root;
|
||
}
|
||
|
||
pub fn to_upper(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.with_cursels_mut(b.root, to_upper_cursel, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const to_upper_meta = .{ .description = "Convert selection or word to upper case" };
|
||
|
||
fn to_lower_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var root = root_;
|
||
const saved = cursel.*;
|
||
const sel = if (cursel.selection) |*sel| sel else ret: {
|
||
var sel = cursel.enable_selection();
|
||
move_cursor_word_begin(root, &sel.begin, self.metrics) catch return error.Stop;
|
||
move_cursor_word_end(root, &sel.end, self.metrics) catch return error.Stop;
|
||
break :ret sel;
|
||
};
|
||
var sfa = std.heap.stackFallback(4096, self.allocator);
|
||
const cut_text = copy_selection(root, sel.*, sfa.get(), self.metrics) catch return error.Stop;
|
||
defer allocator.free(cut_text);
|
||
const ucased = self.get_case_data().toLowerStr(allocator, cut_text) catch return error.Stop;
|
||
defer allocator.free(ucased);
|
||
root = try self.delete_selection(root, cursel, allocator);
|
||
root = self.insert(root, cursel, ucased, allocator) catch return error.Stop;
|
||
cursel.* = saved;
|
||
return root;
|
||
}
|
||
|
||
pub fn to_lower(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.with_cursels_mut(b.root, to_lower_cursel, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const to_lower_meta = .{ .description = "Convert selection or word to lower case" };
|
||
|
||
fn switch_case_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
|
||
var root = root_;
|
||
var saved = cursel.*;
|
||
const sel = if (cursel.selection) |*sel| sel else ret: {
|
||
var sel = cursel.enable_selection();
|
||
move_cursor_right(root, &sel.end, self.metrics) catch return error.Stop;
|
||
saved.cursor = sel.end;
|
||
break :ret sel;
|
||
};
|
||
var result = std.ArrayList(u8).init(self.allocator);
|
||
defer result.deinit();
|
||
const writer: struct {
|
||
self_: *Self,
|
||
result: *std.ArrayList(u8),
|
||
|
||
const Error = @typeInfo(@typeInfo(@TypeOf(CaseData.toUpperStr)).Fn.return_type.?).ErrorUnion.error_set;
|
||
pub fn write(writer: *@This(), bytes: []const u8) Error!void {
|
||
const cd = writer.self_.get_case_data();
|
||
const flipped = if (cd.isLowerStr(bytes))
|
||
try cd.toUpperStr(writer.self_.allocator, bytes)
|
||
else
|
||
try cd.toLowerStr(writer.self_.allocator, bytes);
|
||
defer writer.self_.allocator.free(flipped);
|
||
return writer.result.appendSlice(flipped);
|
||
}
|
||
fn map_error(e: anyerror, _: ?*std.builtin.StackTrace) Error {
|
||
return @errorCast(e);
|
||
}
|
||
} = .{
|
||
.self_ = self,
|
||
.result = &result,
|
||
};
|
||
self.write_range(root, sel.*, writer, @TypeOf(writer).map_error, null) catch return error.Stop;
|
||
root = try self.delete_selection(root, cursel, allocator);
|
||
root = self.insert(root, cursel, writer.result.items, allocator) catch return error.Stop;
|
||
cursel.* = saved;
|
||
return root;
|
||
}
|
||
|
||
pub fn switch_case(self: *Self, _: Context) Result {
|
||
const b = try self.buf_for_update();
|
||
const root = try self.with_cursels_mut(b.root, switch_case_cursel, b.allocator);
|
||
try self.update_buf(root);
|
||
self.clamp();
|
||
}
|
||
pub const switch_case_meta = .{ .description = "Switch the case of selection or character at cursor" };
|
||
|
||
pub fn toggle_eol_mode(self: *Self, _: Context) Result {
|
||
if (self.buffer) |b| {
|
||
b.file_eol_mode = switch (b.file_eol_mode) {
|
||
.lf => .crlf,
|
||
.crlf => .lf,
|
||
};
|
||
self.update_event() catch {};
|
||
}
|
||
}
|
||
pub const toggle_eol_mode_meta = .{ .description = "Toggle end of line sequence" };
|
||
|
||
pub fn toggle_syntax_highlighting(self: *Self, _: Context) Result {
|
||
self.syntax_no_render = !self.syntax_no_render;
|
||
if (self.syntax_no_render) {
|
||
if (self.syntax) |syn| {
|
||
const frame = tracy.initZone(@src(), .{ .name = "editor reset syntax" });
|
||
defer frame.deinit();
|
||
syn.reset();
|
||
self.syntax_last_rendered_root = null;
|
||
self.syntax_refresh_full = true;
|
||
self.syntax_incremental_reparse = false;
|
||
}
|
||
}
|
||
self.logger.print("syntax highlighting {s}", .{if (self.syntax_no_render) "disabled" else "enabled"});
|
||
}
|
||
pub const toggle_syntax_highlighting_meta = .{ .description = "Toggle syntax highlighting" };
|
||
|
||
pub fn toggle_syntax_timing(self: *Self, _: Context) Result {
|
||
self.syntax_report_timing = !self.syntax_report_timing;
|
||
}
|
||
pub const toggle_syntax_timing_meta = .{ .description = "Toggle tree-sitter timing reports" };
|
||
};
|
||
|
||
pub fn create(allocator: Allocator, parent: Widget) !Widget {
|
||
return EditorWidget.create(allocator, parent);
|
||
}
|
||
|
||
pub const EditorWidget = struct {
|
||
plane: Plane,
|
||
parent: Plane,
|
||
|
||
editor: Editor,
|
||
commands: Commands = undefined,
|
||
|
||
last_btn: input.Mouse = .none,
|
||
last_btn_time_ms: i64 = 0,
|
||
last_btn_count: usize = 0,
|
||
|
||
hover: bool = false,
|
||
hover_timer: ?tp.Cancellable = null,
|
||
hover_x: c_int = -1,
|
||
hover_y: c_int = -1,
|
||
|
||
const Self = @This();
|
||
const Commands = command.Collection(Editor);
|
||
|
||
fn create(allocator: Allocator, parent: Widget) !Widget {
|
||
const container = try WidgetList.createH(allocator, parent, "editor.container", .dynamic);
|
||
const self: *Self = try allocator.create(Self);
|
||
try self.init(allocator, container.widget());
|
||
try self.commands.init(&self.editor);
|
||
const editorWidget = Widget.to(self);
|
||
try container.add(try editor_gutter.create(allocator, container.widget(), editorWidget, &self.editor));
|
||
try container.add(editorWidget);
|
||
try container.add(try scrollbar_v.create(allocator, container.widget(), editorWidget, EventHandler.to_unowned(container)));
|
||
return container.widget();
|
||
}
|
||
|
||
fn init(self: *Self, allocator: Allocator, parent: Widget) !void {
|
||
var n = try Plane.init(&(Widget.Box{}).opts("editor"), parent.plane.*);
|
||
errdefer n.deinit();
|
||
|
||
self.* = .{
|
||
.parent = parent.plane.*,
|
||
.plane = n,
|
||
.editor = undefined,
|
||
};
|
||
self.editor.init(allocator, n);
|
||
errdefer self.editor.deinit();
|
||
try self.editor.push_cursor();
|
||
}
|
||
|
||
pub fn deinit(self: *Self, allocator: Allocator) void {
|
||
self.update_hover_timer(.cancel);
|
||
self.commands.deinit();
|
||
self.editor.deinit();
|
||
self.plane.deinit();
|
||
allocator.destroy(self);
|
||
}
|
||
|
||
pub fn update(self: *Self) void {
|
||
self.editor.update();
|
||
}
|
||
|
||
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
|
||
return self.editor.render(theme);
|
||
}
|
||
|
||
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
|
||
return self.receive_safe(m) catch |e| return tp.exit_error(e, @errorReturnTrace());
|
||
}
|
||
|
||
fn receive_safe(self: *Self, m: tp.message) !bool {
|
||
var event: input.Event = undefined;
|
||
var btn: input.MouseType = 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(.{ "M", tp.extract(&x), tp.extract(&y), tp.extract(&xpx), tp.extract(&ypx) })) {
|
||
const hover_y, const hover_x = self.editor.plane.abs_yx_to_rel(y, x);
|
||
if (hover_y != self.hover_y or hover_x != self.hover_x) {
|
||
self.hover_y, self.hover_x = .{ hover_y, hover_x };
|
||
if (self.editor.jump_mode)
|
||
self.update_hover_timer(.init);
|
||
}
|
||
} else if (try m.match(.{ "B", tp.extract(&event), tp.extract(&btn), tp.any, tp.extract(&x), tp.extract(&y), tp.extract(&xpx), tp.extract(&ypx) })) {
|
||
try self.mouse_click_event(event, @enumFromInt(btn), y, x, ypx, xpx);
|
||
} else if (try m.match(.{ "D", tp.extract(&event), tp.extract(&btn), tp.any, tp.extract(&x), tp.extract(&y), tp.extract(&xpx), tp.extract(&ypx) })) {
|
||
try self.mouse_drag_event(event, @enumFromInt(btn), y, x, ypx, xpx);
|
||
} else if (try m.match(.{ "scroll_to", tp.extract(&pos) })) {
|
||
self.editor.scroll_to(pos);
|
||
} else if (try m.match(.{ "filter", "stdout", tp.extract(&bytes) })) {
|
||
self.editor.filter_stdout(bytes) catch {};
|
||
} else if (try m.match(.{ "filter", "stderr", tp.extract(&bytes) })) {
|
||
try self.editor.filter_error(bytes);
|
||
} else if (try m.match(.{ "filter", "term", tp.more })) {
|
||
try self.editor.filter_done();
|
||
} else if (try m.match(.{ "A", tp.more })) {
|
||
self.editor.add_match(m) catch {};
|
||
} else if (try m.match(.{ "H", tp.extract(&self.hover) })) {
|
||
if (self.editor.jump_mode) {
|
||
self.update_hover_timer(.init);
|
||
tui.current().rdr.request_mouse_cursor_pointer(self.hover);
|
||
} else {
|
||
self.update_hover_timer(.cancel);
|
||
tui.current().rdr.request_mouse_cursor_text(self.hover);
|
||
}
|
||
} else if (try m.match(.{"HOVER"})) {
|
||
self.update_hover_timer(.fired);
|
||
if (self.hover_y >= 0 and self.hover_x >= 0)
|
||
try self.editor.hover_at_abs(@intCast(self.hover_y), @intCast(self.hover_x));
|
||
} else if (try m.match(.{ "whitespace_mode", tp.extract(&bytes) })) {
|
||
self.editor.render_whitespace = Editor.from_whitespace_mode(bytes);
|
||
} else {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
fn update_hover_timer(self: *Self, event: enum { init, fired, cancel }) void {
|
||
if (self.hover_timer) |*t| {
|
||
if (event != .fired) t.cancel() catch {};
|
||
t.deinit();
|
||
self.hover_timer = null;
|
||
}
|
||
if (event == .init) {
|
||
const delay_us: u64 = std.time.us_per_ms * 100;
|
||
self.hover_timer = tp.self_pid().delay_send_cancellable(self.editor.allocator, "editor.hover_timer", delay_us, .{"HOVER"}) catch null;
|
||
}
|
||
}
|
||
|
||
const Result = command.Result;
|
||
|
||
fn mouse_click_event(self: *Self, event: input.Event, btn: input.Mouse, y: c_int, x: c_int, ypx: c_int, xpx: c_int) Result {
|
||
if (event != input.event.press) return;
|
||
const ret = (switch (btn) {
|
||
input.mouse.BUTTON1 => &mouse_click_button1,
|
||
input.mouse.BUTTON2 => &mouse_click_button2,
|
||
input.mouse.BUTTON3 => &mouse_click_button3,
|
||
input.mouse.BUTTON4 => &mouse_click_button4,
|
||
input.mouse.BUTTON5 => &mouse_click_button5,
|
||
input.mouse.BUTTON8 => &mouse_click_button8, //back
|
||
input.mouse.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, event: input.Event, btn: input.Mouse, y: c_int, x: c_int, ypx: c_int, xpx: c_int) Result {
|
||
if (event != input.event.press) return;
|
||
return (switch (btn) {
|
||
input.mouse.BUTTON1 => &mouse_drag_button1,
|
||
input.mouse.BUTTON2 => &mouse_drag_button2,
|
||
input.mouse.BUTTON3 => &mouse_drag_button3,
|
||
else => return,
|
||
})(self, y, x, ypx, xpx);
|
||
}
|
||
|
||
fn mouse_click_button1(self: *Self, y: c_int, x: c_int, _: c_int, _: c_int) Result {
|
||
const y_, const x_ = self.editor.plane.abs_yx_to_rel(y, x);
|
||
if (self.last_btn == input.mouse.BUTTON1) {
|
||
const click_time_ms = time.milliTimestamp() - self.last_btn_time_ms;
|
||
if (click_time_ms <= double_click_time_ms) {
|
||
if (self.last_btn_count == 2) {
|
||
self.last_btn_count = 3;
|
||
try self.editor.primary_triple_click(y_, x_);
|
||
return;
|
||
}
|
||
self.last_btn_count = 2;
|
||
try self.editor.primary_double_click(y_, x_);
|
||
return;
|
||
}
|
||
}
|
||
self.last_btn_count = 1;
|
||
try self.editor.primary_click(y_, x_);
|
||
return;
|
||
}
|
||
|
||
fn mouse_drag_button1(self: *Self, y: c_int, x: c_int, _: c_int, _: c_int) Result {
|
||
const y_, const x_ = self.editor.plane.abs_yx_to_rel(y, x);
|
||
self.editor.primary_drag(y_, x_);
|
||
}
|
||
|
||
fn mouse_click_button2(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result {}
|
||
|
||
fn mouse_drag_button2(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result {}
|
||
|
||
fn mouse_click_button3(self: *Self, y: c_int, x: c_int, _: c_int, _: c_int) Result {
|
||
const y_, const x_ = self.editor.plane.abs_yx_to_rel(y, x);
|
||
try self.editor.secondary_click(y_, x_);
|
||
}
|
||
|
||
fn mouse_drag_button3(self: *Self, y: c_int, x: c_int, _: c_int, _: c_int) Result {
|
||
const y_, const x_ = self.editor.plane.abs_yx_to_rel(y, x);
|
||
try self.editor.secondary_drag(y_, x_);
|
||
}
|
||
|
||
fn mouse_click_button4(self: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result {
|
||
try self.editor.scroll_up_pageup(.{});
|
||
}
|
||
|
||
fn mouse_click_button5(self: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result {
|
||
try self.editor.scroll_down_pagedown(.{});
|
||
}
|
||
|
||
fn mouse_click_button8(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result {
|
||
try command.executeName("jump_back", .{});
|
||
}
|
||
|
||
fn mouse_click_button9(_: *Self, _: c_int, _: c_int, _: c_int, _: c_int) Result {
|
||
try command.executeName("jump_forward", .{});
|
||
}
|
||
|
||
pub fn handle_resize(self: *Self, pos: Widget.Box) void {
|
||
self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return;
|
||
self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return;
|
||
self.editor.handle_resize(pos);
|
||
}
|
||
|
||
pub fn subscribe(self: *Self, h: EventHandler) !void {
|
||
self.editor.handlers.add(h) catch {};
|
||
}
|
||
|
||
pub fn unsubscribe(self: *Self, h: EventHandler) !void {
|
||
self.editor.handlers.remove(h) catch {};
|
||
}
|
||
};
|
||
|
||
pub const PosToWidthCache = struct {
|
||
cache: std.ArrayList(usize),
|
||
cached_line: usize = std.math.maxInt(usize),
|
||
cached_root: ?Buffer.Root = null,
|
||
|
||
const Self = @This();
|
||
|
||
pub fn init(allocator: Allocator) !Self {
|
||
return .{
|
||
.cache = try std.ArrayList(usize).initCapacity(allocator, 2048),
|
||
};
|
||
}
|
||
|
||
pub fn deinit(self: *Self) void {
|
||
self.cache.deinit();
|
||
}
|
||
|
||
pub fn range_to_selection(self: *Self, range: syntax.Range, root: Buffer.Root, metrics: Buffer.Metrics) ?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, metrics) catch return null;
|
||
}
|
||
const start_col = if (start.column < self.cache.items.len) self.cache.items[start.column] else start.column;
|
||
const end_col = if (end.row == start.row and end.column < self.cache.items.len) self.cache.items[end.column] else root.pos_to_width(end.row, end.column, metrics) catch end.column;
|
||
return .{ .begin = .{ .row = start.row, .col = start_col }, .end = .{ .row = end.row, .col = end_col } };
|
||
}
|
||
};
|