I am trying to implement vertical cursor movement (up and down arrow keys) in my custom nano-like text editor, but I could not get it working properly and keep introducing new bugs. My text editor uses a gap-buffer to store the text and a single variable pos_idx to handle the insert position. It is the job of the render function to convert that linear cursor position to x and y coordinates. The left and right arrow keys work just fine, as they only need to increment or decrement pos_idx.
main.zig
const std = @import("std");
const ncurses = @cImport({
@cInclude("ncurses.h");
});
const buffer = @import("gap_buffer.zig");
const editor = @import("editor.zig");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
_ = gpa.detectLeaks();
_ = gpa.deinit();
}
const alloc = gpa.allocator();
_ = ncurses.initscr();
defer _ = ncurses.endwin();
_ = ncurses.keypad(ncurses.stdscr, true);
_ = ncurses.noecho();
_ = ncurses.raw();
_ = ncurses.refresh();
var e = try editor.Editor.init(alloc);
defer e.deinit();
var char: c_int = 0;
while (char != 'q' & 0x1f) {
char = ncurses.getch();
try e.handle_key(char);
try e.render();
}
}
editor.zig
const std = @import("std");
const gap_buffer = @import("gap_buffer.zig");
const ncurses = @cImport({
@cInclude("ncurses.h");
});
pub const Editor = struct {
buf: gap_buffer.GapBuffer,
alloc: std.mem.Allocator,
render_buf: []u8,
pos_idx: usize = 0,
render_buf_dirty: bool = false,
pub fn init(allocator: std.mem.Allocator) std.mem.Allocator.Error!Editor {
return Editor{ .buf = try gap_buffer.GapBuffer.init(8, allocator), .alloc = allocator, .render_buf = try allocator.alloc(u8, 9) };
}
pub fn deinit(self: *Editor) void {
self.buf.deinit();
self.alloc.free(self.render_buf);
}
pub fn handle_key(self: *Editor, key: c_int) std.mem.Allocator.Error!void {
if (key == ncurses.KEY_LEFT) {
if (self.pos_idx > 0)
self.pos_idx -= 1;
} else if (key == ncurses.KEY_RIGHT) {
if (self.pos_idx < self.buf.buf.len - self.buf.gap_len)
self.pos_idx += 1;
} else if (key == ncurses.KEY_UP) {
try self.handle_up();
} else if (key == ncurses.KEY_DOWN) {
try self.handle_down();
} else if (key <= 255) {
self.render_buf_dirty = true;
try self.insert(@intCast(key));
}
}
pub fn render(self: *Editor) std.mem.Allocator.Error!void {
const t = self.buf;
if (t.gap_len == t.buf.len) return; // nothing to render
try self.refresh_render_buffer();
var y: u64 = 0;
var line_begin_idx: ?usize = null;
var line_len: usize = 0;
for (self.render_buf[0..self.pos_idx], 0..) |c, i| {
if (c == '\n') {
y += 1;
line_begin_idx = i;
} else {
line_len += 1;
}
}
var x: usize = undefined;
if (line_begin_idx == null) {
x = self.pos_idx;
} else if (self.pos_idx == line_begin_idx.?) {
x = line_len;
} else {
x = self.pos_idx - line_begin_idx.? - 1;
}
_ = ncurses.clear();
_ = ncurses.mvprintw(0, 0, @ptrCast(self.render_buf));
_ = ncurses.move(@intCast(y), @intCast(x));
_ = ncurses.refresh();
}
fn insert(self: *Editor, char: u8) std.mem.Allocator.Error!void {
self.buf.set_pos_idx(self.pos_idx);
try self.buf.insert(char);
self.pos_idx += 1;
}
fn handle_up(self: *Editor) std.mem.Allocator.Error!void {
try self.refresh_render_buffer();
var curr_line_idx: usize = std.mem.lastIndexOf(u8, self.render_buf[0..self.pos_idx], "\n") orelse return; //
var x: usize = undefined;
// this is an edge-case, where we are on a newline
if (self.render_buf[self.pos_idx] == '\n') {
x = 0;
curr_line_idx = self.pos_idx;
} else {
x = self.pos_idx - curr_line_idx - 1;
}
const prev_line_idx: ?usize = std.mem.lastIndexOf(u8, self.render_buf[0..curr_line_idx], "\n");
if (prev_line_idx) |prev_idx| {
self.pos_idx = prev_idx + @min(x, curr_line_idx - prev_idx - 1);
return;
}
self.pos_idx = @min(x, curr_line_idx);
}
fn handle_down(self: *Editor) std.mem.Allocator.Error!void {
try self.refresh_render_buffer();
}
fn expand_render_buffer(self: *Editor) std.mem.Allocator.Error!void {
const t = self.buf;
const l = t.buf.len - t.gap_len;
if (self.render_buf.len >= l + 1)
return;
self.alloc.free(self.render_buf);
self.render_buf = try self.alloc.alloc(u8, l * 2 + 1);
}
fn refresh_render_buffer(self: *Editor) std.mem.Allocator.Error!void {
if (!self.render_buf_dirty)
return;
const t = self.buf;
try self.expand_render_buffer();
@memcpy(self.render_buf[0..t.gap_begin_idx], t.buf[0..t.gap_begin_idx]);
@memcpy(self.render_buf[t.gap_begin_idx .. t.buf.len - t.gap_len], t.buf[t.gap_begin_idx + t.gap_len ..]);
self.render_buf[t.buf.len - t.gap_len] = 0; // null terminator for c-printing
self.render_buf_dirty = false;
}
};
gap_buffer.zig
const std = @import("std");
pub const GapBuffer = struct {
gap_begin_idx: usize,
gap_len: usize,
pos_idx: usize,
buf: []u8,
alloc: std.mem.Allocator,
pub fn init(gap_size: usize, allocator: std.mem.Allocator) std.mem.Allocator.Error!GapBuffer {
const buffer = try allocator.alloc(u8, gap_size);
return GapBuffer{ .gap_begin_idx = 0, .gap_len = gap_size, .buf = buffer, .pos_idx = 0, .alloc = allocator };
}
pub fn deinit(self: *GapBuffer) void {
self.alloc.free(self.buf);
}
pub fn insert(self: *GapBuffer, char: u8) std.mem.Allocator.Error!void {
if (self.gap_len == 0)
try self.expand_gap();
self.move_gap_to_cursor();
self.buf[self.pos_idx] = char;
self.gap_begin_idx += 1;
self.gap_len -= 1;
}
pub fn set_pos_idx(self: *GapBuffer, idx: usize) void {
if (idx <= self.gap_begin_idx) {
self.pos_idx = idx;
} else {
self.pos_idx = idx + self.gap_len;
}
}
fn expand_gap(self: *GapBuffer) std.mem.Allocator.Error!void {
std.debug.assert(self.gap_len == 0);
const text_size = self.buf.len - self.gap_len;
const new_gap_len = (self.gap_len + 1) * 2;
var new_buf = try self.alloc.alloc(u8, text_size + new_gap_len);
@memcpy(new_buf[0..self.pos_idx], self.buf[0..self.pos_idx]);
@memcpy(new_buf[self.pos_idx + new_gap_len ..], self.buf[self.pos_idx..]);
self.alloc.free(self.buf);
self.gap_begin_idx = self.pos_idx;
self.gap_len = new_gap_len;
self.buf = new_buf;
}
fn move_gap_to_cursor(self: *GapBuffer) void {
if (self.gap_begin_idx == self.pos_idx) {
return;
}
if (self.pos_idx > self.gap_begin_idx) {
const gap_end_idx = self.gap_begin_idx + self.gap_len;
const diff = self.pos_idx - gap_end_idx;
std.mem.copyForwards(u8, self.buf[self.gap_begin_idx .. self.gap_begin_idx + diff], self.buf[gap_end_idx .. gap_end_idx + diff]);
self.gap_begin_idx = self.gap_begin_idx + diff;
self.pos_idx = self.gap_begin_idx;
} else {
const diff = self.gap_begin_idx - self.pos_idx;
const save_pos = self.pos_idx + self.gap_len;
std.mem.copyBackwards(u8, self.buf[save_pos .. save_pos + diff], self.buf[self.pos_idx .. self.pos_idx + diff]);
self.gap_begin_idx = self.pos_idx;
}
}
};
The functions where I need help are handle_up and handle_down in editor.zig. The implementation in handle_up doesn't work i.e. the cursor seems to jump to wrong x-positions and sometimes skip empty lines.
Any help on the implementation would be appreciated.
I tried using lldb to find out what was wrong, but I kept introducing new bugs and edge-cases.