Merge branch 'master' into helix-mode-selections

This commit is contained in:
Meredith Oleander 2025-01-22 13:47:37 +11:00 committed by GitHub
commit 80c8795c3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 429 additions and 134 deletions

View file

@ -242,6 +242,7 @@ pub const Editor = struct {
file_path: ?[]const u8,
buffer: ?*Buffer,
buffer_manager: *Buffer.Manager,
lsp_version: usize = 1,
pause_undo: bool = false,
@ -382,7 +383,7 @@ pub const Editor = struct {
self.clamp();
}
fn init(self: *Self, allocator: Allocator, n: Plane) void {
fn init(self: *Self, allocator: Allocator, n: Plane, buffer_manager: *Buffer.Manager) void {
const logger = log.logger("editor");
const frame_rate = tp.env.get().num("frame-rate");
const indent_size = tui.current().config.indent_size;
@ -396,6 +397,7 @@ pub const Editor = struct {
.logger = logger,
.file_path = null,
.buffer = null,
.buffer_manager = buffer_manager,
.handlers = EventHandler.List.init(allocator),
.animation_lag = get_animation_max_lag(),
.animation_frame_rate = frame_rate,
@ -417,7 +419,7 @@ pub const Editor = struct {
self.matches.deinit();
self.handlers.deinit();
self.logger.deinit();
if (self.buffer) |p| p.deinit();
if (self.buffer) |p| self.buffer_manager.retire(p);
if (self.case_data) |cd| cd.deinit();
}
@ -483,28 +485,16 @@ pub const Editor = struct {
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);
return self.open_buffer(file_path, try self.buffer_manager.open_file(file_path));
}
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);
return self.open_buffer(file_path, try self.buffer_manager.open_scratch(file_path, content));
}
fn open_buffer(self: *Self, file_path: []const u8, new_buf: *Buffer) !void {
errdefer new_buf.deinit();
errdefer self.buffer_manager.retire(new_buf);
self.cancel_all_selections();
self.get_primary().reset();
self.file_path = try self.allocator.dupe(u8, file_path);
@ -543,17 +533,7 @@ pub const Editor = struct {
}
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();
if (self.buffer) |b_mut| self.buffer_manager.retire(b_mut);
self.buffer = null;
self.plane.erase();
self.plane.home();
@ -3805,7 +3785,8 @@ pub const Editor = struct {
pub fn close_file_without_saving(self: *Self, _: Context) Result {
self.cancel_all_selections();
try self.close_dirty();
if (self.buffer) |buffer| buffer.reset_to_last_saved();
try self.close();
}
pub const close_file_without_saving_meta = .{ .description = "Close file without saving" };
@ -4794,8 +4775,8 @@ pub const Editor = struct {
pub const set_file_type_meta = .{ .arguments = &.{.string} };
};
pub fn create(allocator: Allocator, parent: Widget) !Widget {
return EditorWidget.create(allocator, parent);
pub fn create(allocator: Allocator, parent: Widget, buffer_manager: *Buffer.Manager) !Widget {
return EditorWidget.create(allocator, parent, buffer_manager);
}
pub const EditorWidget = struct {
@ -4817,10 +4798,10 @@ pub const EditorWidget = struct {
const Self = @This();
const Commands = command.Collection(Editor);
fn create(allocator: Allocator, parent: Widget) !Widget {
fn create(allocator: Allocator, parent: Widget, buffer_manager: *Buffer.Manager) !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.init(allocator, container.widget(), buffer_manager);
try self.commands.init(&self.editor);
const editorWidget = Widget.to(self);
try container.add(try editor_gutter.create(allocator, container.widget(), editorWidget, &self.editor));
@ -4829,7 +4810,7 @@ pub const EditorWidget = struct {
return container.widget();
}
fn init(self: *Self, allocator: Allocator, parent: Widget) !void {
fn init(self: *Self, allocator: Allocator, parent: Widget, buffer_manager: *Buffer.Manager) !void {
var n = try Plane.init(&(Widget.Box{}).opts("editor"), parent.plane.*);
errdefer n.deinit();
@ -4838,7 +4819,7 @@ pub const EditorWidget = struct {
.plane = n,
.editor = undefined,
};
self.editor.init(allocator, n);
self.editor.init(allocator, n, buffer_manager);
errdefer self.editor.deinit();
try self.editor.push_cursor();
}

View file

@ -245,6 +245,12 @@ const cmds = struct {
const Ctx = command.Context;
const Result = command.Result;
pub fn save_all(_: *Self, _: Ctx) Result {
if (tui.get_buffer_manager()) |bm|
bm.save_all() catch |e| return tp.exit_error(e, @errorReturnTrace());
}
pub const save_all_meta = .{ .description = "Save all changed files" };
pub fn home_menu_down(self: *Self, _: Ctx) Result {
self.menu.select_down();
}

View file

@ -14,6 +14,7 @@ const build_options = @import("build_options");
const Plane = @import("renderer").Plane;
const input = @import("input");
const command = @import("command");
const BufferManager = @import("Buffer").Manager;
const tui = @import("tui.zig");
const Box = @import("Box.zig");
@ -47,6 +48,7 @@ active_view: ?usize = 0,
panels: ?*WidgetList = null,
last_match_text: ?[]const u8 = null,
location_history: location_history,
buffer_manager: BufferManager,
file_stack: std.ArrayList([]const u8),
find_in_files_state: enum { init, adding, done } = .done,
file_list_type: FileListType = .find_in_files,
@ -70,6 +72,7 @@ pub fn create(allocator: std.mem.Allocator) !Widget {
.file_stack = std.ArrayList([]const u8).init(allocator),
.views = undefined,
.views_widget = undefined,
.buffer_manager = BufferManager.init(allocator),
};
try self.commands.init(self);
const w = Widget.to(self);
@ -104,6 +107,7 @@ pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
self.commands.deinit();
self.widgets.deinit(allocator);
self.floating_views.deinit();
self.buffer_manager.deinit();
allocator.destroy(self);
}
@ -237,15 +241,8 @@ fn toggle_view(self: *Self, view: anytype) !void {
}
fn check_all_not_dirty(self: *const Self) command.Result {
for (self.editors.items) |editor|
if (editor.is_dirty())
return tp.exit("unsaved changes");
}
fn check_active_not_dirty(self: *const Self) command.Result {
if (self.active_editor) |idx|
if (self.editors.items[idx].is_dirty())
return tp.exit("unsaved changes");
if (self.buffer_manager.is_dirty())
return tp.exit("unsaved changes");
}
const cmds = struct {
@ -296,6 +293,8 @@ const cmds = struct {
self.clear_find_in_files_results(.diagnostics);
if (self.file_list_type == .diagnostics and self.is_panel_view_showing(filelist_view))
try self.toggle_panel_view(filelist_view, false);
self.buffer_manager.deinit();
self.buffer_manager = BufferManager.init(self.allocator);
try project_manager.open(project_dir);
const project = tp.env.get().str("project");
tui.current().rdr.set_terminal_working_directory(project);
@ -352,7 +351,6 @@ const cmds = struct {
if (!same_file) {
if (self.get_active_editor()) |editor| {
try self.check_active_not_dirty();
editor.send_editor_jump_source() catch {};
}
try self.create_editor();
@ -395,7 +393,6 @@ const cmds = struct {
pub const open_gui_config_meta = .{ .description = "Edit gui configuration file" };
pub fn create_scratch_buffer(self: *Self, ctx: Ctx) Result {
try self.check_all_not_dirty();
tui.reset_drag_context();
try self.create_editor();
try command.executeName("open_scratch_buffer", ctx);
@ -863,7 +860,7 @@ fn create_editor(self: *Self) !void {
if (self.get_active_file_path()) |file_path| self.push_file_stack(file_path) catch {};
try self.delete_active_view();
command.executeName("enter_mode_default", .{}) catch {};
var editor_widget = try ed.create(self.allocator, Widget.to(self));
var editor_widget = try ed.create(self.allocator, Widget.to(self), &self.buffer_manager);
errdefer editor_widget.deinit(self.allocator);
const editor = editor_widget.get("editor") orelse @panic("mainview editor not found");
if (self.top_bar) |bar| editor.subscribe(EventHandler.to_unowned(bar)) catch @panic("subscribe unsupported");

View file

@ -11,7 +11,6 @@ pub const create = Type.create;
pub fn load_entries(self: *Type) error{ Exit, OutOfMemory }!void {
const editor = tui.get_active_editor() orelse return;
if (editor.is_dirty()) return tp.exit("unsaved changes");
if (editor.file_path) |old_path|
if (std.mem.lastIndexOf(u8, old_path, "/")) |pos|
try self.file_path.appendSlice(old_path[0 .. pos + 1]);

View file

@ -0,0 +1,49 @@
const std = @import("std");
const cbor = @import("cbor");
const tp = @import("thespian");
const root = @import("root");
const command = @import("command");
const tui = @import("../../tui.zig");
pub const Type = @import("palette.zig").Create(@This());
pub const label = "Switch buffers";
pub const name = " buffer";
pub const description = "buffer";
const dirty_indicator = "";
pub const Entry = struct {
label: []const u8,
hint: []const u8,
};
pub fn load_entries(palette: *Type) !usize {
const buffer_manager = tui.get_buffer_manager() orelse return 0;
const buffers = try buffer_manager.list_most_recently_used(palette.allocator);
defer palette.allocator.free(buffers);
for (buffers) |buffer| {
const hint = if (buffer.is_dirty()) dirty_indicator else "";
(try palette.entries.addOne()).* = .{ .label = buffer.file_path, .hint = hint };
}
return if (palette.entries.items.len == 0) label.len else 2;
}
pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void {
var value = std.ArrayList(u8).init(palette.allocator);
defer value.deinit();
const writer = value.writer();
try cbor.writeValue(writer, entry.label);
try cbor.writeValue(writer, entry.hint);
try cbor.writeValue(writer, matches orelse &[_]usize{});
try palette.menu.add_item_with_handler(value.items, select);
palette.items += 1;
}
fn select(menu: **Type.MenuState, button: *Type.ButtonState) void {
var file_path: []const u8 = undefined;
var iter = button.opts.label;
if (!(cbor.matchString(&iter, &file_path) catch false)) return;
tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("navigate", e);
tp.self_pid().send(.{ "cmd", "navigate", .{} }) catch |e| menu.*.opts.ctx.logger.err("navigate", e);
tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch |e| menu.*.opts.ctx.logger.err("navigate", e);
}

View file

@ -10,6 +10,7 @@ const keybind = @import("keybind");
const project_manager = @import("project_manager");
const command = @import("command");
const EventHandler = @import("EventHandler");
const BufferManager = @import("Buffer").Manager;
const tui = @import("../../tui.zig");
const MessageFilter = @import("../../MessageFilter.zig");
@ -34,6 +35,7 @@ need_reset: bool = false,
need_select_first: bool = true,
longest: usize = 0,
commands: Commands = undefined,
buffer_manager: ?*BufferManager,
pub fn create(allocator: std.mem.Allocator) !tui.Mode {
const mv = tui.current().mainview.dynamic_cast(mainview) orelse return error.NotFound;
@ -51,6 +53,7 @@ pub fn create(allocator: std.mem.Allocator) !tui.Mode {
.ctx = self,
.label = "Search files by name",
}))).dynamic_cast(InputBox.State(*Self)) orelse unreachable,
.buffer_manager = tui.get_buffer_manager(),
};
try self.commands.init(self);
try tui.current().message_filters.add(MessageFilter.bind(self, receive_project_manager));
@ -93,7 +96,7 @@ inline fn max_menu_width() usize {
return @max(15, width - (width / 5));
}
fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool {
fn on_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool {
const style_base = theme.editor_widget;
const style_label = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget;
const style_keybind = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_base;
@ -110,7 +113,8 @@ fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *c
if (!(cbor.matchString(&iter, &file_path) catch false))
file_path = "#ERROR#";
button.plane.set_style(style_keybind);
const pointer = if (selected) "" else " ";
const dirty = if (self.buffer_manager) |bm| if (bm.is_buffer_dirty(file_path)) "" else " " else " ";
const pointer = if (selected) "" else dirty;
_ = button.plane.print("{s}", .{pointer}) catch {};
var buf: [std.fs.max_path_bytes]u8 = undefined;
var removed_prefix: usize = 0;

View file

@ -56,6 +56,7 @@ mouse_idle_timer: ?tp.Cancellable = null,
default_cursor: keybind.CursorShape = .default,
fontface: []const u8 = "",
fontfaces: ?std.ArrayList([]const u8) = null,
enable_mouse_idle_timer: bool = false,
const keepalive = std.time.us_per_day * 365; // one year
const idle_frames = 0;
@ -222,6 +223,7 @@ fn listen_sigwinch(self: *Self) tp.result {
}
fn update_mouse_idle_timer(self: *Self) void {
if (!self.enable_mouse_idle_timer) return;
const delay = std.time.us_per_ms * @as(u64, mouse_idle_time_milliseconds);
if (self.mouse_idle_timer) |*t| {
t.cancel() catch {};
@ -309,6 +311,8 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void {
if (try m.match(.{"resize"})) {
self.resize();
const box = self.screen();
message("{d}x{d}", .{ box.w, box.h });
return;
}
@ -811,6 +815,11 @@ const cmds = struct {
}
pub const open_recent_project_meta = .{ .description = "Open recent project" };
pub fn switch_buffers(self: *Self, _: Ctx) Result {
return self.enter_overlay_mode(@import("mode/overlay/buffer_palette.zig").Type);
}
pub const switch_buffers_meta = .{ .description = "Switch buffers" };
pub fn change_theme(self: *Self, _: Ctx) Result {
return self.enter_overlay_mode(@import("mode/overlay/theme_palette.zig").Type);
}
@ -987,6 +996,10 @@ pub fn get_active_selection(allocator: std.mem.Allocator) ?[]u8 {
return editor.get_selection(sel, allocator) catch null;
}
pub fn get_buffer_manager() ?*@import("Buffer").Manager {
return if (current().mainview.dynamic_cast(mainview)) |mv_| &mv_.buffer_manager else null;
}
fn context_check() void {
if (instance_ == null) @panic("tui call out of context");
}
@ -1176,3 +1189,8 @@ pub fn is_cursor_beam(self: *Self) bool {
pub fn get_selection_style(self: *Self) @import("Buffer").Selection.Style {
return if (self.input_mode) |mode| mode.selection_style else .normal;
}
pub fn message(comptime fmt: anytype, args: anytype) void {
var buf: [256]u8 = undefined;
tp.self_pid().send(.{ "message", std.fmt.bufPrint(&buf, fmt, args) catch @panic("too large") }) catch {};
}