To get the most of this section, it’s recommended to have read the architecture briefing, about commands and keybinds at least.
A word of warning: Flow code evolves and it is possible that some code exposed here is older than the current one. If in doubt, always refer to master.
The editor coordinates visualization and modification of buffer contents, multiple cursors, selections and marks.
We will delve in editor concepts, buffer inner manipulation with ropes is not covered here.
During this section we will refer to the concept of the editor as the one capable of modifying buffer contents, the visible gutters and line numbers as other terminal user interface (TUI) is covered in another chapter.
View holds the information about the area of the buffer that is currently visible in the screen by the user. Is related to the primary cursor.
The primary Cursor holds a position in the Buffer, the Editor makes the link between both of them, signaling the part of the Buffer that can be modified and manipulated as you see it. It scrolls on the current visible portion view of the buffer, when manipulated with the keyboard, the mouse on its own can move the view while the primary mouse is off-screen; when keystrokes arrive to the editor, the view focuses to the primary cursor to make it visible and available to be used.
Flow has multiple cursors each one holding the relative position to the buffer with row, col and target, the last one used to make the cursor jump to a possibly next movement,for example, when moving between lines, this is a way to “remember” where to jump back. When creating multiple cursors they signal many buffer places and a subset is seen inside the view.
Cursors visibility depends on the size of both the buffer contents and the editor in your device.
Most of editors operations act on the set of CurSels and the Primary Cursor is really a CurSel.
A selection is represented by begin and end cursors and offers basic functions that will constitute the changes needed with deletions, insert replacements handled by theeditor services and commands.
A Selection has two cursors that are not visible, they mark the begin and the end of the selection.
The CurSel is what is presented to user, holding a cursor and optionally a selection which allows to have the concept of a cursor with a selection.
To complete the editor scenario, Marks have the potential to become selections; the marks become evident to the eye when in search mode, they are seen as the primary cursor is positioned over an occurrence with a different color according to the theme.
The Editor will be acting on Buffer.Root which is the root of the tree representing the document that is being edited. API Buffer.Root is stable and offers the necessary to insert, delete and move along the buffer, knowing if the end or the beginning of the document has been reached when interacting with a Cursor.
Cursors, Selections and Cursels don’t know about the buffer, and they need to receive a reference to it in order to be aware of the restrictions and usually receive metrics from the editor that help determine if a cursor is about to exit the boundaries of the buffer and be inside bounds.
We mentioned earlier that most of the operations work on all the cursors and selections, moreover, there are various commands acting over the set of cursors, selections, cursels or marks. Given said this, we will be using functions as parameters in most of the situations. Functional programming languages are popular in these scenarios, to name a prominent one, emacs lisp from Emacs. Flow is inspired on it and others.
If the buffer is not to be modified, we will be using the method buf_root to get the root of the buffer to find and position the cursors. In the other hand, we will use buf_for_update when the buffer is to be modified.
The benefit of sending functions as parameters is that code is reused and the codebase can grow without much sacrifice when adding new functionalities, because one would be thinking only in the current cursor and if required, the operation will be applied to all the cursors, the same applies to selections, cursels and marks.
For example, to move the cursors a page up, we can look at the command move_page_up,
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: Meta = .{ .description = "Move cursor page up" };
which uses the method with_cursors_and_view_const sending the function move_cursor_page_up.
Looking inside with_cursors_and_view_const
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 {};
}
It iterates over all the cursors and invokes with_cursor_and_view_const, sending the move function, a cursor; remember that the function passed was move_cursor_page_up as shown previously.
The commitment of move_cursor_page_up is to use the method move_page_up from Cursor, sending editor view and 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);
}
The move functions set is numerous, look for the methods whose name have the word move in them. With the command palette is possible to get a glimpse of the available functions present(ctrl+f2).
Function naming conventions help understand the purpose of each one, there has been taken great deal of care to maintain consistency, it’s unlikely that there is need for new functions, in case of need, add it following what is already present.
Follows an example of a functionality that receives a repetition parameter, the select_up command, which given a number, takes all the current cursors and selections and select lines above them the number of times specified.
pub fn select_up(self: *Self, ctx: Context) Result {
const root = try self.buf_root();
try self.with_selections_const_repeat(root, move_cursor_up, ctx);
self.clamp();
}
pub const select_up_meta: Meta = .{ .description = "Select up", .arguments = &.{.integer} };
It’s meta is guiding about the integer argument that the command can make use of. Invokes with_selections_const_repeat passing the function move_cursor_up as parameter and also the context, which actually holds the integer parameter holding the repetition information.
pub fn with_selections_const_repeat(self: *Self, root: Buffer.Root, move: cursor_operator_const, ctx: Context) error{Stop}!void {
var someone_stopped = false;
var repeat: usize = 1;
_ = ctx.args.match(.{tp.extract(&repeat)}) catch false;
while (repeat > 0) : (repeat -= 1) {
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel|
with_selection_const(root, move, cursel, self.metrics) catch {
someone_stopped = true;
};
self.collapse_cursors();
if (someone_stopped) break;
}
return if (someone_stopped) error.Stop else {};
}
Factor repetition is taken from the context in line XX, being used in the while, then for each repetition, the set of cursels is iterated, cursels hold cursors and selections, applying with_selection_const function, passing root(the buffer), move_cursor_up function, cursel and metrics as parameters. Note that after the function is applied to each cursel, self.collapse_cursors() is invoked, given that it’s possible that some cursors in the operation have overlapped.
With each cursel operation there is also a tracking of possible fails, continuing gracefully, one of such fails can be happening because some cursels might have been reaching the beginning of the buffer and there might be other cursels that have not yet reached the beginning of the file.
pub fn with_selection_const(root: Buffer.Root, move: cursor_operator_const, cursel: *CurSel, metrics: Buffer.Metrics) error{Stop}!void {
const sel = try cursel.enable_selection(root, metrics);
try move(root, &sel.end, metrics);
cursel.cursor = sel.end;
cursel.check_selection(root, metrics);
}
If there are cursels that are not yet selections, their respective selections are activated with cursel.enable_selection, finally move_cursor_up (moves value is a pointer to the move_cursor_up) is applied, passing the end marker of the selection; with its new value, the cursor is positioned and finally a sanity check on cursel is applied with its method check_selection.
Flow allows navigating the code with goto_definition and references commands to deepen the understanding and following the different calls.
The select family of functions is bigger than the set of move functions, in contrast, the cut, delete, insert, paste set looks smaller, and this is accomplished making composition of functions.
Usually when modifying something, first there is a process to locate the cursor, cursel or selection in the proper place and then applying the modification. There are cases when there is need to locate and act right away.
When additions, changes and removal of buffer contents, editor offers the method buf_for_update; we will look first to the method to_upper_cursel, which by default in Flow, takes a CurSel; if it has no selection, the word that it is stepped on, if any, is selected and gets uppercased.
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(root, self.metrics) catch return error.Stop;
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 sfa_allocator = sfa.get();
const cut_text = copy_selection(root, sel.*, sfa_allocator, self.metrics) catch return error.Stop;
defer sfa_allocator.free(cut_text);
const ucased = Buffer.unicode.get_letter_casing().toUpperStr(sfa_allocator, cut_text) catch return error.Stop;
defer sfa_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;
}
Lines through 4 to 9 implement the selection description sketched previously, selecting the word if no selection was present to apply uppercasing.
And to reflect the changes in the buffer from lines 10 to 19:
buffer(root) is received, modified and finally returned.As we saw, the method to_upper_cursel acts on a cursel, used by the to_upper command, which acts on all the present cursels, getting the buffer with the method buf_for_update to make modifications on it.
pub fn to_upper(self: *Self, _: Context) Result {
const b = try self.buf_for_update();
const root = try self.with_cursels_mut_once(b.root, to_upper_cursel, b.allocator);
try self.update_buf(root);
self.clamp();
}
pub const to_upper_meta: Meta = .{ .description = "Convert selection or word to upper case" };
with_cursels_mut_once receives a buffer that will be modified and returns one with modifications, updating what is presented with the method update_buf and an allocator. The previously described function to_upper_cursel is sent
fn with_cursels_mut_once(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;
}
Worth to note that the modified root is being passed to the function with_cursel_mut on each iteration over the cursels along with the allocator and as described previously in selection functions, tracking if there is some fail an error is popped up, whilst everything was successful, the modified root is returned.
Now time to see the method with_cursel_mut that receives the pointer to to_upper_cursel along with root, cursel and allocator.
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);
}
As seen, the only task for with_cursel_mut is providing the required elements to let to_upper_cursel do what was described previously.
As shown, the basics to manipulate the buffer via the editor involves reviewing if cursors, marks, selections are to be modified, changed or if the contents are. Picking functions for it and focusing on the task for a single cursel, cursor or selection is key and the use of the already present functions will allow to act on multiple elements at once. By convention functions with name mut will modify the buffer while const will not.