Compare commits

...

4 commits

Author SHA1 Message Date
Igor Támara
f201728457 hx: Fix closing other buffers and improve user messages 2025-10-06 21:16:33 +02:00
Igor Támara
e41ff1b7a5 Add flags and options in help and paraphrased minilog and log usage 2025-10-06 21:16:33 +02:00
Igor Támara
d3e601e774 Show feedback to user when trying to quit with dirty buffers 2025-10-06 21:16:33 +02:00
Igor Támara
552417091d feat: add helix mode close other buffers and reload all
* x and x! have expected behaviour in Helix mode
2025-10-06 21:16:33 +02:00
4 changed files with 152 additions and 23 deletions

49
help.md
View file

@ -15,6 +15,7 @@ kitty_mod ctrl+alt
For other editors you will probably have to disable or rebind them each For other editors you will probably have to disable or rebind them each
individually. individually.
## Searching ## Searching
Press ctrl+f to search this help file. Type a search term and press Press ctrl+f to search this help file. Type a search term and press
@ -22,15 +23,26 @@ 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 to exit find mode at the current match or Escape to return to your
starting point. starting point.
## Messages and logs
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 ## Input Modes
Flow Control supports multiple input modes that may be changed Flow Control supports multiple input modes that may be changed
interactively at runtime. The current input mode (and some other interactively at runtime. The current input mode (and some other
settings) is persisted in the configuration file automatically. 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 - ctrl+shift+p or alt+x => Show the command palette
@ -87,30 +99,43 @@ 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 The default input mode, called just flow, is based on common GUI
programming editors. It most closely resembles Visual Studio Code, but programming editors. It most closely resembles Visual Studio Code, but
also takes some inspiration from Emacs and others. This mode focuses 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. cycle style of editing.
See the `ctrl+f2` palette when flow mode is selected to see the full See the `ctrl+f2` palette when flow mode is selected to see the full
list of keybindings for this mode. list of keybindings for this mode.
## Vim mode
### Vim mode
The vim modes, shown as NORMAL, INSERT or VISUAL in the status bar, 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 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 are not supported (yet). Keybindings from flow mode that do not conflict
with vim keybindings also work in vim mode. 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) (work in progress)
### Mouse Commands ## 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 => - Left Click =>
Clear all cursors and selections and the place cursor at the mouse pointer Clear all cursors and selections and the place cursor at the mouse pointer
@ -170,7 +195,7 @@ animation_min_lag 0
animation_max_lag 150 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 `theme`, `input_mode` and `show_whitespace` are automatically
persisted when changed interactively with keybindings. persisted when changed interactively with keybindings.
@ -185,3 +210,9 @@ animation altogether.
File types may be configured with the `Edit file type configuration` command. You 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` 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. 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.

View file

@ -134,6 +134,19 @@ pub fn is_dirty(self: *const Self) bool {
return false; return false;
} }
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()) {
count += 1;
}
}
return count;
}
pub fn is_buffer_dirty(self: *const Self, file_path: []const u8) bool { 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; return if (self.buffers.get(file_path)) |buffer| buffer.is_dirty() else false;
} }
@ -149,6 +162,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 { pub fn delete_all(self: *Self) void {
var i = self.buffers.iterator(); var i = self.buffers.iterator();
while (i.next()) |p| { while (i.next()) |p| {
@ -158,6 +182,49 @@ pub fn delete_all(self: *Self) void {
self.buffers.clearRetainingCapacity(); self.buffers.clearRetainingCapacity();
} }
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) {
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) error{OutOfMemory}!usize {
var remaining: usize = 0;
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()) {
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;
}
pub fn buffer_from_ref(self: *Self, buffer_ref: usize) ?*Buffer { pub fn buffer_from_ref(self: *Self, buffer_ref: usize) ?*Buffer {
var i = self.buffers.iterator(); var i = self.buffers.iterator();
while (i.next()) |p| while (i.next()) |p|

View file

@ -273,7 +273,13 @@ const cmds = struct {
const Result = command.Result; const Result = command.Result;
pub fn quit(self: *Self, _: Ctx) 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 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"); try tp.self_pid().send("quit");
} }
pub const quit_meta: Meta = .{ .description = "Quit" }; pub const quit_meta: Meta = .{ .description = "Quit" };

View file

@ -60,20 +60,16 @@ const cmds_ = struct {
} }
pub const wq_meta: Meta = .{ .description = "wq (write/save file and quit)" }; 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 exit, ignoring other unsaved changes)" };
pub fn x(_: *void, _: Ctx) Result { pub fn x(_: *void, _: Ctx) Result {
try cmd("save_file", command.fmt(.{ "then", .{ "quit", .{} } })); try cmd("save_file", command.fmt(.{ "then", .{ "quit", .{} } }));
} }
pub const x_meta: Meta = .{ .description = "x (write/save file and 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 { pub fn wa(_: *void, _: Ctx) Result {
if (tui.get_buffer_manager()) |bm| if (tui.get_buffer_manager()) |bm|
bm.save_all() catch |e| return tp.exit_error(e, @errorReturnTrace()); bm.save_all() catch |e| return tp.exit_error(e, @errorReturnTrace());
@ -94,7 +90,7 @@ const cmds_ = struct {
try cmd("quit_without_saving", .{}); 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 { pub fn wqa(_: *void, _: Ctx) Result {
if (tui.get_buffer_manager()) |bm| if (tui.get_buffer_manager()) |bm|
@ -109,12 +105,18 @@ const cmds_ = struct {
try cmd("quit_without_saving", .{}); 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 { pub fn rl(_: *void, _: Ctx) Result {
try cmd("reload_file", .{}); 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)" };
pub fn o(_: *void, _: Ctx) Result { pub fn o(_: *void, _: Ctx) Result {
try cmd("open_file", .{}); try cmd("open_file", .{});
@ -150,7 +152,30 @@ const cmds_ = struct {
pub fn @"bc!"(_: *void, _: Ctx) Result { pub fn @"bc!"(_: *void, _: Ctx) Result {
try cmd("close_file_without_saving", .{}); 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| try bm.delete_others(buffer);
}
}
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");
defer logger.deinit();
const mv = tui.mainview() orelse return;
const bm = tui.get_buffer_manager() orelse return;
if (mv.get_active_buffer()) |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)" };
pub fn save_selection(_: *void, _: Ctx) Result { pub fn save_selection(_: *void, _: Ctx) Result {
const logger = log.logger("helix-mode"); const logger = log.logger("helix-mode");