~leon_plickat/nfm

nfm: Add tab completion for files v2 PROPOSED

Hugo Machet: 1
 Add tab completion for files

 1 files changed, 55 insertions(+), 0 deletions(-)
#781943 alpine.yml success
#781944 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/33059/mbox | git am -3
Learn more about email & git

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

Implements: https://todo.sr.ht/~leon_plickat/nfm/17
---
v1 -> v2:
  Rebased and modifed to work with dirmap re-design

 src/nfm.zig | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 55 insertions(+)

diff --git a/src/nfm.zig b/src/nfm.zig
index bc5fe5870bc6..e39f578cd173 100644
--- a/src/nfm.zig
+++ b/src/nfm.zig
@@ -512,6 +512,19 @@ fn handleInputUserInput(in: spoon.Input) !void {
                try handleReturnUserInput();
                return;
            },
            '\t' => {
                var current = try util.codepointSliceToUtf8SlizeZAlloc(context.gpa, buffer.buffer.items);
                defer context.gpa.free(current);

                var it = mem.tokenize(u8, current, " ");
                var buf: []const u8 = undefined;
                while (it.next()) |tok| buf = tok;

                if (buf.len < 2) return;
                var comp = try completion(buf);
                if (mem.eql(u8, comp, buf)) return;
                try buffer.insertFormat(comp);
            },
            127 => { // Backspace.
                if (context.mode.user_input.history_index != null) return;
                buffer.delete(.left, 1);
@@ -623,6 +636,48 @@ 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([:0]const u8) = .{};
    defer found.deinit(context.gpa);

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

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

/// 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];
}

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

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

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

✓ #781943 SUCCESS nfm/patches/alpine.yml  https://builds.sr.ht/~leon_plickat/job/781943
✗ #781944 FAILED  nfm/patches/freebsd.yml https://builds.sr.ht/~leon_plickat/job/781944
> + '\t' => {
> + var current = try util.codepointSliceToUtf8SlizeZAlloc(context.gpa,
> buffer.buffer.items);
> + defer context.gpa.free(current);
> +
> + var it = mem.tokenize(u8, current, " ");
> + var buf: []const u8 = undefined;
> + while (it.next()) |tok| buf = tok;
> +
> + if (buf.len < 2) return;
> + var comp = try completion(buf);
> + if (mem.eql(u8, comp, buf)) return;
> + try buffer.insertFormat(comp);
> + },
I think it might be better to first tokenize and then allocate an
utf8 slice containing only the word we are interested in.

Not sure if mem.tokenize() will play along, but since we are only
looking for whitespace, this could be done by hand anyway. Just start
at the cursor position and iterate backwards until you find whitespace
(careful to ignore whitespace directly at the curosr).
> +/// 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([:0]const u8) = .{};
> + defer found.deinit(context.gpa);
> +
> + for (context.dirmap.cwd.files.items) |file| {
> + if (!context.show_hidden and file.isHidden()) continue;
I think we came to the conclusion to not skip hidden files, so this
check should be removed (otherwise you could just use DirMap.visible_files).

Also maybe a list of *File pointers might be better than duping all
similar names we find.