dashboard / erock/rush / feat: tab completion for tilde expansions #128 rss

open · opened on 2026-06-18T17:34:30Z by erock
Help
checkout latest patchset:
ssh pr.pico.sh print pr-128 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print ps-X | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 128
add review to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add --review 128
accept PR:
ssh pr.pico.sh pr accept 128
close PR:
ssh pr.pico.sh pr close 128
Timeline Patchsets
This enables the ability to tab complete when `cd ~/<tab>`
+13 -6 src/completion.zig link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
diff --git a/src/completion.zig b/src/completion.zig
index 68e8c73..11c9f18 100644
--- a/src/completion.zig
+++ b/src/completion.zig
@@ -11,6 +11,7 @@ const runner = @import("runner.zig");
 const runtime = @import("runtime.zig");
 const shell_state_mod = @import("shell/state.zig");
 const editor_completion = @import("editor/completion.zig");
+const expand = @import("shell/expand.zig");
 
 pub const CancellationToken = editor_completion.CancellationToken;
 pub const Kind = editor_completion.Kind;
@@ -1658,6 +1659,7 @@ fn appendProviderCandidates(
                 semantic.replace_start,
                 semantic.replace_end,
                 .{},
+                shell_state.envLookup(),
             );
             defer freeCandidates(allocator, candidates);
             for (candidates) |candidate| try builder.appendCandidateIfMissing(allocator, candidate);
@@ -1670,6 +1672,7 @@ fn appendProviderCandidates(
                 semantic.replace_start,
                 semantic.replace_end,
                 .{ .directories_only = true },
+                shell_state.envLookup(),
             );
             defer freeCandidates(allocator, candidates);
             for (candidates) |candidate| try builder.appendCandidateIfMissing(allocator, candidate);
@@ -1735,6 +1738,7 @@ fn appendFunctionProviderCandidates(
         value_position,
         parsed_options,
         operands,
+        shell_state.envLookup(),
     );
     defer provider_state.deinit();
 
@@ -2241,7 +2245,7 @@ pub fn defaultPathApplication(
     const word = try decodeShellCompletionSlice(allocator, source, replace_start, replace_end);
     defer allocator.free(word);
 
-    const candidates = try pathCandidates(allocator, io, word, replace_start, replace_end);
+    const candidates = try pathCandidates(allocator, io, word, replace_start, replace_end, .{});
     defer freeCandidates(allocator, candidates);
     return applyCandidatesForInputWithPolicy(allocator, source, candidates, .prefixOnly());
 }
@@ -2270,7 +2274,7 @@ pub fn defaultApplication(
     const candidates = if (context.kind == .command and std.mem.indexOfScalar(u8, word, '/') == null)
         try commandCandidates(allocator, io, shell_state, replace_start, replace_end)
     else
-        try pathCandidates(allocator, io, word, replace_start, replace_end);
+        try pathCandidates(allocator, io, word, replace_start, replace_end, shell_state.envLookup());
     defer freeCandidates(allocator, candidates);
     return applyCandidatesForInputWithPolicy(allocator, source, candidates, .prefixOnly());
 }
@@ -2296,8 +2300,9 @@ fn pathCandidates(
     word: []const u8,
     replace_start: usize,
     replace_end: usize,
+    env: expand.EnvLookup,
 ) ![]Candidate {
-    return pathCandidatesWithOptions(allocator, io, word, replace_start, replace_end, .{});
+    return pathCandidatesWithOptions(allocator, io, word, replace_start, replace_end, .{}, env);
 }
 
 const PathCandidateOptions = struct {
@@ -2311,12 +2316,14 @@ fn pathCandidatesWithOptions(
     replace_start: usize,
     replace_end: usize,
     options: PathCandidateOptions,
+    env: expand.EnvLookup,
 ) ![]Candidate {
     const split = std.mem.findScalarLast(u8, word, '/');
     const dir_prefix = if (split) |index| word[0 .. index + 1] else "";
     const entry_prefix = if (split) |index| word[index + 1 ..] else word;
-    const dir_path = pathDirectoryToOpen(dir_prefix);
-
+    const dir_path_raw = if (dir_prefix.len == 0) "." else dir_prefix;
+    const dir_path = try expand.expandTilde(allocator, dir_path_raw, env);
+    defer allocator.free(dir_path);
     var dir = std.Io.Dir.cwd().openDir(io, dir_path, .{ .iterate = true }) catch |err| switch (err) {
         error.FileNotFound, error.NotDir, error.AccessDenied => return &.{},
         else => return err,
@@ -2644,8 +2651,8 @@ fn appendShellEscapedValue(
 }
 
 fn needsUnquotedEscape(byte: u8, index: usize) bool {
+    _ = index;
     if (std.ascii.isWhitespace(byte)) return true;
-    if (index == 0 and byte == '~') return true;
     return switch (byte) {
         '\\', '\'', '"', '`', '$', '&', '|', ';', '<', '>', '(', ')', '[', ']', '{', '}', '*', '?', '!', '#' => true,
         else => false,
+0 -2 src/editor/driver.zig link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
diff --git a/src/editor/driver.zig b/src/editor/driver.zig
index 456e1f6..306cc1c 100644
--- a/src/editor/driver.zig
+++ b/src/editor/driver.zig
@@ -32,7 +32,6 @@ const completion_progress_start = "\x1b]9;4;3\x07";
 const completion_progress_stop = "\x1b]9;4;0\x07";
 const completion_progress_delay_ms = 500;
 
-
 pub const TerminalEvent = terminal.Event;
 pub const ColorScheme = terminal.ColorScheme;
 pub const ColorReport = terminal.ColorReport;
@@ -144,7 +143,6 @@ pub const ReadLineResult = union(enum) {
 
 const completion_flash_ms = 80;
 
-
 pub const TerminalSession = struct {
     allocator: std.mem.Allocator,
     io: std.Io,
+0 -1 src/editor/vi.zig link
1
2
3
4
5
6
7
8
9
diff --git a/src/editor/vi.zig b/src/editor/vi.zig
index 64c1edf..c22ea72 100644
--- a/src/editor/vi.zig
+++ b/src/editor/vi.zig
@@ -678,4 +678,3 @@ pub fn isAsciiWhitespace(byte: u8) bool {
         else => false,
     };
 }
-
+7 -1 src/extensions/editor/rush_complete.zig link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
diff --git a/src/extensions/editor/rush_complete.zig b/src/extensions/editor/rush_complete.zig
index 08ae08d..b831349 100644
--- a/src/extensions/editor/rush_complete.zig
+++ b/src/extensions/editor/rush_complete.zig
@@ -6,6 +6,7 @@ const api = @import("../api.zig");
 const editor_completion = @import("../../editor/completion.zig");
 const shell_builtin = @import("../../shell/builtin.zig");
 const shell_state_mod = @import("../../shell/state.zig");
+const expand = @import("../../shell/expand.zig");
 
 pub const builtins = [_]shell_builtin.Builtin{
     shell_builtin.Builtin.initExtension("rush_complete", .extension_state),
@@ -36,6 +37,7 @@ pub const State = struct {
     operands: []const ParsedOperand,
     candidates: std.ArrayList(editor_completion.Candidate) = .empty,
     next_source_order: usize = 0,
+    env_lookup: expand.EnvLookup,
 
     pub fn init(
         allocator: std.mem.Allocator,
@@ -48,6 +50,7 @@ pub const State = struct {
         value_position: []const u8,
         parsed_options: []const ParsedOption,
         operands: []const ParsedOperand,
+        env_lookup: expand.EnvLookup,
     ) State {
         return .{
             .allocator = allocator,
@@ -60,6 +63,7 @@ pub const State = struct {
             .value_position = value_position,
             .parsed_options = parsed_options,
             .operands = operands,
+            .env_lookup = env_lookup,
         };
     }
 
@@ -317,7 +321,9 @@ fn appendPathCandidates(state: *State, directories_only: bool) !void {
     const split_after_separator = separator_index != 0 or std.mem.startsWith(u8, state.prefix, "/");
     const dir_prefix = if (split_after_separator) state.prefix[0 .. separator_index + 1] else "";
     const entry_prefix = if (split_after_separator) state.prefix[separator_index + 1 ..] else state.prefix;
-    const dir_path = if (dir_prefix.len == 0) "." else dir_prefix;
+    const dir_path_raw = if (dir_prefix.len == 0) "." else dir_prefix;
+    const dir_path = try expand.expandTilde(state.allocator, dir_path_raw, state.env_lookup);
+    defer state.allocator.free(dir_path);
     var dir = std.Io.Dir.cwd().openDir(state.io, dir_path, .{ .iterate = true }) catch |err| switch (err) {
         error.FileNotFound, error.NotDir, error.AccessDenied => return,
         else => return err,
+32 -36 src/shell/eval.zig link
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
diff --git a/src/shell/eval.zig b/src/shell/eval.zig
index 73af41f..26158a7 100644
--- a/src/shell/eval.zig
+++ b/src/shell/eval.zig
@@ -328,22 +328,19 @@ fn semanticCommandExecutionFrame(
     };
     try fd_table.applyRedirectionPlan(allocator, redirections);
     const stdin: execution_frame.InputEndpoint = switch (fd_table.boundEndpoint(0) orelse
-        @as(execution_frame.FdEndpoint, .{ .input = parent_frame.spec.stdin }))
-    {
+        @as(execution_frame.FdEndpoint, .{ .input = parent_frame.spec.stdin })) {
         .input => |input| input,
         .closed => .closed,
         .output => parent_frame.spec.stdin,
     };
     const stdout: execution_frame.OutputEndpoint = switch (fd_table.boundEndpoint(1) orelse
-        @as(execution_frame.FdEndpoint, .{ .output = parent_frame.spec.stdout }))
-    {
+        @as(execution_frame.FdEndpoint, .{ .output = parent_frame.spec.stdout })) {
         .output => |output| output,
         .closed => .discard,
         .input => parent_frame.spec.stdout,
     };
     const stderr: execution_frame.OutputEndpoint = switch (fd_table.boundEndpoint(2) orelse
-        @as(execution_frame.FdEndpoint, .{ .output = parent_frame.spec.stderr }))
-    {
+        @as(execution_frame.FdEndpoint, .{ .output = parent_frame.spec.stderr })) {
         .output => |output| output,
         .closed => .discard,
         .input => parent_frame.spec.stderr,
@@ -488,8 +485,7 @@ fn captureSet(
             .command_substitution_stdout => .{ .channels = &.{ .pipeline_data, .command_substitution_stdout } },
         },
         .command_substitution_stdout => switch (second orelse
-            return .{ .channels = &.{.command_substitution_stdout} })
-        {
+            return .{ .channels = &.{.command_substitution_stdout} }) {
             .side_stdout => .{ .channels = &.{ .command_substitution_stdout, .side_stdout } },
             .side_stderr => .{ .channels = &.{ .command_substitution_stdout, .side_stderr } },
             .pipeline_data => .{ .channels = &.{ .command_substitution_stdout, .pipeline_data } },
@@ -5172,24 +5168,24 @@ fn evaluateCommandSubstitutionBody(
         .simple => |plan| blk: {
             var input = EvaluationInput.empty();
             break :blk evaluatePlanWithInput(
-            evaluator,
-            substitution_state,
-            substitution_context.withTarget(plan.target),
-            plan,
-            &input,
-            frame,
-        );
+                evaluator,
+                substitution_state,
+                substitution_context.withTarget(plan.target),
+                plan,
+                &input,
+                frame,
+            );
         },
         .compound => |plan| blk: {
             var input = EvaluationInput.empty();
             break :blk evaluateCompoundPlanWithInput(
-            evaluator,
-            substitution_state,
-            substitution_context.withTarget(plan.target),
-            plan,
-            &input,
-            frame,
-        );
+                evaluator,
+                substitution_state,
+                substitution_context.withTarget(plan.target),
+                plan,
+                &input,
+                frame,
+            );
         },
         .pipeline => |plan| evaluatePipelinePlanWithFrame(
             evaluator,
@@ -5230,24 +5226,24 @@ fn evaluateCommandSubstitutionBodyPayload(
         .simple => |plan| blk: {
             var input = EvaluationInput.empty();
             break :blk evaluatePlanWithInput(
-            evaluator,
-            substitution_state,
-            substitution_context.withTarget(plan.target),
-            plan,
-            &input,
-            frame,
-        );
+                evaluator,
+                substitution_state,
+                substitution_context.withTarget(plan.target),
+                plan,
+                &input,
+                frame,
+            );
         },
         .compound => |plan| blk: {
             var input = EvaluationInput.empty();
             break :blk evaluateCompoundPlanWithInput(
-            evaluator,
-            substitution_state,
-            substitution_context.withTarget(plan.target),
-            plan,
-            &input,
-            frame,
-        );
+                evaluator,
+                substitution_state,
+                substitution_context.withTarget(plan.target),
+                plan,
+                &input,
+                frame,
+            );
         },
         .pipeline => |plan| evaluatePipelinePlanWithFrame(
             evaluator,
+14 -1 src/shell/state.zig link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
diff --git a/src/shell/state.zig b/src/shell/state.zig
index fd43145..4f638a6 100644
--- a/src/shell/state.zig
+++ b/src/shell/state.zig
@@ -8,6 +8,7 @@ const command_plan = @import("command_plan.zig");
 const context = @import("context.zig");
 const runtime_process = @import("../runtime/process.zig");
 const trap_semantics = @import("trap.zig");
+const expand = @import("expand.zig");
 
 pub const ExitStatus = u8;
 pub const TrapSignal = trap_semantics.Signal;
@@ -221,7 +222,6 @@ pub const VariableAttributes = struct {
     readonly: bool = false,
 };
 
-
 pub const Variable = struct {
     value: []const u8,
     exported: bool = false,
@@ -430,6 +430,12 @@ pub const BackgroundJobNotification = struct {
     }
 };
 
+fn shellStateLookup(ctx: ?*const anyopaque, name: []const u8) ?[]const u8 {
+    const state: *const ShellState = @ptrCast(@alignCast(ctx.?));
+    const vr = state.getVariable(name);
+    return if (vr) |v| v.value else null;
+}
+
 pub const ShellState = struct {
     allocator: std.mem.Allocator,
     scope: Scope = .current_shell,
@@ -460,6 +466,13 @@ pub const ShellState = struct {
         running,
     };
 
+    pub fn envLookup(self: *const ShellState) expand.EnvLookup {
+        return .{
+            .context = self,
+            .lookupFn = shellStateLookup,
+        };
+    }
+
     pub fn init(allocator: std.mem.Allocator) ShellState {
         return .{ .allocator = allocator };
     }