fuzzig

Fuzzy matcher based on the matching algorithm implementation of ms-edit as the editor is MIT-Licensed, this project is also under the MIT-License.

Caution

Only builds using the zig master version are tested to work.

Install

Add or update this library as a dependency in your zig project run the following command:

zig fetch --save git+https://gitea.yves-biener.de/yves-biener/fuzzig

Afterwards add the library as a dependency to any module in your build.zig:

const fuzzig_dependency = b.dependency("fuzzig", .{
  .target = target,
  .optimize = optimize,
});

Usage

The following snippet shows a test case to illustrate usage of fuzzig to run fuzzy matches:

test "matching `s` on local files" {
    var gpa = testing.allocator;

    // files to fuzzy match against
    var files: std.ArrayList([]const u8) = .empty;
    defer {
        for (files.items) |file| gpa.free(file);
        files.deinit(gpa);
    }

    // fuzzy matching results (containing only the scores)
    var results: std.ArrayList(Result) = .empty;
    defer {
        for (results.items) |*result| result.deinit(gpa);
        results.deinit(gpa);
    }

    // arrange
    var dir = try std.fs.cwd().openDir(".", .{ .iterate = true });
    defer dir.close();

    var iter = try dir.walk(gpa);
    defer iter.deinit();

    while (try iter.next()) |entry| {
        switch (entry.kind) {
            .file => {
                if (std.mem.startsWith(u8, entry.path, ".git/")) continue;
                if (std.mem.startsWith(u8, entry.path, ".zig-cache")) continue;
                const path = try gpa.dupe(u8, entry.path[0..entry.path.len]);
                try files.append(gpa, path);
            },
            else => continue,
        }
    }
    try results.ensureTotalCapacity(gpa, files.items.len);

    // act
    const search = "s";

    // create fuzzy score for each file entry
    for (0.., files.items) |idx, entry| {
        const result = try match(gpa, entry, search, idx) orelse continue;
        try results.append(gpa, result);
    }
    // sort scores by their received score descending
    std.sort.heap(Result, results.items, {}, greaterThan);

    var buf: [128]u8 = undefined;
    var buffer = std.fs.File.stderr().writer(&buf);
    var writer = &buffer.interface;
    defer writer.flush() catch unreachable;

    std.debug.lockStdErr();
    defer std.debug.unlockStdErr();

    // assert
    var scored_entries: usize = 0;
    var unscored_entries: usize = 0;
    for (results.items) |result| {
        if (result.score > 0) scored_entries += 1 else unscored_entries += 1;
        if (result.score == 0) continue; // do not print results that are unmatched

        const item = files.items[result.index];
        var match_highlights: []u8 = try gpa.alloc(u8, item.len);
        defer gpa.free(match_highlights);

        @memset(match_highlights, ' ');
        // highlight what caused this search result
        for (result.positions.items) |pos| match_highlights[pos] = '^';
        // print item and its highlighted positions
        // NOTE uncomment the print for the writer to show matches and their highlights of what matched
        // -> as the writer prints to *stderr* writing will cause the test to fail, hence it is commented out by default
        try writer.print("{s}\n{s}\n", .{ item, match_highlights });
    }
    try testing.expectEqual(5, scored_entries);
    try testing.expectEqual(results.items.len - 5, unscored_entries);
}

Resulting in the output of the found (and ordered) matches:

src/root.zig
^
LICENSE
     ^
.gitea/workflows/test.yaml
                   ^
.gitea/workflows/release.yaml
                      ^
.typos-config
     ^
Description
Fuzzy search as a zig library
Readme MIT 40 KiB
Languages
Zig 100%