~leon_plickat/nfm

nfm: Add tab completion for files v4 PROPOSED

Hugo Machet: 1
 Add tab completion for files

 2 files changed, 90 insertions(+), 0 deletions(-)
#786872 alpine.yml success
#786873 freebsd.yml failed
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~leon_plickat/nfm/patches/33247/mbox | git am -3
Learn more about email & git

[PATCH nfm v4] Add tab completion for files Export this patch

Implements: https://todo.sr.ht/~leon_plickat/nfm/17
---
v3 -> v4
  Add a InputBuffer.getCursorToken() to complete the word where the cursor
  is instead of the last word of the buffer.

 src/InputBuffer.zig |  9 +++++
 src/nfm.zig         | 81 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 90 insertions(+)

diff --git a/src/InputBuffer.zig b/src/InputBuffer.zig
index d7d053d60810..06c31c72d32b 100644
--- a/src/InputBuffer.zig
+++ b/src/InputBuffer.zig
@@ -56,6 +56,15 @@ pub fn moveCursor(self: *Self, comptime direction: Direction, amount: usize) voi
    }
}

pub fn getCursorToken(self: *Self) []u21 {
    var i: usize = self.cursor;
    while (i > 0) {
        if (isSpace(self.buffer.items[i - 1])) break;
        i -= 1;
    }
    return self.buffer.items[i..self.cursor];
}

pub fn delete(self: *Self, comptime direction: Direction, amount: usize) void {
    var i: usize = 0;
    switch (direction) {
diff --git a/src/nfm.zig b/src/nfm.zig
index ca4ee4d07714..be8a34f04ed7 100644
--- a/src/nfm.zig
+++ b/src/nfm.zig
@@ -534,6 +534,19 @@ fn handleInputUserInput(in: spoon.Input) !void {
                try handleReturnUserInput();
                return;
            },
            '\t' => {
                if (buffer.buffer.items.len < 2) return;

                var buf = buffer.getCursorToken();
                if (buf.len < 2) return;

                var to_complete = try util.codepointSliceToUtf8SlizeZAlloc(context.gpa, buf);
                defer context.gpa.free(to_complete);

                if (try completion(to_complete)) |compl| {
                    try buffer.insertFormat(compl);
                }
            },
            127 => { // Backspace.
                if (context.mode.user_input.history_index != null) return;
                buffer.delete(.left, 1);
@@ -658,6 +671,74 @@ fn handleReturnUserInput() !void {
    }
}

/// Compare current input with files/directories names. Complete the name if
/// there is only one match else only complete the common part.
/// Case insensitive.
fn completion(input: []const u8) !?[]const u8 {
    var found: std.ArrayListUnmanaged(*DirMap.File) = .{};
    defer found.deinit(context.gpa);

    for (context.dirmap.cwd.files.items) |*file| {
        if (file.name.len <= input.len) continue;
        if (ascii.startsWithIgnoreCase(file.name, input)) {
            try found.append(context.gpa, file);
        }
    }

    if (found.items.len == 0) {
        return null;
    } else if (found.items.len == 1) {
        const escaped = try escapeSpace(found.items[0].name[input.len..]);
        return escaped;
    } else {
        var common: []const u8 = found.items[0].name;
        var i: usize = 1;
        while (i < found.items.len) : (i += 1) {
            common = commonStart(common, found.items[i].name);
        }
        const escaped = try escapeSpace(common[input.len..]);
        return escaped;
    }
}

/// Compare two strings and return the common beginning part.
/// Case insensitive.
fn commonStart(a: []const u8, b: []const u8) []const u8 {
    const len: usize = if (a.len < b.len) a.len else b.len;

    var offset: usize = 0;
    while (offset < len) : (offset += 1) {
        if (ascii.toLower(a[offset]) != ascii.toLower(b[offset])) break;
    }

    return a[0..offset];
}

/// Escape space in file names.
///   foo bar -> foo\ bar
///   foo\ bar -> foo\\\ bar
fn escapeSpace(s: []const u8) ![]const u8 {
    var ret: std.ArrayListUnmanaged(u8) = .{};

    var i: usize = 0;
    while (i < s.len) : (i += 1) {
        if (s[i] == '\\') {
            if (s[i + 1] == ' ') {
                try ret.appendSlice(context.gpa, "\\\\\\ ");
                i += 1;
            } else {
                try ret.append(context.gpa, s[i]);
            }
        } else if (s[i] == ' ') {
            try ret.appendSlice(context.gpa, "\\ ");
        } else {
            try ret.append(context.gpa, s[i]);
        }
    }

    return ret.toOwnedSlice(context.gpa);
}

test {
    _ = @import("ini.zig");
    _ = @import("CommandTokenizer.zig");
-- 
2.36.1
nfm/patches: FAILED in 55s

[Add tab completion for files][0] v4 from [Hugo Machet][1]

[0]: https://lists.sr.ht/~leon_plickat/nfm/patches/33247
[1]: mailto:mail@hmachet.com

✓ #786872 SUCCESS nfm/patches/alpine.yml  https://builds.sr.ht/~leon_plickat/job/786872
✗ #786873 FAILED  nfm/patches/freebsd.yml https://builds.sr.ht/~leon_plickat/job/786873