Implements: https://todo.sr.ht/~leon_plickat/nfm/17
---
v4 -> v5
Rebase and fixes with latest master
src/InputBuffer.zig | 9 +++++
src/nfm.zig | 87 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 96 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 76decbdc4726..488cc234c5f8 100644
--- a/src/nfm.zig
+++ b/src/nfm.zig
@@ -35,6 +35,7 @@ const time = std.time;
const CommandTokenizer = @import("CommandTokenizer.zig");
const Config = @import("Config.zig");
const DirMap = @import("DirMap.zig");
+const File = @import("File.zig");
const History = @import("History.zig");
const Mode = @import("mode.zig").Mode;
const UserInterface = @import("UserInterface.zig");
@@ -570,6 +571,21 @@ 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;
+
+ const alloc = context.gpa.allocator();
+ var to_complete = try util.codepointSliceToUtf8SlizeZAlloc(alloc, buf);
+ defer alloc.free(to_complete);
+
+ if (try completion(to_complete)) |compl| {
+ try buffer.insertFormat(compl);
+ defer alloc.free(compl);
+ }
+ },
127 => { // Backspace.
if (context.mode.user_input.history_index != null) return;
buffer.delete(.left, 1);
@@ -775,6 +791,77 @@ fn mouseClickPathFromTitle(_x: usize) !void {
// Fallthrough means the user clicked on the current CWD.
}
+/// 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 {
+ const alloc = context.gpa.allocator();
+
+ var found: std.ArrayListUnmanaged(*File) = .{};
+ defer found.deinit(alloc);
+
+ for (context.dirmap.cwd.files.items) |*file| {
+ if (file.name.len <= input.len) continue;
+ if (ascii.startsWithIgnoreCase(file.name, input)) {
+ try found.append(alloc, 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 {
+ const alloc = context.gpa.allocator();
+ 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(alloc, "\\\\\\ ");
+ i += 1;
+ } else {
+ try ret.append(alloc, s[i]);
+ }
+ } else if (s[i] == ' ') {
+ try ret.appendSlice(alloc, "\\ ");
+ } else {
+ try ret.append(alloc, s[i]);
+ }
+ }
+
+ return ret.toOwnedSlice(alloc);
+}
+
test {
_ = @import("ini.zig");
_ = @import("CommandTokenizer.zig");
--
2.37.1