~jamii/focus

e4936e82a2a3ecbddce718c631e940448c765b7d — Jamie Brandon 11 months ago 3254b30
Switch from bytes to tree
M lib/focus.zig => lib/focus.zig +1 -0
@@ 1,6 1,7 @@
pub const common = @import("./focus/common.zig");
pub const meta = @import("./focus/meta.zig");
pub const Atlas = @import("./focus/atlas.zig").Atlas;
pub const Tree = @import("./focus/tree.zig").Tree;
pub const Buffer = @import("./focus/buffer.zig").Buffer;
pub const LineWrappedBuffer = @import("./focus/line_wrapped_buffer.zig").LineWrappedBuffer;
pub const Editor = @import("./focus/editor.zig").Editor;

M lib/focus/buffer.zig => lib/focus/buffer.zig +128 -123
@@ 3,6 3,7 @@ usingnamespace focus.common;
const App = focus.App;
const Editor = focus.Editor;
const meta = focus.meta;
const Tree = focus.Tree;
const LineWrappedBuffer = focus.LineWrappedBuffer;

pub const BufferSource = union(enum) {


@@ 59,7 60,7 @@ const Edit = union(enum) {
pub const Buffer = struct {
    app: *App,
    source: BufferSource,
    bytes: ArrayList(u8),
    tree: Tree,
    undos: ArrayList([]Edit),
    doing: ArrayList(Edit),
    redos: ArrayList([]Edit),


@@ 75,7 76,7 @@ pub const Buffer = struct {
        self.* = Buffer{
            .app = app,
            .source = .None,
            .bytes = ArrayList(u8).init(app.allocator),
            .tree = Tree.init(app.allocator),
            .undos = ArrayList([]Edit).init(app.allocator),
            .doing = ArrayList(Edit).init(app.allocator),
            .redos = ArrayList([]Edit).init(app.allocator),


@@ 130,7 131,7 @@ pub const Buffer = struct {
        }
        self.redos.deinit();

        self.bytes.deinit();
        self.tree.deinit();

        self.source.deinit(self.app);



@@ 163,6 164,7 @@ pub const Buffer = struct {
    fn load(self: *Buffer, kind: enum { Init, Refresh }) void {
        if (self.tryLoad()) |result| {
            switch (kind) {
                // TODO load directly into a tree?
                .Init => self.rawReplace(result.bytes),
                .Refresh => self.replace(result.bytes),
            }


@@ 228,7 230,7 @@ pub const Buffer = struct {
                };
                defer file.close();

                file.writeAll(self.bytes.items) catch |err| panic("{} while saving {s}", .{ err, file_source.absolute_filename });
                self.tree.writeInto(file.writer(), 0, self.tree.getTotalBytes()) catch |err| panic("{} while saving {s}", .{ err, file_source.absolute_filename });
                const stat = file.stat() catch |err| panic("{} while saving {s}", .{ err, file_source.absolute_filename });
                file_source.mtime = stat.mtime;
                self.modified_since_last_save = false;


@@ 237,7 239,7 @@ pub const Buffer = struct {
    }

    pub fn getBufferEnd(self: *Buffer) usize {
        return self.bytes.items.len;
        return self.tree.getTotalBytes();
    }

    pub fn getPosForLine(self: *Buffer, line: usize) usize {


@@ 265,8 267,7 @@ pub const Buffer = struct {
    }

    pub fn searchForwards(self: *Buffer, pos: usize, needle: []const u8) ?usize {
        const bytes = self.bytes.items[pos..];
        return if (std.mem.indexOf(u8, bytes, needle)) |result_pos| result_pos + pos else null;
        return self.tree.searchForwards(pos, needle);
    }

    pub fn isCloseParen(char: u8) bool {


@@ 278,39 279,44 @@ pub const Buffer = struct {
    }

    fn matchParenBackwards(self: *Buffer, pos: usize) ?usize {
        const bytes = self.bytes.items;
        var point = self.tree.getPointForPos(pos).?;
        var num_closing: usize = 0;
        var search_pos = pos;
        while (search_pos > 0) : (search_pos -= 1) {
            const char = bytes[search_pos];
        while (true) {
            if (point.seekPrevByte() == .NotFound) break;
            const char = point.getNextByte();
            if (isCloseParen(char)) num_closing += 1;
            if (isOpenParen(char)) num_closing -= 1;
            if (num_closing == 0) return search_pos;
            if (num_closing == 0) return point.pos;
        }
        return null;
    }

    fn matchParenForwards(self: *Buffer, pos: usize) ?usize {
        const bytes = self.bytes.items;
        const len = bytes.len;
        var point = self.tree.getPointForPos(pos).?;
        var num_opening: usize = 0;
        var search_pos = pos;
        while (search_pos < len) : (search_pos += 1) {
            const char = bytes[search_pos];
        while (true) {
            const char = point.getNextByte();
            if (isCloseParen(char)) num_opening -= 1;
            if (isOpenParen(char)) num_opening += 1;
            if (num_opening == 0) return search_pos;
            if (num_opening == 0) return point.pos;
            if (point.seekNextByte() == .NotFound) break;
        }
        return null;
    }

    pub fn matchParen(self: *Buffer, pos: usize) ?[2]usize {
        if (pos < self.bytes.items.len and isOpenParen(self.bytes.items[pos]))
            if (self.matchParenForwards(pos)) |matching_pos|
                return [2]usize{ pos, matching_pos };
        if (pos > 0 and isCloseParen(self.bytes.items[pos - 1]))
            if (self.matchParenBackwards(pos - 1)) |matching_pos|
                return [2]usize{ pos - 1, matching_pos };
        var point = self.tree.getPointForPos(pos).?;
        if (pos < self.getBufferEnd()) {
            if (isOpenParen(point.getNextByte()))
                if (self.matchParenForwards(pos)) |matching_pos|
                    return [2]usize{ pos, matching_pos };
        }
        if (pos > 0) {
            _ = point.seekPrevByte();
            if (isCloseParen(point.getNextByte()))
                if (self.matchParenBackwards(pos)) |matching_pos|
                    return [2]usize{ pos - 1, matching_pos };
        }
        return null;
    }



@@ 324,11 330,8 @@ pub const Buffer = struct {
        return self.line_ranges.items[line][1];
    }

    // TODO pass outStream instead of Allocator for easy concat/sentinel? but costs more allocations?
    pub fn dupe(self: *Buffer, allocator: *Allocator, start: usize, end: usize) []const u8 {
        assert(start <= end);
        assert(end <= self.bytes.items.len);
        return std.mem.dupe(allocator, u8, self.bytes.items[start..end]) catch oom();
    pub fn copy(self: *Buffer, allocator: *Allocator, start: usize, end: usize) []const u8 {
        return self.tree.copy(allocator, start, end);
    }

    fn rawInsert(self: *Buffer, pos: usize, bytes: []const u8) void {


@@ 336,9 339,7 @@ pub const Buffer = struct {
        const line_end = self.getLineEnd(pos);
        self.removeRangeFromCompletions(line_start, line_end);

        self.bytes.resize(self.bytes.items.len + bytes.len) catch oom();
        std.mem.copyBackwards(u8, self.bytes.items[pos + bytes.len ..], self.bytes.items[pos .. self.bytes.items.len - bytes.len]);
        std.mem.copy(u8, self.bytes.items[pos..], bytes);
        self.tree.insert(pos, bytes);

        self.addRangeToCompletions(line_start, line_end + bytes.len);



@@ 351,14 352,13 @@ pub const Buffer = struct {

    fn rawDelete(self: *Buffer, start: usize, end: usize) void {
        assert(start <= end);
        assert(end <= self.bytes.items.len);
        assert(end <= self.getBufferEnd());

        const line_start = self.getLineStart(start);
        const line_end = self.getLineEnd(end);
        self.removeRangeFromCompletions(line_start, line_end);

        std.mem.copy(u8, self.bytes.items[start..], self.bytes.items[end..]);
        self.bytes.shrink(self.bytes.items.len - (end - start));
        self.tree.delete(start, end);

        self.addRangeToCompletions(line_start, line_end - (end - start));



@@ 376,8 376,9 @@ pub const Buffer = struct {
        }
        std.mem.reverse([][2]usize, line_colss.items);

        self.bytes.resize(0) catch oom();
        self.bytes.appendSlice(new_bytes) catch oom();
        self.tree.deinit();
        self.tree = Tree.init(self.app.allocator);
        self.tree.insert(0, new_bytes);

        self.updateLineRanges();
        self.modified_since_last_save = true;


@@ 408,7 409,7 @@ pub const Buffer = struct {
            .Delete = .{
                .start = start,
                .end = end,
                .old_bytes = std.mem.dupe(self.app.allocator, u8, self.bytes.items[start..end]) catch oom(),
                .old_bytes = self.tree.copy(self.app.allocator, start, end),
            },
        }) catch oom();
        for (self.redos.items) |edits| {


@@ 420,22 421,20 @@ pub const Buffer = struct {
    }

    pub fn replace(self: *Buffer, new_bytes: []const u8) void {
        if (!std.mem.eql(u8, self.bytes.items, new_bytes)) {
            self.newUndoGroup();
            self.doing.append(.{
                .Replace = .{
                    .old_bytes = std.mem.dupe(self.app.allocator, u8, self.bytes.items) catch oom(),
                    .new_bytes = std.mem.dupe(self.app.allocator, u8, new_bytes) catch oom(),
                },
            }) catch oom();
            for (self.redos.items) |edits| {
                for (edits) |edit| edit.deinit(self.app.allocator);
                self.app.allocator.free(edits);
            }
            self.redos.shrink(0);
            self.rawReplace(new_bytes);
            self.newUndoGroup();
        self.newUndoGroup();
        self.doing.append(.{
            .Replace = .{
                .old_bytes = self.tree.copy(self.app.allocator, 0, self.tree.getTotalBytes()),
                .new_bytes = std.mem.dupe(self.app.allocator, u8, new_bytes) catch oom(),
            },
        }) catch oom();
        for (self.redos.items) |edits| {
            for (edits) |edit| edit.deinit(self.app.allocator);
            self.app.allocator.free(edits);
        }
        self.redos.shrink(0);
        self.rawReplace(new_bytes);
        self.newUndoGroup();
    }

    pub fn newUndoGroup(self: *Buffer) void {


@@ 508,46 507,38 @@ pub const Buffer = struct {
        };
    }

    pub fn getChar(self: *Buffer, pos: usize) u8 {
        return self.bytes.items[pos];
    }

    fn isLikeIdent(byte: u8) bool {
        return (byte >= 'a' and byte <= 'z') or (byte >= 'A' and byte <= 'Z') or (byte >= '0' and byte <= '9') or (byte == '_');
    }

    fn updateLineRanges(self: *Buffer) void {
        var line_ranges = &self.line_ranges;
        const bytes = self.bytes.items;
        const len = bytes.len;

        self.line_ranges.resize(0) catch oom();

        var start: usize = 0;
        while (start <= len) {
            var end = start;
            while (end < len and bytes[end] != '\n') : (end += 1) {}
            line_ranges.append(.{ start, end }) catch oom();
            start = end + 1;
        const line_ranges = &self.line_ranges;
        line_ranges.resize(0) catch oom();

        var point = self.tree.getPointForPos(0).?;
        while (true) {
            const start = point.pos;
            while (!point.isAtEnd() and point.getNextByte() != '\n') : (_ = point.seekNextByte()) {}
            line_ranges.append(.{ start, point.pos }) catch oom();
            if (point.isAtEnd()) break;
            _ = point.seekNextByte();
        }
    }

    fn updateCompletions(self: *Buffer) void {
        if (self.role == .Preview) return;

        for (self.completions.items) |completion| self.app.allocator.free(completion);
        self.completions.resize(0) catch oom();

        const bytes = self.bytes.items;
        const len = bytes.len;
        const completions = &self.completions;
        var start: usize = 0;
        while (start < len) {
            var end = start;
            while (end < len and isLikeIdent(bytes[end])) : (end += 1) {}
            if (end > start) completions.append(self.app.dupe(bytes[start..end])) catch oom();
            start = end + 1;
            while (start < len and !isLikeIdent(bytes[start])) : (start += 1) {}
        for (completions.items) |completion| self.app.allocator.free(completion);
        completions.resize(0) catch oom();

        var point = self.tree.getPointForPos(0).?;
        while (!point.isAtEnd()) {
            const start = point.pos;
            while (!point.isAtEnd() and isLikeIdent(point.getNextByte())) : (_ = point.seekNextByte()) {}
            if (point.pos > start)
                completions.append(self.tree.copy(self.app.allocator, start, point.pos)) catch oom();
            while (!point.isAtEnd() and !isLikeIdent(point.getNextByte())) : (_ = point.seekNextByte()) {}
        }

        std.sort.sort([]const u8, completions.items, {}, struct {


@@ 558,15 549,14 @@ pub const Buffer = struct {
    }

    fn removeRangeFromCompletions(self: *Buffer, range_start: usize, range_end: usize) void {
        const bytes = self.bytes.items;
        const completions = &self.completions;
        var start = range_start;
        while (start < range_end) {
            var end = start;
            while (end < range_end and isLikeIdent(bytes[end])) : (end += 1) {}
            if (end > start) {
        var point = self.tree.getPointForPos(range_start).?;
        while (point.pos < range_end) {
            const start = point.pos;
            while (point.pos < range_end and isLikeIdent(point.getNextByte())) : (_ = point.seekNextByte()) {}
            if (point.pos > start) {
                const completions_items = completions.items;
                const completion = bytes[start..end];
                const completion = self.tree.copy(self.app.frame_allocator, start, point.pos);
                var left: usize = 0;
                var right: usize = completions_items.len;



@@ 580,27 570,25 @@ pub const Buffer = struct {
                        }
                    }
                    // completion should definitely exist in the list
                    @panic("how");
                    @panic("Tried to remove non-existent completion");
                };

                const removed = completions.orderedRemove(pos);
                self.app.allocator.free(removed);
            }
            start = end + 1;
            while (start < range_end and !isLikeIdent(bytes[start])) : (start += 1) {}
            while (point.pos < range_end and !isLikeIdent(point.getNextByte())) : (_ = point.seekNextByte()) {}
        }
    }

    fn addRangeToCompletions(self: *Buffer, range_start: usize, range_end: usize) void {
        const bytes = self.bytes.items;
        const completions = &self.completions;
        var start = range_start;
        while (start < range_end) {
            var end = start;
            while (end < range_end and isLikeIdent(bytes[end])) : (end += 1) {}
            if (end > start) {
        var point = self.tree.getPointForPos(range_start).?;
        while (point.pos < range_end) {
            const start = point.pos;
            while (point.pos < range_end and isLikeIdent(point.getNextByte())) : (_ = point.seekNextByte()) {}
            if (point.pos > start) {
                const completions_items = completions.items;
                const completion = bytes[start..end];
                const completion = self.tree.copy(self.app.allocator, start, point.pos);
                var left: usize = 0;
                var right: usize = completions_items.len;



@@ 613,19 601,17 @@ pub const Buffer = struct {
                            .lt => right = mid,
                        }
                    }
                    // completion might not be in the list, but there is where it should be added
                    // completion might not be in the list, but this is where it should be added
                    break :pos left;
                };

                completions.insert(pos, self.app.dupe(completion)) catch oom();
                completions.insert(pos, completion) catch oom();
            }
            start = end + 1;
            while (start < range_end and !isLikeIdent(bytes[start])) : (start += 1) {}
            while (point.pos < range_end and !isLikeIdent(point.getNextByte())) : (_ = point.seekNextByte()) {}
        }
    }

    pub fn getCompletionsInto(self: *Buffer, prefix: []const u8, results: *ArrayList([]const u8)) void {
        const bytes = self.bytes.items;
        const completions = &self.completions;
        const completions_items = completions.items;
        var left: usize = 0;


@@ 654,36 640,55 @@ pub const Buffer = struct {
    }

    pub fn getCompletionsPrefix(self: *Buffer, pos: usize) []const u8 {
        const bytes = self.bytes.items;
        var start = pos;
        while (start > 0 and isLikeIdent(bytes[start - 1])) : (start -= 1) {}
        return bytes[start..pos];
        var point = self.tree.getPointForPos(pos).?;
        const start = start: {
            while (true) {
                if (point.seekPrevByte() == .NotFound)
                    break :start point.pos;
                if (!isLikeIdent(point.getNextByte()))
                    break :start point.pos + 1;
            }
        };
        return self.tree.copy(self.app.frame_allocator, start, pos);
    }

    pub fn getCompletionsToken(self: *Buffer, pos: usize) []const u8 {
        const bytes = self.bytes.items;
        const len = bytes.len;
        var start = pos;
        while (start > 0 and isLikeIdent(bytes[start - 1])) : (start -= 1) {}
        var end = pos;
        while (end < len and isLikeIdent(bytes[end])) : (end += 1) {}
        return bytes[start..end];
        var start_point = self.tree.getPointForPos(pos).?;
        var end_point = start_point;

        const start = start: {
            while (true) {
                if (start_point.seekPrevByte() == .NotFound)
                    break :start start_point.pos;
                if (!isLikeIdent(start_point.getNextByte()))
                    break :start start_point.pos + 1;
            }
        };

        while (!end_point.isAtEnd() and isLikeIdent(end_point.getNextByte())) : (_ = end_point.seekNextByte()) {}
        const end = end_point.pos;

        return self.tree.copy(self.app.frame_allocator, start, end);
    }

    pub fn insertCompletion(self: *Buffer, pos: usize, completion: []const u8) void {
        const bytes = self.bytes.items;
        const len = bytes.len;

        // get range of current token
        var start = pos;
        while (start > 0 and isLikeIdent(bytes[start - 1])) : (start -= 1) {}
        var end = pos;
        while (end < len and isLikeIdent(bytes[end])) : (end += 1) {}
        var start_point = self.tree.getPointForPos(pos).?;
        var end_point = start_point;
        const start = start: {
            while (true) {
                if (start_point.seekPrevByte() == .NotFound)
                    break :start start_point.pos;
                if (!isLikeIdent(start_point.getNextByte()))
                    break :start start_point.pos + 1;
            }
        };
        while (!end_point.isAtEnd() and isLikeIdent(end_point.getNextByte())) : (_ = end_point.seekNextByte()) {}
        const end = end_point.pos;

        // replace completion
        // (insert before delete so completion gets duped before self.completions updates)
        self.delete(start, end);
        self.insert(start, completion);
        self.delete(start + completion.len, end + completion.len);
    }

    pub fn registerEditor(self: *Buffer, editor: *Editor) void {

M lib/focus/buffer_searcher.zig => lib/focus/buffer_searcher.zig +1 -1
@@ 67,7 67,7 @@ pub const BufferSearcher = struct {
                while (self.preview_editor.buffer.searchForwards(pos, filter)) |found_pos| {
                    const start = self.preview_editor.buffer.getLineStart(found_pos);
                    const end = self.preview_editor.buffer.getLineEnd(found_pos + filter.len);
                    const selection = self.preview_editor.buffer.dupe(self.app.frame_allocator, start, end);
                    const selection = self.preview_editor.buffer.tree.copy(self.app.frame_allocator, start, end);
                    assert(selection[0] != '\n' and selection[selection.len - 1] != '\n');

                    var result = ArrayList(u8).init(self.app.frame_allocator);

M lib/focus/editor.zig => lib/focus/editor.zig +22 -17
@@ 377,7 377,7 @@ pub const Editor = struct {
        const max_line_ix = min(@intCast(usize, max(visible_end_line + 1, 0)), self.line_wrapped_buffer.wrapped_line_ranges.items.len);
        while (line_ix < max_line_ix) : (line_ix += 1) {
            const line_range = self.line_wrapped_buffer.wrapped_line_ranges.items[line_ix];
            const line = self.buffer.bytes.items[line_range[0]..line_range[1]];
            const line = self.buffer.copy(self.app.frame_allocator, line_range[0], line_range[1]);

            const line_top_pixel = @intCast(Coord, line_ix) * self.app.atlas.char_height;
            const y = text_rect.y + @intCast(Coord, line_top_pixel - self.top_pixel);


@@ 797,7 797,7 @@ pub const Editor = struct {

    pub fn dupeSelection(self: *Editor, allocator: *Allocator, cursor: *Cursor) []const u8 {
        const range = self.getSelectionRange(cursor);
        return self.buffer.dupe(allocator, range[0], range[1]);
        return self.buffer.tree.copy(allocator, range[0], range[1]);
    }

    // TODO clipboard stack on app


@@ 893,11 893,10 @@ pub const Editor = struct {
            // work out current indent
            self.goRealLineStart(edit_cursor);
            const this_line_start_pos = edit_cursor.head.pos;
            var this_indent: usize = 0;
            while (this_line_start_pos + this_indent < self.buffer.bytes.items.len and self.buffer.bytes.items[this_line_start_pos + this_indent] == ' ') {
                this_indent += 1;
            }
            const line_start_char = if (this_line_start_pos + this_indent < self.buffer.bytes.items.len) self.buffer.bytes.items[this_line_start_pos + this_indent] else 0;
            var this_line_start_point = self.buffer.tree.getPointForPos(this_line_start_pos).?;
            while (!this_line_start_point.isAtEnd() and this_line_start_point.getNextByte() == ' ') : (_ = this_line_start_point.seekNextByte()) {}
            const this_indent = this_line_start_point.pos - this_line_start_pos;
            const line_start_char = if (!this_line_start_point.isAtEnd()) this_line_start_point.getNextByte() else 0;

            // work out prev line indent
            var prev_indent: usize = 0;


@@ 909,20 908,24 @@ pub const Editor = struct {
                    var start = edit_cursor.head.pos;
                    const end = self.buffer.getLineEnd(edit_cursor.head.pos);
                    var is_blank = true;
                    while (start < end) : (start += 1) {
                        if (self.buffer.bytes.items[start] != ' ') is_blank = false;
                    var point = self.buffer.tree.getPointForPos(start).?;
                    while (point.pos < end) : (_ = point.seekNextByte()) {
                        if (point.getNextByte() != ' ') {
                            is_blank = false;
                            break;
                        }
                    }
                    if (!is_blank or start == 0) break;
                }

                const line_end_pos = self.buffer.getLineEnd(edit_cursor.head.pos);
                const line_end_char = if (line_end_pos > 0 and line_end_pos - 1 < self.buffer.bytes.items.len) self.buffer.bytes.items[line_end_pos - 1] else 0;
                const line_end_char = if (line_end_pos > 0 and line_end_pos - 1 < self.buffer.getBufferEnd()) self.buffer.tree.getPointForPos(line_end_pos - 1).?.getNextByte() else 0;

                self.goRealLineStart(edit_cursor);
                const prev_line_start_pos = edit_cursor.head.pos;
                while (prev_line_start_pos + prev_indent < self.buffer.bytes.items.len and self.buffer.bytes.items[prev_line_start_pos + prev_indent] == ' ') {
                    prev_indent += 1;
                }
                var prev_line_start_point = self.buffer.tree.getPointForPos(prev_line_start_pos).?;
                while (!prev_line_start_point.isAtEnd() and prev_line_start_point.getNextByte() == ' ') : (_ = prev_line_start_point.seekNextByte()) {}
                prev_indent = prev_line_start_point.pos - prev_line_start_pos;

                // add extra indent when opening a block
                // TODO this is kind of fragile


@@ 994,9 997,11 @@ pub const Editor = struct {
        while (num_lines > 0) : (num_lines -= 1) {
            // find first non-whitespace char
            self.goRealLineStart(edit_cursor);
            var start = edit_cursor.head.pos;
            var line_start = edit_cursor.head.pos;
            var point = self.buffer.tree.getPointForPos(line_start).?;
            while (!point.isAtEnd() and point.getNextByte() == ' ') : (_ = point.seekNextByte()) {}
            const start = point.pos;
            const end = self.buffer.getLineEnd(start);
            while (start < end and self.buffer.bytes.items[start] == ' ') : (start += 1) {}

            // insert or remove comment
            switch (action) {


@@ 1005,7 1010,7 @@ pub const Editor = struct {
                },
                .Remove => {
                    if (end - start >= comment_string.len and
                        meta.deepEqual(comment_string, self.buffer.bytes.items[start .. start + comment_string.len]))
                        meta.deepEqual(comment_string, self.buffer.tree.copy(self.app.frame_allocator, start, start + comment_string.len)))
                    {
                        self.buffer.delete(start, start + comment_string.len);
                    }


@@ 1035,7 1040,7 @@ pub const Editor = struct {
        process.stdout_behavior = .Pipe;
        process.stderr_behavior = .Pipe;
        process.spawn() catch |err| panic("Error spawning zig fmt: {}", .{err});
        process.stdin.?.outStream().writeAll(self.buffer.bytes.items) catch |err| panic("Error writing to zig fmt: {}", .{err});
        self.buffer.tree.writeInto(process.stdin.?.writer(), 0, self.buffer.getBufferEnd()) catch |err| panic("Error writing to zig fmt: {}", .{err});
        process.stdin.?.close();
        process.stdin = null;
        // NOTE this is fragile - currently zig fmt closes stdout before stderr so this works but reading the other way round will sometimes block

M lib/focus/line_wrapped_buffer.zig => lib/focus/line_wrapped_buffer.zig +12 -13
@@ 26,42 26,41 @@ pub const LineWrappedBuffer = struct {
    }

    pub fn update(self: *LineWrappedBuffer) void {
        const bytes = self.buffer.bytes.items;
        const wrapped_line_ranges = &self.wrapped_line_ranges;
        wrapped_line_ranges.resize(0) catch oom();
        for (self.buffer.line_ranges.items) |real_line_range, real_line| {
            const real_line_end = real_line_range[1];
            var line_start: usize = real_line_range[0];
            if (real_line_end - line_start <= self.max_chars_per_line) {
            if (real_line_range[1] - real_line_range[0] <= self.max_chars_per_line) {
                wrapped_line_ranges.append(real_line_range) catch oom();
                continue;
            }
            const real_line_end = real_line_range[1];
            var line_start = real_line_range[0];
            while (true) {
                var line_end = line_start;
                var maybe_line_end = line_end;
                var maybe_line_end = self.buffer.tree.getPointForPos(line_end).?;
                {
                    while (true) {
                        if (maybe_line_end >= real_line_end) {
                            line_end = maybe_line_end;
                        if (maybe_line_end.pos >= real_line_end) {
                            line_end = maybe_line_end.pos;
                            break;
                        }
                        const char = bytes[maybe_line_end];
                        if (maybe_line_end - line_start > self.max_chars_per_line) {
                        const char = maybe_line_end.getNextByte();
                        if (maybe_line_end.pos - line_start > self.max_chars_per_line) {
                            // if we haven't soft wrapped yet, hard wrap before this char, otherwise use soft wrap
                            if (line_end == line_start) {
                                line_end = maybe_line_end;
                                line_end = maybe_line_end.pos;
                            }
                            break;
                        }
                        if (char == '\n') {
                            // wrap here
                            line_end = maybe_line_end;
                            line_end = maybe_line_end.pos;
                            break;
                        }
                        maybe_line_end += 1;
                        _ = maybe_line_end.seekNextByte();
                        if (char == ' ') {
                            // commit to including this char
                            line_end = maybe_line_end;
                            line_end = maybe_line_end.pos;
                        }
                        // otherwise keep looking ahead
                    }

M lib/focus/single_line_editor.zig => lib/focus/single_line_editor.zig +1 -2
@@ 51,8 51,7 @@ pub const SingleLineEditor = struct {
    }

    pub fn getText(self: *SingleLineEditor) []const u8 {
        // TODO should this copy?
        return self.buffer.bytes.items;
        return self.buffer.tree.copy(self.app.frame_allocator, 0, self.buffer.getBufferEnd());
    }

    pub fn setText(self: *SingleLineEditor, text: []const u8) void {

M lib/focus/tree.zig => lib/focus/tree.zig +16 -12
@@ 274,18 274,18 @@ pub const Point = struct {
    num_leaf_bytes: Leaf.Offset,
    offset: Leaf.Offset,

    fn isAtEnd(self: Point) bool {
    pub fn isAtEnd(self: Point) bool {
        return self.offset == self.num_leaf_bytes;
    }

    fn getNextByte(self: *Point) u8 {
    pub fn getNextByte(self: *Point) u8 {
        assert(!self.isAtEnd());
        return self.leaf.bytes[self.offset];
    }

    const Seek = enum { Found, NotFound };

    fn seekNextLeaf(self: *Point) Seek {
    pub fn seekNextLeaf(self: *Point) Seek {
        var node = self.leaf.node;

        self.pos += self.num_leaf_bytes - self.offset;


@@ 312,13 312,14 @@ pub const Point = struct {
                    return .Found;
                }
            } else {
                self.offset = self.num_leaf_bytes;
                return .NotFound;
            }
        }
    }

    fn seekNextByte(self: *Point) Seek {
        if (self.offset >= self.num_leaf_bytes - 1) {
    pub fn seekNextByte(self: *Point) Seek {
        if (self.offset + 1 >= self.num_leaf_bytes) {
            if (self.seekNextLeaf() == .NotFound) return .NotFound;
        } else {
            self.pos += 1;


@@ 327,7 328,7 @@ pub const Point = struct {
        return .Found;
    }

    fn seekPrevLeaf(self: *Point) Seek {
    pub fn seekPrevLeaf(self: *Point) Seek {
        var node = self.leaf.node;

        self.pos -= self.offset;


@@ 354,12 355,13 @@ pub const Point = struct {
                    return .Found;
                }
            } else {
                self.offset = 0;
                return .NotFound;
            }
        }
    }

    fn seekPrevByte(self: *Point) Seek {
    pub fn seekPrevByte(self: *Point) Seek {
        if (self.offset == 0) {
            if (self.seekPrevLeaf() == .NotFound) return .NotFound;
        } else {


@@ 374,7 376,7 @@ pub const Tree = struct {
    allocator: *Allocator,
    root: Branch,

    fn init(allocator: *Allocator) Tree {
    pub fn init(allocator: *Allocator) Tree {
        var branch = Branch.init(allocator);
        var leaf = Leaf.init(allocator);
        branch.insertChild(0, leaf.node, 0);


@@ 384,7 386,7 @@ pub const Tree = struct {
        };
    }

    fn deinit(self: Tree) void {
    pub fn deinit(self: Tree) void {
        self.root.deinit(self.allocator);
    }



@@ 608,7 610,7 @@ pub const Tree = struct {
        }
    }

    fn getTotalBytes(self: Tree) usize {
    pub fn getTotalBytes(self: Tree) usize {
        return self.root.sumNumBytes();
    }



@@ 628,7 630,9 @@ pub const Tree = struct {
        return buffer;
    }

    pub fn copyInto(self: Tree, buffer: []u8, start: usize) void {
    pub fn copyInto(self: Tree, _buffer: []u8, start: usize) void {
        var buffer = _buffer;

        var point = self.getPointForPos(start).?;

        while (true) {


@@ 646,7 650,7 @@ pub const Tree = struct {
        }
    }

    fn writeInto(self: Tree, writer: anytype, start: usize, end: usize) !void {
    pub fn writeInto(self: Tree, writer: anytype, start: usize, end: usize) !void {
        var point = self.getPointForPos(start).?;

        var num_remaining_write_bytes = end - start;