~gpanders/wk

785887cd46b9ff998c33e47d190ba1d97343220e — Greg Anders a month ago 8b5ba70
Refactor backlinks logic

Instead of parsing backlinks when getting the list of Zettels, only find
backlinks when they are actually being written. Also use a HashMap to
represent backlinks instead of a pointer to the Zettel object, which is
much safer.
4 files changed, 62 insertions(+), 70 deletions(-)

M src/cmd/backlinks.zig
M src/cmd/new.zig
M src/cmd/open.zig
M src/zettel.zig
M src/cmd/backlinks.zig => src/cmd/backlinks.zig +6 -5
@@ 6,6 6,7 @@ const stdout = std.io.getStdOut().outStream();
const Command = @import("../cmd.zig").Command;
const Zettel = @import("../zettel.zig").Zettel;
const getZettels = @import("../zettel.zig").getZettels;
const updateBacklinks = @import("../zettel.zig").updateBacklinks;

pub const cmd = Command{
    .name = "backlinks",


@@ 16,13 17,13 @@ pub const cmd = Command{
};

pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    const zettels = try getZettels(allocator);
    try updateBacklinks(allocator, zettels);

    var nmodified: u32 = 0;
    for (try getZettels(allocator)) |zet| {
        try zet.writeBacklinks();
    for (zettels) |zet| {
        if (try zet.modified()) nmodified += 1;
    }

    if (nmodified > 0) {
        stdout.print("Updated backlinks in {} notes.\n", .{nmodified}) catch return;
    }
    stdout.print("Updated backlinks in {} notes.\n", .{nmodified}) catch return;
}

M src/cmd/new.zig => src/cmd/new.zig +6 -14
@@ 10,6 10,7 @@ const Zettel = @import("../zettel.zig").Zettel;
const execAndCheck = @import("../util.zig").execAndCheck;
const getZettels = @import("../zettel.zig").getZettels;
const openZettels = @import("../zettel.zig").openZettels;
const updateBacklinks = @import("../zettel.zig").updateBacklinks;

pub const cmd = Command{
    .name = "new",


@@ 24,6 25,7 @@ pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!v
        const title = args.?[0];
        var new_zettel = try Zettel.new(allocator, title);

        // Add new Zettel to git if in a git repo
        if (fs.cwd().access(".git", .{})) |_| {
            // Fail early if git is not found
            _ = execAndCheck(allocator, &[_][]const u8{


@@ 36,6 38,7 @@ pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!v
            };
        } else |_| {}

        // If stdout is not a tty just print the new Zettel's filename
        if (!io.getStdOut().isTty()) {
            stdout.print("{}\n", .{new_zettel.fname}) catch {};
            return;


@@ 45,21 48,10 @@ pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!v

        new_zettel.contents = try new_zettel.read();

        const zettels = try getZettels(allocator);
        const links = try new_zettel.links();
        var nmodified: u32 = 0;
        for (links) |link| {
            for (zettels) |*zet| {
                if (mem.eql(u8, link, zet.id)) {
                    try zet.backlinks.append(&new_zettel);
                    if (try zet.modified()) nmodified += 1;
                }
            }
        }
        var zettels = std.ArrayList(Zettel).fromOwnedSlice(allocator, try getZettels(allocator));
        try zettels.append(new_zettel);

        if (nmodified > 0) {
            stdout.print("Updated backlinks in {} notes.\n", .{nmodified}) catch return;
        }
        try updateBacklinks(allocator, zettels.items);
    } else {
        return error.MissingRequiredArgument;
    }

M src/cmd/open.zig => src/cmd/open.zig +7 -6
@@ 9,6 9,7 @@ const Command = @import("../cmd.zig").Command;
const Zettel = @import("../zettel.zig").Zettel;
const getZettels = @import("../zettel.zig").getZettels;
const openZettels = @import("../zettel.zig").openZettels;
const updateBacklinks = @import("../zettel.zig").updateBacklinks;

pub const cmd = Command{
    .name = "open",


@@ 19,15 20,15 @@ pub const cmd = Command{
};

pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    var zettels = std.ArrayList([]const u8).init(allocator);
    defer zettels.deinit();
    var files = std.ArrayList([]const u8).init(allocator);

    const all_zettels = try getZettels(allocator);
    const zettels = try getZettels(allocator);
    if (args) |patterns| {
        for (all_zettels) |zet| {
            if (zet.match(patterns)) try zettels.append(zet.fname);
        for (zettels) |zet| {
            if (zet.match(patterns)) try files.append(zet.fname);
        }
    }

    try openZettels(allocator, zettels.items);
    try openZettels(allocator, files.items);
    try updateBacklinks(allocator, zettels);
}

M src/zettel.zig => src/zettel.zig +43 -45
@@ 23,7 23,6 @@ pub const Zettel = struct {
    title: []const u8,
    mtime: i64,
    contents: []const u8,
    backlinks: std.ArrayList(*const Zettel),

    /// Create a new Zettel with the given title.
    pub fn new(allocator: *mem.Allocator, title: []const u8) !Zettel {


@@ 56,7 55,6 @@ pub const Zettel = struct {
            .title = try mem.dupe(allocator, u8, title),
            .mtime = (try file.stat()).mtime,
            .contents = contents,
            .backlinks = std.ArrayList(*const Zettel).init(allocator),
        };
    }



@@ 131,38 129,6 @@ pub const Zettel = struct {

        return links_list.toOwnedSlice();
    }

    /// Write backlinks to file
    pub fn writeBacklinks(self: Zettel) !void {
        if (self.backlinks.items.len == 0) return;

        const start_index = if (mem.indexOf(u8, self.contents, bl_header)) |s|
            s - 1
        else
            self.contents.len;

        var backlinks = try std.ArrayList([]const u8).initCapacity(self.allocator, self.backlinks.items.len);
        for (self.backlinks.items) |bl| {
            comptime var template = "- [[{}]] {}";
            const item = try fmt.allocPrint(self.allocator, template, .{ bl.id, bl.title });
            backlinks.appendAssumeCapacity(item);
        }

        const new_contents = try mem.join(self.allocator, "\n", &[_][]const u8{
            self.contents[0 .. start_index - 1],
            "",
            bl_header,
            "",
            try mem.join(self.allocator, "\n", backlinks.items),
        });

        if (!mem.eql(u8, new_contents, self.contents)) {
            var file = try fs.cwd().openFile(self.fname, .{ .write = true });
            defer file.close();

            try file.outStream().writeAll(new_contents);
        }
    }
};

pub fn openZettels(allocator: *mem.Allocator, zettels: []const []const u8) !void {


@@ 213,19 179,52 @@ pub fn getZettels(allocator: *mem.Allocator) ![]Zettel {
        }
    }.lessThan);

    // Sorting *must* be done before setting backlinks since we are using
    // pointers in the backlinks list. If sorting happens after then the
    // pointers end up pointing to the wrong object.
    for (zettels.items) |*zet| {
        const links = try zet.links();
        for (links) |link| {
            for (zettels.items) |*other| {
                if (mem.eql(u8, link, other.id)) try other.backlinks.append(zet);
    return zettels.toOwnedSlice();
}

/// Find all backlinks between notes and write them to file
pub fn updateBacklinks(allocator: *mem.Allocator, zettels: []const Zettel) !void {
    for (zettels) |zet| {
        var backlinks = std.StringHashMap([]const u8).init(allocator);
        for (zettels) |other| {
            if (mem.eql(u8, other.id, zet.id)) continue;
            for (try other.links()) |link| {
                if (mem.eql(u8, link, zet.id)) {
                    _ = try backlinks.put(other.id, other.title);
                }
            }
        }
    }

    return zettels.toOwnedSlice();
        if (backlinks.size == 0) continue;

        const start_index = if (mem.indexOf(u8, zet.contents, bl_header)) |s|
            s - 1
        else
            zet.contents.len;

        var bl_list = try std.ArrayList([]const u8).initCapacity(allocator, backlinks.size);
        var it = backlinks.iterator();
        while (it.next()) |kv| {
            comptime var template = "- [[{}]] {}";
            const item = try fmt.allocPrint(allocator, template, .{ kv.key, kv.value });
            bl_list.appendAssumeCapacity(item);
        }

        const new_contents = try mem.join(allocator, "\n", &[_][]const u8{
            zet.contents[0 .. start_index - 1],
            "",
            bl_header,
            "",
            try mem.join(allocator, "\n", bl_list.items),
        });

        if (!mem.eql(u8, new_contents, zet.contents)) {
            var file = try fs.cwd().openFile(zet.fname, .{ .write = true });
            defer file.close();

            try file.outStream().writeAll(new_contents);
        }
    }
}

pub fn strftime(allocator: *mem.Allocator, format: [:0]const u8) ![]const u8 {


@@ 308,7 307,6 @@ fn fromEntry(allocator: *mem.Allocator, entry: fs.Dir.Entry) !Zettel {
        .title = title.?,
        .mtime = (try file.stat()).mtime,
        .contents = contents,
        .backlinks = std.ArrayList(*const Zettel).init(allocator),
    };
}