summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--build.zig.zon4
-rw-r--r--src/Buffer.zig202
-rw-r--r--src/Selection.zig (renamed from src/selection.zig)225
-rw-r--r--src/Token.zig103
-rw-r--r--src/main.janet77
-rw-r--r--src/main.zig117
6 files changed, 429 insertions, 299 deletions
diff --git a/build.zig.zon b/build.zig.zon
index f62d31f..c52be37 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -17,8 +17,8 @@
             .hash = "vaxis-0.5.1-BWNV_CAVCQAmzxx2i2hBs5SvEupHwDbKHtE5VSOZwwtl",
         },
         .zsanett = .{
-            .url = "git+https://trong.loang.net/~cnx/zsanett#f5b2d6f3122b54fcdcb3bd6953420480bb9bc752",
-            .hash = "zsanett-0.0.0-1TMqaI7-AAD5R_t1Udvn92vPkwJnBQXnVzLgKMlJkOIB",
+            .url = "git+https://trong.loang.net/~cnx/zsanett#7d61d53d2e6c33b7bb847f608f43e2775fa3da9b",
+            .hash = "zsanett-0.0.0-1TMqaN_8AADxYf9AQY9cfZhx2uKrh8iEhn1TLwNKiN4d",
         },
     },
     .paths = .{
diff --git a/src/Buffer.zig b/src/Buffer.zig
new file mode 100644
index 0000000..faee5c4
--- /dev/null
+++ b/src/Buffer.zig
@@ -0,0 +1,202 @@
+// Gap buffer
+// SPDX-FileCopyrightText: 2025 Nguyễn Gia Phong
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+const assert = std.debug.assert;
+const eql = std.meta.eql;
+const std = @import("std");
+
+const Input = tree_sitter.Input;
+const Parser = tree_sitter.Parser;
+const Point = tree_sitter.Point;
+const Tree = tree_sitter.Tree;
+const tree_sitter = @import("tree-sitter");
+
+const Grapheme = Graphemes.Grapheme;
+const Graphemes = vaxis.Graphemes;
+const vaxis = @import("vaxis");
+
+const janet = zsanett.janet;
+const zsanett = @import("zsanett");
+
+const Selection = @import("Selection.zig");
+const languages = @import("languages");
+const graphemes = &@import("main.zig").graphemes;
+
+const initial_gap_size = std.atomic.cache_line;
+
+const Buffer = @This();
+/// File content with gap, owned by Janet runtime.
+data: []u8,
+gap_position: u32 = 0,
+gap_size: u32 = initial_gap_size,
+parser: *Parser,
+tree: *Tree, // TODO: polyglot
+scroll_row: u32 = 0,
+selection: Selection,
+paste_direction: enum { before, after } = .before,
+
+fn read(payload: ?*anyopaque, byte_index: u32, _: Point,
+        bytes_read: *u32) callconv(.C) [*c]const u8 {
+    const buffer: *Buffer = @alignCast(@ptrCast(payload.?));
+    if (byte_index < buffer.gap_position) {
+        bytes_read.* = @intCast(buffer.gap_position - byte_index);
+        return buffer.data[byte_index..buffer.gap_position].ptr;
+    }
+    const index_with_gap = byte_index + buffer.gap_size;
+    if (index_with_gap < buffer.data.len) {
+        bytes_read.* = @intCast(buffer.data.len - index_with_gap);
+        return buffer.data[index_with_gap..].ptr;
+    } else {
+        bytes_read.* = 0;
+        return "";
+    }
+}
+
+pub fn open(text: []u8) !Buffer {
+    const data = gap: {
+        const n: i32 = @intCast(text.len);
+        const buffer = janet.pointerBufferUnsafe(@ptrCast(text.ptr), n, n);
+        const capacity: usize = @intCast(n + initial_gap_size);
+        if (janet.realloc(buffer.*.data, capacity)) |data| {
+            janet.gcpressure(initial_gap_size);
+            buffer.*.data = @ptrCast(data);
+            buffer.*.capacity = @intCast(capacity);
+            buffer.*.count = buffer.*.capacity;
+            // TODO: use @memmove in Zig 0.15
+            var i = capacity;
+            while (i >= initial_gap_size) : (i -= 1)
+                buffer.*.data[i] = buffer.*.data[i - initial_gap_size];
+            break :gap buffer.*.data[0..capacity];
+        } else return error.OutOfMemory;
+    };
+
+    const parser = Parser.create();
+    errdefer parser.destroy();
+    const language = languages.c();
+    errdefer language.destroy();
+    try parser.setLanguage(language);
+    var result = Buffer{
+        .data = data,
+        .parser = parser,
+        .tree = undefined,
+        .selection = undefined,
+    };
+    result.tree = parser.parse(.{
+        .payload = &result,
+        .read = Buffer.read,
+    }, null).?;
+    result.selection = .{
+        .head = .{ .node = result.tree.rootNode() },
+        .tail = .{ .node = result.tree.rootNode() },
+    };
+    return result;
+}
+
+pub fn close(buffer: Buffer) void {
+    buffer.tree.getLanguage().destroy();
+    buffer.tree.destroy();
+    buffer.parser.destroy();
+}
+
+fn skipLines(text: []const u8, from: u32, n: u32) union(enum) {
+    done: u32,
+    left: u32,
+} {
+    assert(n > 0);
+    var lines: u32 = 0;
+    for (text[from..], 1..) |c, i|
+        if (c == '\n') {
+            lines += 1;
+            if (lines == n)
+                return .{ .done = @intCast(from + i) };
+        };
+    return .{ .left = n - lines };
+}
+
+pub fn iterate(buffer: Buffer) struct {
+    const Iterator = @This();
+    graphemes_iterator: Graphemes.Iterator,
+    data: []const u8,
+    offset: u32,
+    before_gap: bool,
+    gap_start: u32,
+    gap_end: u32,
+
+    pub fn next(iterator: *Iterator) ?Grapheme {
+        if (iterator.graphemes_iterator.next()) |grapheme| {
+            return .{
+                .offset = grapheme.offset + iterator.offset,
+                .len = grapheme.len,
+            };
+        } else if (iterator.before_gap) {
+            const buffer_after_gap = iterator.data[iterator.gap_end..];
+            iterator.graphemes_iterator = graphemes.iterator(buffer_after_gap);
+            iterator.offset = iterator.gap_start;
+            iterator.before_gap = false;
+            return iterator.next();
+        } else return null;
+    }
+} {
+    const root_node = buffer.tree.rootNode();
+    const point = Point{ .row = buffer.scroll_row, .column = 0 };
+    const node = root_node.descendantForPointRange(point, point).?;
+    const start_point = node.startPoint();
+    assert(start_point.row < point.row or eql(start_point, point));
+
+    const start_byte = node.startByte();
+    const start = if (eql(start_point, point))
+        start_byte
+    else if (start_byte >= buffer.gap_position)
+        skipLines(buffer.data[buffer.gap_size..], start_byte,
+                  point.row - start_point.row).done
+    else switch (skipLines(buffer.data[0..buffer.gap_position], start_byte,
+                           point.row - start_point.row)) {
+        .done => |offset| offset,
+        .left => |left| skipLines(buffer.data[buffer.gap_size..],
+                                  buffer.gap_position, left).done,
+    };
+    if (start < buffer.gap_position) {
+        const buffer_before_gap = buffer.data[start..buffer.gap_position];
+        return .{
+            .graphemes_iterator = graphemes.iterator(buffer_before_gap),
+            .data = buffer.data,
+            .offset = start,
+            .before_gap = true,
+            .gap_start = buffer.gap_position,
+            .gap_end = buffer.gap_position + buffer.gap_size,
+        };
+    }
+    const offset = start + buffer.gap_size;
+    return .{
+        .graphemes_iterator = graphemes.iterator(buffer.data[offset..]),
+        .data = undefined,
+        .offset = start,
+        .before_gap = true,
+        .gap_start = undefined,
+        .gap_end = undefined,
+    };
+}
+
+pub fn bytes(buffer: Buffer, grapheme: Grapheme) []const u8 {
+    if (grapheme.offset < buffer.gap_position)
+        return grapheme.bytes(buffer.data);
+    const start = grapheme.offset + buffer.gap_size;
+    const end = start + grapheme.len;
+    return buffer.data[start..end];
+}
+
+pub fn selectedBytes(buffer: Buffer) []const u8 {
+    const start = buffer.selection.startByte();
+    const end = buffer.selection.endByte();
+    if (end <= buffer.gap_position)
+        return buffer.data[start..end];
+    if (start >= buffer.gap_position)
+        return buffer.data[buffer.gap_size..][start..end];
+    unreachable;
+}
+
+pub fn paste(buffer: Buffer, data: []const u8) Buffer {
+    _ = data;
+    return buffer;
+}
diff --git a/src/selection.zig b/src/Selection.zig
index 44d537f..352af7a 100644
--- a/src/selection.zig
+++ b/src/Selection.zig
@@ -6,101 +6,124 @@ const eql = std.meta.eql;
 const order = std.math.order;
 const std = @import("std");
 
-const Grapheme = Graphemes.Grapheme;
-const Graphemes = vaxis.Graphemes;
 const Node = tree_sitter.Node;
 const Tree = tree_sitter.Tree;
 const tree_sitter = @import("tree-sitter");
+
+const Grapheme = vaxis.Graphemes.Grapheme;
 const vaxis = @import("vaxis");
 
-fn graphemeAt(graphemes: Graphemes, text: []const u8, index: u32) Grapheme {
+const graphemes = &@import("root").graphemes;
+
+const Selection = @This();
+head: Unit,
+tail: Unit,
+
+pub fn startByte(selection: Selection) u32 {
+    return @min(selection.head.startByte(), selection.tail.startByte());
+}
+
+pub fn endByte(selection: Selection) u32 {
+    return @max(selection.head.endByte(), selection.tail.endByte());
+}
+
+pub fn inHead(selection: Selection, grapheme: Grapheme) bool {
+    return selection.head.contains(grapheme);
+}
+
+pub fn inBody(selection: Selection, grapheme: Grapheme) bool {
+    return (!selection.inHead(grapheme)
+            and selection.startByte() <= grapheme.offset
+            and grapheme.offset + grapheme.len <= selection.endByte());
+}
+
+fn graphemeAt(text: []const u8, index: u32) Grapheme {
     var iterator = graphemes.iterator(text[index..]);
     return .{ .offset = index, .len = iterator.peek().?.len };
 }
 
-fn graphemeBefore(graphemes: Graphemes, text: []const u8, index: u32) Grapheme {
+fn graphemeBefore(text: []const u8, index: u32) Grapheme {
     var iterator = graphemes.reverseIterator(text[0..index]);
     return iterator.peek().?;
 }
 
-// TODO: undo
-pub const Unit = union(enum) {
-    node: Node,
-    span: Span,
-    grapheme: GraphemeUnit,
-
-    // TODO: nesting
-    const Span = struct {
-        start: u32,
-        end: u32,
-        tree: *const Tree,
+// TODO: nesting
+const Span = struct {
+    start: u32,
+    end: u32,
+    tree: *const Tree,
 
-        fn parent(unit: Span) Node {
-            const root_node = unit.tree.rootNode();
-            return root_node.descendantForByteRange(unit.start, unit.end).?;
-        }
-    };
+    fn parent(unit: Span) Node {
+        const root_node = unit.tree.rootNode();
+        return root_node.descendantForByteRange(unit.start, unit.end).?;
+    }
+};
 
-    const GraphemeUnit = struct {
-        slice: Grapheme,
-        tree: *const Tree,
+const GraphemeUnit = struct {
+    slice: Grapheme,
+    tree: *const Tree,
 
-        fn startByte(unit: GraphemeUnit) u32 {
-            return unit.slice.offset;
-        }
+    fn startByte(unit: GraphemeUnit) u32 {
+        return unit.slice.offset;
+    }
 
-        fn endByte(unit: GraphemeUnit) u32 {
-            return unit.slice.offset + unit.slice.len;
-        }
+    fn endByte(unit: GraphemeUnit) u32 {
+        return unit.slice.offset + unit.slice.len;
+    }
 
-        fn parent(unit: GraphemeUnit) Span {
-            const start = unit.startByte();
-            const end = unit.endByte();
-            const root_node = unit.tree.rootNode();
-            const parent_node = root_node.descendantForByteRange(start, end).?;
-            if (parent_node.firstChildForByte(end)) |next_node| {
-                return if (next_node.prevSibling()) |prev_node| .{
-                    .start = prev_node.endByte(),
-                    .end = next_node.startByte(),
-                    .tree = unit.tree,
-                } else .{
-                    .start = parent_node.startByte(),
-                    .end = next_node.startByte(),
-                    .tree = unit.tree,
-                };
-            } else {
-                const siblings = parent_node.childCount();
-                if (siblings == 0)
-                    return .{
-                        .start = parent_node.startByte(),
-                        .end = parent_node.endByte(),
-                        .tree = unit.tree,
-                    };
-                const prev_node = parent_node.child(siblings - 1).?;
+    fn parent(unit: GraphemeUnit) Span {
+        const start = unit.startByte();
+        const end = unit.endByte();
+        const root_node = unit.tree.rootNode();
+        const parent_node = root_node.descendantForByteRange(start, end).?;
+        if (parent_node.firstChildForByte(end)) |next_node| {
+            return if (next_node.prevSibling()) |prev_node| .{
+                .start = prev_node.endByte(),
+                .end = next_node.startByte(),
+                .tree = unit.tree,
+            } else .{
+                .start = parent_node.startByte(),
+                .end = next_node.startByte(),
+                .tree = unit.tree,
+            };
+        } else {
+            const siblings = parent_node.childCount();
+            if (siblings == 0)
                 return .{
-                    .start = prev_node.endByte(),
+                    .start = parent_node.startByte(),
                     .end = parent_node.endByte(),
                     .tree = unit.tree,
                 };
-            }
-        }
-
-        fn at(unit: GraphemeUnit, graphemes: Graphemes,
-              text: []const u8, index: u32) GraphemeUnit {
+            const prev_node = parent_node.child(siblings - 1).?;
             return .{
-                .slice = graphemeAt(graphemes, text, index),
+                .start = prev_node.endByte(),
+                .end = parent_node.endByte(),
                 .tree = unit.tree,
             };
         }
+    }
 
-        fn before(unit: GraphemeUnit, graphemes: Graphemes,
-                  text: []const u8, index: u32) GraphemeUnit {
-            return .{
-                .slice = graphemeBefore(graphemes, text, index),
-                .tree = unit.tree,
-            };
-        }
-    };
+    fn at(unit: GraphemeUnit, text: []const u8, index: u32) GraphemeUnit {
+        return .{
+            .slice = graphemeAt(text, index),
+            .tree = unit.tree,
+        };
+    }
+
+    fn before(unit: GraphemeUnit,
+              text: []const u8, index: u32) GraphemeUnit {
+        return .{
+            .slice = graphemeBefore(text, index),
+            .tree = unit.tree,
+        };
+    }
+};
+
+// TODO: undo
+pub const Unit = union(enum) {
+    node: Node,
+    span: Span,
+    grapheme: GraphemeUnit,
 
     fn startByte(unit: Unit) u32 {
         return switch (unit) {
@@ -118,11 +141,11 @@ pub const Unit = union(enum) {
         };
     }
 
-    pub fn up(unit: Unit) Unit {
+    pub fn up(unit: Unit, text: []const u8) Unit {
         switch (unit) {
             .node => |node| return if (node.parent()) |parent_node|
                 if (parent_node.eql(node))
-                    .up(.{ .node = parent_node })
+                    .up(.{ .node = parent_node }, text)
                 else
                     .{ .node = parent_node }
             else unit,
@@ -130,7 +153,7 @@ pub const Unit = union(enum) {
                 const parent_node = span.parent();
                 return if (parent_node.startByte() == span.start
                                and parent_node.endByte() == span.end)
-                    .up(.{ .node = parent_node })
+                    .up(.{ .node = parent_node }, text)
                 else
                     .{ .node = parent_node };
             },
@@ -138,14 +161,14 @@ pub const Unit = union(enum) {
                 const parent_span = grapheme.parent();
                 return if (parent_span.start == grapheme.startByte()
                                and parent_span.end == grapheme.endByte())
-                    .up(.{ .span = parent_span })
+                    .up(.{ .span = parent_span }, text)
                 else
                     .{ .span = parent_span };
             },
         }
     }
 
-    pub fn right(unit: Unit, graphemes: Graphemes, text: []const u8) Unit {
+    pub fn right(unit: Unit, text: []const u8) Unit {
         switch (unit) {
             .node => |node| if (node.nextSibling()) |sibling| {
                 const prev = node.endByte();
@@ -163,21 +186,21 @@ pub const Unit = union(enum) {
                 };
             },
             .span => |span| {
-                const parent_node = unit.up().node;
+                const parent_node = unit.up(text).node;
                 if (parent_node.firstChildForByte(span.end)) |next_node|
                     return .{ .node = next_node };
             },
             .grapheme => |grapheme| {
                 const start = grapheme.endByte();
-                const next_grapheme = grapheme.at(graphemes, text, start);
+                const next_grapheme = grapheme.at(text, start);
                 if (eql(next_grapheme.parent(), grapheme.parent()))
                     return .{ .grapheme = next_grapheme };
             },
         }
-        return .up(unit);
+        return .up(unit, text);
     }
 
-    pub fn left(unit: Unit, graphemes: Graphemes, text: []const u8) Unit {
+    pub fn left(unit: Unit, text: []const u8) Unit {
         switch (unit) {
             .node => |node| if (node.prevSibling()) |sibling| {
                 const prev = sibling.endByte();
@@ -195,7 +218,7 @@ pub const Unit = union(enum) {
                 };
             },
             .span => |span| {
-                const parent_node = span.parent();
+                const parent_node = unit.up(text).node;
                 if (parent_node.firstChildForByte(span.end)) |next_node| {
                     if (next_node.prevSibling()) |prev_node|
                         return .{ .node = prev_node };
@@ -206,15 +229,15 @@ pub const Unit = union(enum) {
             },
             .grapheme => |grapheme| {
                 const end = grapheme.slice.offset;
-                const prev_grapheme = grapheme.before(graphemes, text, end);
+                const prev_grapheme = grapheme.before(text, end);
                 if (eql(prev_grapheme.parent(), grapheme.parent()))
                     return .{ .grapheme = prev_grapheme };
             },
         }
-        return .up(unit);
+        return .up(unit, text);
     }
 
-    pub fn down(unit: Unit, graphemes: Graphemes, text: []const u8) Unit {
+    pub fn down(unit: Unit, text: []const u8) Unit {
         switch (unit) {
             .node => |node| {
                 const start = node.startByte();
@@ -222,17 +245,17 @@ pub const Unit = union(enum) {
                 // TODO: history
                 return if (node.child(0)) |child|
                     if (child.startByte() == start and child.endByte() == end)
-                        .down(.{ .node = child }, graphemes, text)
+                        .down(.{ .node = child }, text)
                     else
                         .{ .node = child }
                 else .down(.{
                     .span = .{ .start = start, .end = end, .tree = node.tree }
-                }, graphemes, text);
+                }, text);
             },
-            // TODO: history
             .span => |span| return .{
                 .grapheme = .{
-                    .slice = graphemeAt(graphemes, text, span.start),
+                    // TODO: history
+                    .slice = graphemeAt(text, span.start),
                     .tree = span.tree,
                 }
             },
@@ -250,35 +273,3 @@ pub const Unit = union(enum) {
         };
     }
 };
-
-pub const Segment = struct {
-    head: Unit,
-    tail: Unit,
-
-    pub fn contains(segment: Segment, grapheme: Grapheme) bool {
-        const start = @min(segment.head.startByte(), segment.tail.startByte());
-        const end = @min(segment.head.endByte(), segment.tail.endByte());
-        return (start <= grapheme.offset
-                and grapheme.offset + grapheme.len <= end);
-    }
-
-    pub fn goUp(segment: *Segment) void {
-        segment.head = segment.head.up();
-        segment.tail = segment.tail.up();
-    }
-
-    pub fn goRight(segment: *Segment, graphemes: Graphemes, text: []const u8) void {
-        segment.head = segment.head.right(graphemes, text);
-        segment.tail = segment.tail.right(graphemes, text);
-    }
-
-    pub fn goLeft(segment: *Segment, graphemes: Graphemes, text: []const u8) void {
-        segment.head = segment.head.left(graphemes, text);
-        segment.tail = segment.tail.left(graphemes, text);
-    }
-
-    pub fn goDown(segment: *Segment, graphemes: Graphemes, text: []const u8) void {
-        segment.head = segment.head.down(graphemes, text);
-        segment.tail = segment.tail.down(graphemes, text);
-    }
-};
diff --git a/src/Token.zig b/src/Token.zig
deleted file mode 100644
index 49b15bb..0000000
--- a/src/Token.zig
+++ /dev/null
@@ -1,103 +0,0 @@
-// Tokenizer
-// SPDX-FileCopyrightText: 2025 Nguyễn Gia Phong
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-const order = std.math.order;
-const std = @import("std");
-
-const Node = tree_sitter.Node;
-const Parser = tree_sitter.Parser;
-const Tree = tree_sitter.Tree;
-const TreeCursor = tree_sitter.TreeCursor;
-const tree_sitter = @import("tree-sitter");
-
-const CreateLanguage = @import("languages").Create;
-
-const Token = @This();
-text: []const u8,
-node: ?Node = null,
-
-const Iterator = struct {
-    text: []const u8,
-    parser: *Parser,
-    tree: *Tree,
-    cursor: TreeCursor,
-    next_node: ?Node = null,
-    pos: u32 = 0,
-
-    /// Returns the next tree-sitter node.
-    fn nextNode(self: *Iterator) ?Node {
-        if (self.next_node) |node| {
-            self.next_node = null;
-            return node;
-        }
-        const node = self.cursor.node();
-        return if (self.cursor.gotoFirstChild())
-            node
-        else if (self.cursor.gotoNextSibling())
-            node
-        else while (self.cursor.gotoParent()) {
-            if (self.cursor.gotoNextSibling())
-                break node;
-        } else null;
-    }
-
-    pub fn next(self: *Iterator) ?Token {
-        if (self.pos == self.text.len)
-            return null;
-        while (self.nextNode()) |node|
-            if (node.childCount() > 0) {
-                const start = node.startByte();
-                const end = node.endByte();
-                switch (order(self.pos, start)) {
-                    .lt => {
-                        defer self.pos = start;
-                        self.next_node = node;
-                        return .{ .text = self.text[self.pos..start] };
-                    },
-                    .eq => {
-                        defer self.pos = end;
-                        return .{ .text = self.text[start..end], .node = node };
-                    },
-                    .gt => unreachable,
-                }
-            };
-        switch (order(self.pos, self.text.len)) {
-            .lt => {
-                defer self.pos = @intCast(self.text.len);
-                return .{ .text = self.text[self.pos..] };
-            },
-            .eq => return null,
-            .gt => unreachable,
-        }
-    }
-
-    pub fn reset(self: *Iterator) void {
-        self.cursor.reset(self.tree.rootNode());
-        self.next_node = null;
-        self.pos = 0;
-    }
-
-    pub fn deinit(self: *Iterator) void {
-        self.cursor.destroy();
-        self.tree.destroy();
-        self.parser.getLanguage().?.destroy();
-        self.parser.destroy();
-        self.* = undefined;
-    }
-};
-
-/// Parse text in given language and return an iterator of tokens.
-pub fn ize(text: []const u8, createLanguage: CreateLanguage) error {
-    IncompatibleVersion,
-}!Iterator {
-    const parser = Parser.create();
-    try parser.setLanguage(createLanguage());
-    const tree = parser.parseString(text, null).?;
-    return .{
-        .text = text,
-        .parser = parser,
-        .tree = tree,
-        .cursor = tree.walk(),
-    };
-}
diff --git a/src/main.janet b/src/main.janet
index 8ca1215..4034dc9 100644
--- a/src/main.janet
+++ b/src/main.janet
@@ -2,24 +2,73 @@
 # SPDX-FileCopyrightText: 2025 Nguyễn Gia Phong
 # SPDX-License-Identifier: GPL-3.0-or-later
 
+(defn kay/select/move
+  [buf direction]
+  (let [data (buf :data)
+        selection (buf :selection)]
+    (struct ;(kvs buf)
+            :selection (match selection
+                         {:head head :tail tail}
+                         (struct ;(kvs selection)
+                                 :head (direction head data)
+                                 :tail (direction tail data))))))
+
+(defn kay/select/head
+  [buf direction]
+  (let [data (buf :data)
+        selection (buf :selection)]
+    (struct ;(kvs buf)
+            :selection (match selection
+                         {:head head :tail tail}
+                         (struct ;(kvs selection)
+                                 :head (direction head data)
+                                 :tail tail)))))
+
+(defn kay/select/flip
+  [buf]
+  (struct ;(kvs buf)
+          :selection (match (buf :selection)
+                       {:head head :tail tail} {:head tail :tail head})))
+
 (defn handle
-  [event]
+  [event buf]
   (match event
-    [:key-press key] (cond
-                       (:matches key (chr "c") {:ctrl true}) [:quit]
-                       (:matches key (chr "h") {}) (do (:go-up env) [])
-                       (:matches key (chr "j") {}) (do (:go-right env) [])
-                       (:matches key (chr "k") {}) (do (:go-left env) [])
-                       (:matches key (chr "l") {}) (do (:go-down env) [])
-                       [])))
+    {:winsize ws} (do (:resize kay/env ws)
+                      [[] buf])
+    {:key-press key}
+    (cond
+      (:matches key (chr "c") {:ctrl true}) [[:quit] buf]
+      (:matches key (chr "y") {}) (do (:yank kay/env buf)
+                                      [[] buf])
+      (:matches key (chr "i") {}) (do (:paste kay/env)
+                                      [[] buf])
+      (:matches key (chr "p") {}) [[] (struct ;(kvs buf)
+                                              :scroll-row (max 0 (dec (buf :scroll-row))))]
+      (:matches key (chr "n") {}) [[] (struct ;(kvs buf)
+                                              :scroll-row (inc (buf :scroll-row)))]
+      (:matches key (chr "h") {}) [[] (kay/select/move buf :up)]
+      (:matches key (chr "j") {}) [[] (kay/select/move buf :right)]
+      (:matches key (chr "k") {}) [[] (kay/select/move buf :left)]
+      (:matches key (chr "l") {}) [[] (kay/select/move buf :down)]
+      (:matches key (chr "h") {:shift true}) [[] (kay/select/head buf :up)]
+      (:matches key (chr "j") {:shift true}) [[] (kay/select/head buf :right)]
+      (:matches key (chr "k") {:shift true}) [[] (kay/select/head buf :left)]
+      (:matches key (chr "l") {:shift true}) [[] (kay/select/head buf :down)]
+      (:matches key (chr ";") {}) [[] (kay/select/flip buf)]
+      [[] buf])
+    {:paste paste} [[] (:paste kay/env buf paste)]))
 
 (defn run
-  [events]
+  [events buf]
   (if-let [event (array/pop events)]
     (unless (= :quit event)
-      (let [next-events (handle event)]
-        (:render env)
-        (run (array/join events next-events))))
-    (run @[(:next-event env)])))
+      (let [[next-events next-buf] (handle event buf)]
+        (:render kay/env next-buf)
+        (run (array/join events next-events)
+             next-buf)))
+    (run @[(:next-event kay/env)]
+         buf)))
 
-(run @[])
+(with [buf (kay/open (with [f (file/open (string kay/path))]
+                       (buffer (:read f :all))))]
+  (run @[] buf))
diff --git a/src/main.zig b/src/main.zig
index f34d878..d26cefa 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -14,6 +14,7 @@ const ns_per_s = std.time.ns_per_s;
 const smp_allocator = std.heap.smp_allocator;
 const std = @import("std");
 
+const Graphemes = vaxis.Graphemes;
 const Key = vaxis.Key;
 const Loop = vaxis.Loop;
 const Style = vaxis.Cell.Style;
@@ -24,71 +25,65 @@ const Winsize = vaxis.Winsize;
 const gwidth = vaxis.gwidth.gwidth;
 const Vaxis = vaxis.Vaxis;
 const vaxis = @import("vaxis");
-
-const Parser = tree_sitter.Parser;
-const tree_sitter = @import("tree-sitter");
+pub const panic = vaxis.panic_handler;
 
 const zsanett = @import("zsanett");
 pub fn zsanettIntern(T: type) bool {
     return switch (T) {
-        Environment, Key, Loop(Event) => true,
+        Buffer, Environment, Key, Loop(Event),
+        Selection, Selection.Unit => true,
         else => false,
     };
 }
 
+const Buffer = @import("Buffer.zig");
 const Config = @import("Config.zig");
-const selection = @import("selection.zig");
-const languages = @import("languages");
+const Selection = @import("Selection.zig");
+
+pub var graphemes: Graphemes = undefined;
 
 const Event = union(enum) {
     key_press: Key,
     winsize: Winsize,
+    paste: []const u8,
 };
 
 const Environment = struct {
     allocator: Allocator,
     loop: *Loop(Event),
-    parser: *Parser,
-    segment: *selection.Segment,
-    text: []const u8,
     tty: *Tty,
     vaxis: *Vaxis,
 
-    pub fn nextEvent(self: Environment) !Event {
-        while (true) {
-            const event = self.loop.nextEvent();
-            switch (event) {
-                .key_press => return event,
-                .winsize => |ws| {
-                    try self.vaxis.resize(self.allocator,
-                                          self.tty.anyWriter(), ws);
-                    try self.render();
-                },
-            }
-        }
+    pub fn nextEvent(self: Environment) Event {
+        return self.loop.nextEvent();
     }
 
-    pub fn render(self: Environment) !void {
+    pub fn render(self: Environment, buffer: Buffer) !void {
         const window = self.vaxis.window();
         window.clear();
         var col: u16 = 0;
         var row: u16 = 0;
-        var graphemes = self.vaxis.unicode.graphemeIterator(self.text);
-        while (graphemes.next()) |grapheme| {
+        var graphemes_iter = buffer.iterate();
+        while (graphemes_iter.next()) |grapheme| {
+            const in_head = buffer.selection.inHead(grapheme);
+            const in_body = buffer.selection.inBody(grapheme);
             const style = Style {
-                .reverse = self.segment.contains(grapheme),
+                .reverse = in_head,
+                .ul_style = if (in_body) .single else .off,
             };
-            const bytes = grapheme.bytes(self.text);
+            const bytes = buffer.bytes(grapheme);
             if (eql(u8, bytes, "\n")) {
-                const width = window.gwidth("$");
-                defer col = 0;
-                defer row += 1;
-                window.writeCell(col, row, .{
-                    .char = .{ .grapheme = "$", .width = @intCast(width) },
-                    .style = style,
-                });
+                while (col < window.width) : (col += 1)
+                    window.writeCell(col, row, .{
+                        .char = .{ .grapheme = " " },
+                        .style = style,
+                    });
+                col = 0;
+                row += 1;
+                if (row >= window.height)
+                    break;
             } else if (eql(u8, bytes, "\t")) {
-                var tab = self.vaxis.unicode.graphemeIterator("  ");
+                var tab = graphemes.iterator("  ");
                 while (tab.next()) |g| {
                     const b = g.bytes("  ");
                     const width = window.gwidth(b);
@@ -110,20 +105,23 @@ const Environment = struct {
         try self.vaxis.render(self.tty.anyWriter());
     }
 
-    pub fn goUp(self: Environment) void {
-        self.segment.goUp();
+    pub fn resize(env: Environment, ws: Winsize) !void {
+        try env.vaxis.resize(env.allocator, env.tty.anyWriter(), ws);
     }
 
-    pub fn goRight(self: Environment) void {
-        self.segment.goRight(self.vaxis.unicode.width_data.graphemes, self.text);
+    pub fn yank(env: Environment, buffer: Buffer) !void {
+        try env.vaxis.copyToSystemClipboard(env.tty.anyWriter(),
+                                            buffer.selectedBytes(),
+                                            env.allocator);
     }
 
-    pub fn goLeft(self: Environment) void {
-        self.segment.goLeft(self.vaxis.unicode.width_data.graphemes, self.text);
+    pub fn requestPaste(env: Environment) !void {
+        try env.vaxis.requestSystemClipboard(env.tty.anyWriter());
     }
 
-    pub fn goDown(self: Environment) void {
-        self.segment.goDown(self.vaxis.unicode.width_data.graphemes, self.text);
+    pub fn paste(env: Environment, buffer: Buffer, data: []const u8) Buffer {
+        defer env.vaxis.opts.system_clipboard_allocator.?.free(data);
+        return buffer.paste(data);
     }
 };
 
@@ -137,45 +135,38 @@ pub fn main() !void {
 
     var tty = try Tty.init();
     defer tty.deinit();
-    var vx = try vaxis.init(allocator, .{});
+    var vx = try vaxis.init(allocator, .{
+        .system_clipboard_allocator = allocator,
+    });
     defer vx.deinit(allocator, tty.anyWriter());
+    graphemes = vx.unicode.width_data.graphemes;
+    defer graphemes = undefined;
 
     var loop = Loop(Event){ .tty = &tty, .vaxis = &vx };
     try loop.init();
     try loop.start();
     defer loop.stop();
 
-    const parser = Parser.create();
-    defer parser.destroy();
-
     const args = try argsAlloc(allocator);
     defer argsFree(allocator, args);
-    const text = try cwd().readFileAlloc(allocator, args[1], maxInt(u32));
-    defer allocator.free(text);
     try vx.enterAltScreen(tty.anyWriter());
     try vx.queryTerminal(tty.anyWriter(), 1 * ns_per_s); // for alt screen
 
-    const language = languages.c();
-    defer language.destroy();
-    try parser.setLanguage(language);
-    const tree = parser.parseString(text, null).?;
-    defer tree.destroy();
-
     zsanett.init();
     defer zsanett.deinit();
-    var segment = selection.Segment{
-        .head = .{ .node = tree.rootNode() },
-        .tail = .{ .node = tree.rootNode() },
-    };
-    try zsanett.def("env", Environment{
+    try zsanett.def("kay/path", args[1], "Path to file.");
+    try zsanett.def("kay/env", Environment{
         .allocator = allocator,
         .loop = &loop,
-        .parser = parser,
-        .segment = &segment,
-        .text = text,
         .tty = &tty,
         .vaxis = &vx,
-    }, "eval environment");
+    }, "Eval environment.");
+    try zsanett.defn(Buffer.open, .{
+        .prefix = "kay",
+        .name = "open",
+        .documentation = "(kay/open text)\n\nOpen text for editing.",
+        .source = @src(),
+    });
     switch (try zsanett.eval(void, @embedFile("main.janet"), "main.janet")) {
         .ret => {},
         .err => |err| @panic(err),