From 552417091d97d4e8141dee2c056c16916d1a2954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20T=C3=A1mara?= Date: Fri, 3 Oct 2025 00:24:33 -0500 Subject: [PATCH 1/4] feat: add helix mode close other buffers and reload all * x and x! have expected behaviour in Helix mode --- src/buffer/Manager.zig | 37 +++++++++++++++++++++++++++++++++++++ src/tui/mode/helix.zig | 35 ++++++++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig index 93b883b..4c4f1e9 100644 --- a/src/buffer/Manager.zig +++ b/src/buffer/Manager.zig @@ -149,6 +149,17 @@ pub fn save_all(self: *const Self) Buffer.StoreToFileError!void { } } +pub fn reload_all(self: *const Self) Buffer.LoadFromFileError!void { + var i = self.buffers.iterator(); + while (i.next()) |kv| { + const buffer = kv.value_ptr.*; + if (buffer.is_ephemeral()) + buffer.mark_clean() + else + try buffer.refresh_from_file(); + } +} + pub fn delete_all(self: *Self) void { var i = self.buffers.iterator(); while (i.next()) |p| { @@ -158,6 +169,32 @@ pub fn delete_all(self: *Self) void { self.buffers.clearRetainingCapacity(); } +pub fn delete_others(self: *Self, protected: *Buffer) void { + var i = self.buffers.iterator(); + while (i.next()) |p| { + const buffer = p.value_ptr.*; + if (buffer != protected) { + buffer.reset_to_last_saved(); + self.close_buffer(buffer); + } + } +} + +pub fn close_others(self: *Self, protected: *Buffer) usize { + var removed: usize = 0; + var i = self.buffers.iterator(); + if (self.is_dirty()) return 0; + while (i.next()) |p| { + const buffer = p.value_ptr.*; + if (buffer != protected) { + if (self.buffers.remove(buffer.get_file_path())) + removed += 1; + buffer.deinit(); + } + } + return removed; +} + pub fn buffer_from_ref(self: *Self, buffer_ref: usize) ?*Buffer { var i = self.buffers.iterator(); while (i.next()) |p| diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index d186cf3..810ce15 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -60,20 +60,16 @@ const cmds_ = struct { } pub const wq_meta: Meta = .{ .description = "wq (write/save file and quit)" }; + pub fn @"x!"(_: *void, _: Ctx) Result { + try cmd("save_file", command.fmt(.{ "then", .{ "quit_without_saving", .{} } })); + } + pub const @"x!_meta": Meta = .{ .description = "x! (write/save file and close forcefully, ignoring other unsaved changes)" }; + pub fn x(_: *void, _: Ctx) Result { try cmd("save_file", command.fmt(.{ "then", .{ "quit", .{} } })); } pub const x_meta: Meta = .{ .description = "x (write/save file and quit)" }; - // This one needs some help, the intention is to close only the current buffer - // , if is the only buffer, exit... - // TODO - // pub fn @"x!"(_: *void, _: Ctx) Result { - // try cmd("save_file", .{}); - // try cmd("close_file_without_saving", .{}); - // } - // pub const @"x!_meta": Meta = .{ .description = "x! (write/save file and close forcefully, ignoring unsaved changes)" }; - pub fn wa(_: *void, _: Ctx) Result { if (tui.get_buffer_manager()) |bm| bm.save_all() catch |e| return tp.exit_error(e, @errorReturnTrace()); @@ -116,6 +112,12 @@ const cmds_ = struct { } pub const rl_meta: Meta = .{ .description = "rl (force reload current file)" }; + pub fn rla(_: *void, _: Ctx) Result { + if (tui.get_buffer_manager()) |bm| + bm.reload_all() catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + pub const rla_meta: Meta = .{ .description = "rla (reload all files discarding the current contents)" }; + pub fn o(_: *void, _: Ctx) Result { try cmd("open_file", .{}); } @@ -152,6 +154,21 @@ const cmds_ = struct { } pub const @"bc!_meta": Meta = .{ .description = "bc! (Close buffer/tab forcefully, ignoring changes)" }; + pub fn @"bco!"(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + if (tui.get_buffer_manager()) |bm| { + if (mv.get_active_buffer()) |buffer| bm.delete_others(buffer); + } + } + pub const @"bco!_meta": Meta = .{ .description = "bco! (Close other buffers/tabs forcefully, ignoring changes)" }; + + pub fn bco(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const bm = tui.get_buffer_manager() orelse return; + if (mv.get_active_buffer()) |buffer| _ = bm.close_others(buffer); + } + pub const bco_meta: Meta = .{ .description = "bco (Close other buffers/tabs, except this one)" }; + pub fn save_selection(_: *void, _: Ctx) Result { const logger = log.logger("helix-mode"); defer logger.deinit(); From d3e601e774b18e2bf177c4742fa1ef8120716636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20T=C3=A1mara?= Date: Fri, 3 Oct 2025 16:07:03 -0500 Subject: [PATCH 2/4] Show feedback to user when trying to quit with dirty buffers --- help.md | 29 ++++++++++++++++++++++++----- src/buffer/Manager.zig | 25 +++++++++++++++++++------ src/tui/mainview.zig | 8 +++++++- src/tui/mode/helix.zig | 9 ++++++++- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/help.md b/help.md index 5db2e7b..757f181 100644 --- a/help.md +++ b/help.md @@ -22,15 +22,23 @@ ctrl+n/ctrl+p or f3/shift+f3 to jump through the matches. Press Enter to exit find mode at the current match or Escape to return to your starting point. +## Messages and logs + +Messages of issues regarding tasks that are not accomplished are +reported to the log view, to open it, use ctrl+shift+p > View log. For +example, when you try to close a buffer that is not saved, or try to +exit Flow without all the buffers saved, it will be reported in the log. + + ## Input Modes Flow Control supports multiple input modes that may be changed interactively at runtime. The current input mode (and some other settings) is persisted in the configuration file automatically. -- f4 => Cycle major input modes (flow, vim, ...) +- f4 => Cycle major input modes (flow, emacs, vim, helix,...) -The current input mode is displayed in the at the left side of the statusbar. +The current input mode is displayed at the left side of the statusbar. - ctrl+shift+p or alt+x => Show the command palette @@ -102,15 +110,26 @@ list of keybindings for this mode. The vim modes, shown as NORMAL, INSERT or VISUAL in the status bar, follow the basic modal editing style of vim. The basics follow vim -closely, but more advanced vim functions (e.g. macrosand registers) +closely, but more advanced vim functions (e.g. macros and registers) are not supported (yet). Keybindings from flow mode that do not conflict with vim keybindings also work in vim mode. +## Helix mode + +The Helix modes, shown as NOR, INS or SEL in the status bar, follow +the basic modal editing style of Helix. The basics are being adapted +closely, more advanced functions (e.g. surround, macros, +selections, registers) are not supported (yet). Usual keybinding +with LSPs are used for tasks like 'go to definition', 'go to reference' +and 'inline documentation' featuring inline diagnostics. Keybindings +from flow mode that do not conflict with helix keybindings also work in +helix mode. + (work in progress) ### Mouse Commands -Mouse commands are not rebindable and are not listed in the command palette. +Mouse commands are NOT rebindable and are not listed in the command palette. - Left Click => Clear all cursors and selections and the place cursor at the mouse pointer @@ -170,7 +189,7 @@ animation_min_lag 0 animation_max_lag 150 ``` -Most of these options are fairly self explanitory. +Most of these options are fairly self explanatory. `theme`, `input_mode` and `show_whitespace` are automatically persisted when changed interactively with keybindings. diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig index 4c4f1e9..1e22ed6 100644 --- a/src/buffer/Manager.zig +++ b/src/buffer/Manager.zig @@ -134,6 +134,19 @@ pub fn is_dirty(self: *const Self) bool { return false; } +pub fn number_of_dirties(self: *const Self) usize { + var dirties: usize = 0; + var i = self.buffers.iterator(); + + while (i.next()) |p| { + const buffer = p.value_ptr.*; + if (!buffer.is_ephemeral() and buffer.is_dirty()) { + dirties += 1; + } + } + return dirties; +} + pub fn is_buffer_dirty(self: *const Self, file_path: []const u8) bool { return if (self.buffers.get(file_path)) |buffer| buffer.is_dirty() else false; } @@ -181,18 +194,18 @@ pub fn delete_others(self: *Self, protected: *Buffer) void { } pub fn close_others(self: *Self, protected: *Buffer) usize { - var removed: usize = 0; + var remaining: usize = 0; var i = self.buffers.iterator(); - if (self.is_dirty()) return 0; while (i.next()) |p| { const buffer = p.value_ptr.*; if (buffer != protected) { - if (self.buffers.remove(buffer.get_file_path())) - removed += 1; - buffer.deinit(); + if (buffer.is_ephemeral() or !buffer.is_dirty()) { + _ = self.buffers.remove(buffer.get_file_path()); + buffer.deinit(); + } else remaining += 1; } } - return removed; + return remaining; } pub fn buffer_from_ref(self: *Self, buffer_ref: usize) ?*Buffer { diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 530e8cf..def6ac9 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -273,7 +273,13 @@ const cmds = struct { const Result = command.Result; pub fn quit(self: *Self, _: Ctx) Result { - try self.check_all_not_dirty(); + const logger = log.logger("buffer"); + defer logger.deinit(); + self.check_all_not_dirty() catch |err| { + const dirties = self.buffer_manager.number_of_dirties(); + logger.print("There are {} unsaved buffer(s), use 'quit without saving' if not needed to save them", .{dirties}); + return err; + }; try tp.self_pid().send("quit"); } pub const quit_meta: Meta = .{ .description = "Quit" }; diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 810ce15..6ba7c03 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -163,9 +163,16 @@ const cmds_ = struct { pub const @"bco!_meta": Meta = .{ .description = "bco! (Close other buffers/tabs forcefully, ignoring changes)" }; pub fn bco(_: *void, _: Ctx) Result { + const logger = log.logger("helix-mode"); + defer logger.deinit(); const mv = tui.mainview() orelse return; const bm = tui.get_buffer_manager() orelse return; - if (mv.get_active_buffer()) |buffer| _ = bm.close_others(buffer); + if (mv.get_active_buffer()) |buffer| { + const remaining = bm.close_others(buffer); + if (remaining > 0) { + logger.print("{} unsaved buffer(s) remaining", .{remaining}); + } + } } pub const bco_meta: Meta = .{ .description = "bco (Close other buffers/tabs, except this one)" }; From e41ff1b7a55c63f0b9213af45c3bfe28bca2d93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20T=C3=A1mara?= Date: Fri, 3 Oct 2025 17:50:10 -0500 Subject: [PATCH 3/4] Add flags and options in help and paraphrased minilog and log usage --- help.md | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/help.md b/help.md index 757f181..ca0e993 100644 --- a/help.md +++ b/help.md @@ -15,6 +15,7 @@ kitty_mod ctrl+alt For other editors you will probably have to disable or rebind them each individually. + ## Searching Press ctrl+f to search this help file. Type a search term and press @@ -22,12 +23,15 @@ ctrl+n/ctrl+p or f3/shift+f3 to jump through the matches. Press Enter to exit find mode at the current match or Escape to return to your starting point. + ## Messages and logs -Messages of issues regarding tasks that are not accomplished are -reported to the log view, to open it, use ctrl+shift+p > View log. For -example, when you try to close a buffer that is not saved, or try to -exit Flow without all the buffers saved, it will be reported in the log. +Messages of issues regarding tasks that are not accomplished, like +trying to close flow with unsaved files, as well as other information +are shown briefly in the bottom status bar; most recent messages can +be seen in the log view too, to open it, use ctrl+shift+p > `View log`; +it's possible to make it taller dragging the toolbar with the mouse +up or downwards. ## Input Modes @@ -95,18 +99,19 @@ Multiple inheritance is supported with the `inherits` options like this: ... ``` -## Flow mode +### Flow mode The default input mode, called just flow, is based on common GUI programming editors. It most closely resembles Visual Studio Code, but also takes some inspiration from Emacs and others. This mode focuses -on powerful multi cursor support with a find -> select -> modify +on powerful multi cursor support with a find -> select -> modify cycle style of editing. See the `ctrl+f2` palette when flow mode is selected to see the full list of keybindings for this mode. -## Vim mode + +### Vim mode The vim modes, shown as NORMAL, INSERT or VISUAL in the status bar, follow the basic modal editing style of vim. The basics follow vim @@ -114,20 +119,21 @@ closely, but more advanced vim functions (e.g. macros and registers) are not supported (yet). Keybindings from flow mode that do not conflict with vim keybindings also work in vim mode. -## Helix mode -The Helix modes, shown as NOR, INS or SEL in the status bar, follow -the basic modal editing style of Helix. The basics are being adapted -closely, more advanced functions (e.g. surround, macros, -selections, registers) are not supported (yet). Usual keybinding -with LSPs are used for tasks like 'go to definition', 'go to reference' -and 'inline documentation' featuring inline diagnostics. Keybindings +### Helix mode + +The helix modes, shown as NOR, INS or SEL in the status bar, follow +the basic modal editing style of helix. The basics are being adapted +closely, more advanced functions (e.g. surround, macros, selections, +registers) are not supported (yet). Usual keybinding with LSPs are +used for tasks like 'go to definition', 'go to reference' and +'inline documentation' featuring inline diagnostics. Keybindings from flow mode that do not conflict with helix keybindings also work in helix mode. (work in progress) -### Mouse Commands +## Mouse Commands Mouse commands are NOT rebindable and are not listed in the command palette. @@ -204,3 +210,9 @@ animation altogether. File types may be configured with the `Edit file type configuration` command. You can also create a new file type by adding a new `.conf` file to the `file_type` directory. Have a look at an existing file type to see what options are available. + +## Flags and options + +As every respectable terminal program, flow provide various invoking +options that among others, will allow you to inspect various aspects of +the running session. Feel free to run `flow --help` to explore them. From f201728457aae12a7ba1823a79a8dce7179eafe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20T=C3=A1mara?= Date: Mon, 6 Oct 2025 13:06:22 -0500 Subject: [PATCH 4/4] hx: Fix closing other buffers and improve user messages --- src/buffer/Manager.zig | 47 ++++++++++++++++++++++++++++-------------- src/tui/mainview.zig | 4 ++-- src/tui/mode/helix.zig | 21 ++++++++++--------- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig index 1e22ed6..309b25d 100644 --- a/src/buffer/Manager.zig +++ b/src/buffer/Manager.zig @@ -134,17 +134,17 @@ pub fn is_dirty(self: *const Self) bool { return false; } -pub fn number_of_dirties(self: *const Self) usize { - var dirties: usize = 0; +pub fn count_dirty_buffers(self: *const Self) usize { + var count: usize = 0; var i = self.buffers.iterator(); while (i.next()) |p| { const buffer = p.value_ptr.*; if (!buffer.is_ephemeral() and buffer.is_dirty()) { - dirties += 1; + count += 1; } } - return dirties; + return count; } pub fn is_buffer_dirty(self: *const Self, file_path: []const u8) bool { @@ -182,29 +182,46 @@ pub fn delete_all(self: *Self) void { self.buffers.clearRetainingCapacity(); } -pub fn delete_others(self: *Self, protected: *Buffer) void { - var i = self.buffers.iterator(); - while (i.next()) |p| { +pub fn delete_others(self: *Self, protected: *Buffer) error{OutOfMemory}!void { + var keys = try std.ArrayList(*[]const u8).initCapacity(self.allocator, self.buffers.size); + defer keys.deinit(self.allocator); + + var it = self.buffers.iterator(); + + while (it.next()) |p| { const buffer = p.value_ptr.*; if (buffer != protected) { - buffer.reset_to_last_saved(); - self.close_buffer(buffer); + try keys.append(self.allocator, p.key_ptr); } } + for (keys.items) |k| { + const buffer = self.buffers.get(k.*) orelse continue; + _ = self.buffers.remove(k.*); + buffer.deinit(); + } } -pub fn close_others(self: *Self, protected: *Buffer) usize { +pub fn close_others(self: *Self, protected: *Buffer) error{OutOfMemory}!usize { var remaining: usize = 0; - var i = self.buffers.iterator(); - while (i.next()) |p| { + var keys = try std.ArrayList(*[]const u8).initCapacity(self.allocator, self.buffers.size); + defer keys.deinit(self.allocator); + + var it = self.buffers.iterator(); + while (it.next()) |p| { const buffer = p.value_ptr.*; if (buffer != protected) { if (buffer.is_ephemeral() or !buffer.is_dirty()) { - _ = self.buffers.remove(buffer.get_file_path()); - buffer.deinit(); - } else remaining += 1; + try keys.append(self.allocator, p.key_ptr); + } else { + remaining += 1; + } } } + for (keys.items) |k| { + const buffer = self.buffers.get(k.*) orelse continue; + _ = self.buffers.remove(k.*); + buffer.deinit(); + } return remaining; } diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index def6ac9..0a53846 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -276,8 +276,8 @@ const cmds = struct { const logger = log.logger("buffer"); defer logger.deinit(); self.check_all_not_dirty() catch |err| { - const dirties = self.buffer_manager.number_of_dirties(); - logger.print("There are {} unsaved buffer(s), use 'quit without saving' if not needed to save them", .{dirties}); + const count_dirty_buffers = self.buffer_manager.count_dirty_buffers(); + logger.print("{} unsaved buffer(s), use 'quit without saving' to exit", .{count_dirty_buffers}); return err; }; try tp.self_pid().send("quit"); diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 6ba7c03..9e1abe9 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -63,7 +63,7 @@ const cmds_ = struct { pub fn @"x!"(_: *void, _: Ctx) Result { try cmd("save_file", command.fmt(.{ "then", .{ "quit_without_saving", .{} } })); } - pub const @"x!_meta": Meta = .{ .description = "x! (write/save file and close forcefully, ignoring other unsaved changes)" }; + pub const @"x!_meta": Meta = .{ .description = "x! (write/save file and exit, ignoring other unsaved changes)" }; pub fn x(_: *void, _: Ctx) Result { try cmd("save_file", command.fmt(.{ "then", .{ "quit", .{} } })); @@ -90,7 +90,7 @@ const cmds_ = struct { try cmd("quit_without_saving", .{}); } } - pub const @"xa!_meta": Meta = .{ .description = "xa! (write all and quit forcefully, ignoring unsaved changes)" }; + pub const @"xa!_meta": Meta = .{ .description = "xa! (write all and exit, ignoring other unsaved changes)" }; pub fn wqa(_: *void, _: Ctx) Result { if (tui.get_buffer_manager()) |bm| @@ -105,18 +105,18 @@ const cmds_ = struct { try cmd("quit_without_saving", .{}); } } - pub const @"wqa!_meta": Meta = .{ .description = "wqa! (write all and quit forcefully, ignoring unsaved changes)" }; + pub const @"wqa!_meta": Meta = .{ .description = "wqa! (write all and exit, ignoring unsaved changes)" }; pub fn rl(_: *void, _: Ctx) Result { try cmd("reload_file", .{}); } - pub const rl_meta: Meta = .{ .description = "rl (force reload current file)" }; + pub const rl_meta: Meta = .{ .description = "rl (reload current file)" }; pub fn rla(_: *void, _: Ctx) Result { if (tui.get_buffer_manager()) |bm| bm.reload_all() catch |e| return tp.exit_error(e, @errorReturnTrace()); } - pub const rla_meta: Meta = .{ .description = "rla (reload all files discarding the current contents)" }; + pub const rla_meta: Meta = .{ .description = "rla (reload all files)" }; pub fn o(_: *void, _: Ctx) Result { try cmd("open_file", .{}); @@ -152,15 +152,15 @@ const cmds_ = struct { pub fn @"bc!"(_: *void, _: Ctx) Result { try cmd("close_file_without_saving", .{}); } - pub const @"bc!_meta": Meta = .{ .description = "bc! (Close buffer/tab forcefully, ignoring changes)" }; + pub const @"bc!_meta": Meta = .{ .description = "bc! (Close buffer/tab, ignoring changes)" }; pub fn @"bco!"(_: *void, _: Ctx) Result { const mv = tui.mainview() orelse return; if (tui.get_buffer_manager()) |bm| { - if (mv.get_active_buffer()) |buffer| bm.delete_others(buffer); + if (mv.get_active_buffer()) |buffer| try bm.delete_others(buffer); } } - pub const @"bco!_meta": Meta = .{ .description = "bco! (Close other buffers/tabs forcefully, ignoring changes)" }; + pub const @"bco!_meta": Meta = .{ .description = "bco! (Close other buffers/tabs, discarding changes)" }; pub fn bco(_: *void, _: Ctx) Result { const logger = log.logger("helix-mode"); @@ -168,13 +168,14 @@ const cmds_ = struct { const mv = tui.mainview() orelse return; const bm = tui.get_buffer_manager() orelse return; if (mv.get_active_buffer()) |buffer| { - const remaining = bm.close_others(buffer); + const remaining = try bm.close_others(buffer); if (remaining > 0) { logger.print("{} unsaved buffer(s) remaining", .{remaining}); + try cmd("next_tab", .{}); } } } - pub const bco_meta: Meta = .{ .description = "bco (Close other buffers/tabs, except this one)" }; + pub const bco_meta: Meta = .{ .description = "bco (Close other buffers/tabs)" }; pub fn save_selection(_: *void, _: Ctx) Result { const logger = log.logger("helix-mode");