Hugo Machet: 1 Add tab completion for files 1 files changed, 55 insertions(+), 0 deletions(-)
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 -3Learn more about email & git
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..]; + } +}
I think it might be nicer if this function did not return the entire string, but only the completion bit. So it "pro" gets completed to "projects", only "jects" should be returned. If no completion is found return null. That way, callsite can manage the function a bit nicer I think, as you'd only have to do something like this: if (complete(token)) |compl| { input_buffer.insert(compl); }
+
Neat! BTW, I think there'll be a problem with file names containing spaces. Either we should insert quotes, or automatically escape them. Friendly greetings, Leon Henrik Plickat
+/// 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
builds.sr.ht <builds@sr.ht>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.