Hugo Machet: 1 Add tab completion for files 1 files changed, 84 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/33224/mbox | git am -3Learn more about email & git
Implements: https://todo.sr.ht/~leon_plickat/nfm/17 --- v2 -> v3 * Tokenize before allocating the UTF8 string * Complete hidden files * Use an ArrayList of *File pointers * Return null in completion() if nothing is found * Escape space and already escaped space in file name src/nfm.zig | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/nfm.zig b/src/nfm.zig index ca4ee4d07714..ec419cebcfd4 100644 --- a/src/nfm.zig +++ b/src/nfm.zig @@ -534,6 +534,22 @@ fn handleInputUserInput(in: spoon.Input) !void { try handleReturnUserInput(); return; }, + '\t' => { + if (buffer.buffer.items.len < 2) return; + + var buf: []const u21 = undefined; + var it = mem.tokenize(u21, buffer.buffer.items, &[_]u21{' '}); + while (it.next()) |tok| buf = tok; + + 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 +674,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
builds.sr.ht <builds@sr.ht>nfm/patches: FAILED in 1m2s [Add tab completion for files][0] v3 from [Hugo Machet][1] [0]: https://lists.sr.ht/~leon_plickat/nfm/patches/33224 [1]: mailto:mail@hmachet.com ✓ #786541 SUCCESS nfm/patches/alpine.yml https://builds.sr.ht/~leon_plickat/job/786541 ✗ #786542 FAILED nfm/patches/freebsd.yml https://builds.sr.ht/~leon_plickat/job/786542