395 lines
No EOL
16 KiB
Text
395 lines
No EOL
16 KiB
Text
---
|
|
.title = "Editor",
|
|
.date = @date("2025-10-19T00:00:00"),
|
|
.author = "Igor Támara",
|
|
.layout = "tutorial.shtml",
|
|
.draft = false,
|
|
.custom = {
|
|
.githubedit = "docs/architecture/editor.smd",
|
|
.codepath = "src/tui/editor.zig",
|
|
},
|
|
---
|
|
|
|
To get the most of this section, it's recommended to have read the
|
|
[architecture briefing](/docs/architecture), about
|
|
[commands](/docs/architecture/command) and
|
|
[keybinds](/docs/architecture/keybind) 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](https://github.com/neurocyte/flow).
|
|
|
|
[]($section.id("concepts"))
|
|
## Some concepts
|
|
|
|
The [editor](#editor_concept) coordinates visualization and
|
|
modification of buffer contents, multiple [cursors](#cursor_concept),
|
|
[selections](#selection_concept) and [marks](#mark_concept).
|
|
|
|
We will delve in editor concepts, buffer inner manipulation with ropes
|
|
is not covered here.
|
|
|
|
[]($section.id("tui_editor"))
|
|
### Editor and TUI
|
|
|
|
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.
|
|
|
|
[]($section.id("view_concept"))
|
|
### View
|
|
|
|
`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](#cursor_concept).
|
|
|
|
[]($section.id("cursor_concept"))
|
|
### 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](#view_concept) 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](#view_concept).
|
|
|
|
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](#cursel_concept).
|
|
|
|
[]($section.id("selection_concept"))
|
|
### Selection
|
|
|
|
A selection is represented by begin and end [cursors](#cursor_concept)
|
|
and offers basic functions that will constitute the changes needed with
|
|
deletions, insert replacements handled by the[editor](#tui_editor)
|
|
services and [commands](/docs/architecture/command).
|
|
|
|
A `Selection` has two cursors that are not visible, they mark the begin
|
|
and the end of the selection.
|
|
|
|
[]($section.id("cursel_concept"))
|
|
### CurSel
|
|
|
|
The CurSel is what is presented to user, holding a
|
|
[cursor](#cursor_concept) and optionally a
|
|
[selection](#selection_concept) which allows to have the concept of a
|
|
cursor with a selection.
|
|
|
|
[]($section.id("mark_concept"))
|
|
### Mark
|
|
|
|
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.
|
|
|
|
[]($section.id("editor_concept"))
|
|
### Editor
|
|
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.
|
|
|
|
[]($section.id("commands"))
|
|
## Editor Commands
|
|
|
|
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.
|
|
|
|
[]($section.id("moving"))
|
|
## Moving
|
|
|
|
For example, to move the cursors a page up, we can look at the
|
|
[command](docs/architecture/command)
|
|
`move_page_up`,
|
|
|
|
>[]($block.attrs('line-numbers-js'))
|
|
>```zig
|
|
>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`
|
|
|
|
>[]($block.attrs('line-numbers-js'))
|
|
>```zig
|
|
>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`.
|
|
|
|
```zig
|
|
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).
|
|
|
|
[]($section.id("selecting"))
|
|
## Selections
|
|
|
|
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.
|
|
|
|
```zig
|
|
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.
|
|
|
|
|
|
>[]($block.attrs('line-numbers-js'))
|
|
>```zig
|
|
>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.
|
|
|
|
```zig
|
|
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` (`move`s 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.
|
|
|
|
[]($section.id("modifying"))
|
|
## Modifying the buffer
|
|
|
|
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.
|
|
|
|
>[]($block.attrs('line-numbers-js'))
|
|
>```zig
|
|
>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:
|
|
|
|
1. copy contents,
|
|
1. uppercase
|
|
1. remove the selection,
|
|
1. and add the uppercased in the position where the cut was made.
|
|
making sure the cursel is positioned in the expected place.
|
|
1. A `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.
|
|
|
|
```zig
|
|
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
|
|
|
|
>[]($block.attrs('line-numbers-js'))
|
|
>```zig
|
|
> 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.
|
|
|
|
```zig
|
|
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.
|
|
|
|
## Adding functionalities to the editor
|
|
|
|
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.
|
|
|
|
[]($section.id("next"))
|
|
## Next steps
|
|
|
|
* [minimodes](/docs/architecture/minimode) invoke
|
|
[commands](/docs/architecture/minimode) defined in the editor
|
|
* [palettes](/docs/architecture/palette) are open by commands and when
|
|
selected an item, a command is invoked
|
|
* Plenty of [commands](/docs/architecture/command) are defined in the
|
|
editor
|
|
* [Passing parameters](/docs/architecture/inner_data_exchange) between
|
|
commands and functions |