rush
created pr with
128.1
cmds
checkout latest patchset:
ssh pr.pico.sh print 128 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 128.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 128
Patchset
128.1
feat: tab completion for tilde expansions
Eric Bower
2026-06-18T14:46:01ZThis enables the ability to tab complete when `cd ~/<tab>`
Semantic diff summary
2 added,
20 modified,
0 signature changed,
0 removed
across 6 analyzed files
src/completion.zig
-
chunklines 11-17modified -
function_declarationappendProviderCandidatesmodified -
function_declarationappendFunctionProviderCandidatesmodified -
function_declarationdefaultPathApplicationmodified -
function_declarationdefaultApplicationmodified -
function_declarationpathCandidatesmodified -
function_declarationpathCandidatesWithOptionsmodified -
function_declarationappendShellEscapedValuemodified
+13
-6
src/completion.zig
#
| ... | ... | @@ -11,6 +11,7 @@ const runner = @import("runner.zig"); | |
| 11 | 11 | const runtime = @import("runtime.zig"); | |
| 12 | 12 | const shell_state_mod = @import("shell/state.zig"); | |
| 13 | 13 | const editor_completion = @import("editor/completion.zig"); | |
| 14 | + | const expand = @import("shell/expand.zig"); | |
| 14 | 15 | ||
| 15 | 16 | pub const CancellationToken = editor_completion.CancellationToken; | |
| 16 | 17 | pub const Kind = editor_completion.Kind; |
| ... | ... | @@ -1658,6 +1659,7 @@ fn appendProviderCandidates( | |
| 1658 | 1659 | semantic.replace_start, | |
| 1659 | 1660 | semantic.replace_end, | |
| 1660 | 1661 | .{}, | |
| 1662 | + | shell_state.envLookup(), | |
| 1661 | 1663 | ); | |
| 1662 | 1664 | defer freeCandidates(allocator, candidates); | |
| 1663 | 1665 | for (candidates) |candidate| try builder.appendCandidateIfMissing(allocator, candidate); |
| ... | ... | @@ -1670,6 +1672,7 @@ fn appendProviderCandidates( | |
| 1670 | 1672 | semantic.replace_start, | |
| 1671 | 1673 | semantic.replace_end, | |
| 1672 | 1674 | .{ .directories_only = true }, | |
| 1675 | + | shell_state.envLookup(), | |
| 1673 | 1676 | ); | |
| 1674 | 1677 | defer freeCandidates(allocator, candidates); | |
| 1675 | 1678 | for (candidates) |candidate| try builder.appendCandidateIfMissing(allocator, candidate); |
| ... | ... | @@ -2241,7 +2245,7 @@ pub fn defaultPathApplication( | |
| 2241 | 2245 | const word = try decodeShellCompletionSlice(allocator, source, replace_start, replace_end); | |
| 2242 | 2246 | defer allocator.free(word); | |
| 2243 | 2247 | ||
| 2244 | - | const candidates = try pathCandidates(allocator, io, word, replace_start, replace_end); | |
| 2248 | + | const candidates = try pathCandidates(allocator, io, word, replace_start, replace_end, .{}); | |
| 2245 | 2249 | defer freeCandidates(allocator, candidates); | |
| 2246 | 2250 | return applyCandidatesForInputWithPolicy(allocator, source, candidates, .prefixOnly()); | |
| 2247 | 2251 | } |
| ... | ... | @@ -2270,7 +2274,7 @@ pub fn defaultApplication( | |
| 2270 | 2274 | const candidates = if (context.kind == .command and std.mem.indexOfScalar(u8, word, '/') == null) | |
| 2271 | 2275 | try commandCandidates(allocator, io, shell_state, replace_start, replace_end) | |
| 2272 | 2276 | else | |
| 2273 | - | try pathCandidates(allocator, io, word, replace_start, replace_end); | |
| 2277 | + | try pathCandidates(allocator, io, word, replace_start, replace_end, shell_state.envLookup()); | |
| 2274 | 2278 | defer freeCandidates(allocator, candidates); | |
| 2275 | 2279 | return applyCandidatesForInputWithPolicy(allocator, source, candidates, .prefixOnly()); | |
| 2276 | 2280 | } |
| ... | ... | @@ -2296,8 +2300,9 @@ fn pathCandidates( | |
| 2296 | 2300 | word: []const u8, | |
| 2297 | 2301 | replace_start: usize, | |
| 2298 | 2302 | replace_end: usize, | |
| 2303 | + | env: expand.EnvLookup, | |
| 2299 | 2304 | ) ![]Candidate { | |
| 2300 | - | return pathCandidatesWithOptions(allocator, io, word, replace_start, replace_end, .{}); | |
| 2305 | + | return pathCandidatesWithOptions(allocator, io, word, replace_start, replace_end, .{}, env); | |
| 2301 | 2306 | } | |
| 2302 | 2307 | ||
| 2303 | 2308 | const PathCandidateOptions = struct { |
| ... | ... | @@ -2311,12 +2316,14 @@ fn pathCandidatesWithOptions( | |
| 2311 | 2316 | replace_start: usize, | |
| 2312 | 2317 | replace_end: usize, | |
| 2313 | 2318 | options: PathCandidateOptions, | |
| 2319 | + | env: expand.EnvLookup, | |
| 2314 | 2320 | ) ![]Candidate { | |
| 2315 | 2321 | const split = std.mem.findScalarLast(u8, word, '/'); | |
| 2316 | 2322 | const dir_prefix = if (split) |index| word[0 .. index + 1] else ""; | |
| 2317 | 2323 | const entry_prefix = if (split) |index| word[index + 1 ..] else word; | |
| 2318 | - | const dir_path = pathDirectoryToOpen(dir_prefix); | |
| 2319 | - | ||
| 2324 | + | const dir_path_raw = if (dir_prefix.len == 0) "." else dir_prefix; | |
| 2325 | + | const dir_path = try expand.expandTilde(allocator, dir_path_raw, env); | |
| 2326 | + | defer allocator.free(dir_path); | |
| 2320 | 2327 | var dir = std.Io.Dir.cwd().openDir(io, dir_path, .{ .iterate = true }) catch |err| switch (err) { | |
| 2321 | 2328 | error.FileNotFound, error.NotDir, error.AccessDenied => return &.{}, | |
| 2322 | 2329 | else => return err, |
| ... | ... | @@ -2644,8 +2651,8 @@ fn appendShellEscapedValue( | |
| 2644 | 2651 | } | |
| 2645 | 2652 | ||
| 2646 | 2653 | fn needsUnquotedEscape(byte: u8, index: usize) bool { | |
| 2654 | + | _ = index; | |
| 2647 | 2655 | if (std.ascii.isWhitespace(byte)) return true; | |
| 2648 | - | if (index == 0 and byte == '~') return true; | |
| 2649 | 2656 | return switch (byte) { | |
| 2650 | 2657 | '\\', '\'', '"', '`', '$', '&', '|', ';', '<', '>', '(', ')', '[', ']', '{', '}', '*', '?', '!', '#' => true, | |
| 2651 | 2658 | else => false, |
+0
-2
src/editor/driver.zig
#
| ... | ... | @@ -32,7 +32,6 @@ const completion_progress_start = "\x1b]9;4;3\x07"; | |
| 32 | 32 | const completion_progress_stop = "\x1b]9;4;0\x07"; | |
| 33 | 33 | const completion_progress_delay_ms = 500; | |
| 34 | 34 | ||
| 35 | - | ||
| 36 | 35 | pub const TerminalEvent = terminal.Event; | |
| 37 | 36 | pub const ColorScheme = terminal.ColorScheme; | |
| 38 | 37 | pub const ColorReport = terminal.ColorReport; |
+0
-1
src/editor/vi.zig
#
+7
-1
src/extensions/editor/rush_complete.zig
#
| ... | ... | @@ -6,6 +6,7 @@ const api = @import("../api.zig"); | |
| 6 | 6 | const editor_completion = @import("../../editor/completion.zig"); | |
| 7 | 7 | const shell_builtin = @import("../../shell/builtin.zig"); | |
| 8 | 8 | const shell_state_mod = @import("../../shell/state.zig"); | |
| 9 | + | const expand = @import("../../shell/expand.zig"); | |
| 9 | 10 | ||
| 10 | 11 | pub const builtins = [_]shell_builtin.Builtin{ | |
| 11 | 12 | shell_builtin.Builtin.initExtension("rush_complete", .extension_state), |
| ... | ... | @@ -317,7 +321,9 @@ fn appendPathCandidates(state: *State, directories_only: bool) !void { | |
| 317 | 321 | const split_after_separator = separator_index != 0 or std.mem.startsWith(u8, state.prefix, "/"); | |
| 318 | 322 | const dir_prefix = if (split_after_separator) state.prefix[0 .. separator_index + 1] else ""; | |
| 319 | 323 | const entry_prefix = if (split_after_separator) state.prefix[separator_index + 1 ..] else state.prefix; | |
| 320 | - | const dir_path = if (dir_prefix.len == 0) "." else dir_prefix; | |
| 324 | + | const dir_path_raw = if (dir_prefix.len == 0) "." else dir_prefix; | |
| 325 | + | const dir_path = try expand.expandTilde(state.allocator, dir_path_raw, state.env_lookup); | |
| 326 | + | defer state.allocator.free(dir_path); | |
| 321 | 327 | var dir = std.Io.Dir.cwd().openDir(state.io, dir_path, .{ .iterate = true }) catch |err| switch (err) { | |
| 322 | 328 | error.FileNotFound, error.NotDir, error.AccessDenied => return, | |
| 323 | 329 | else => return err, |
+32
-36
src/shell/eval.zig
#
| ... | ... | @@ -328,22 +328,19 @@ fn semanticCommandExecutionFrame( | |
| 328 | 328 | }; | |
| 329 | 329 | try fd_table.applyRedirectionPlan(allocator, redirections); | |
| 330 | 330 | const stdin: execution_frame.InputEndpoint = switch (fd_table.boundEndpoint(0) orelse | |
| 331 | - | @as(execution_frame.FdEndpoint, .{ .input = parent_frame.spec.stdin })) | |
| 332 | - | { | |
| 331 | + | @as(execution_frame.FdEndpoint, .{ .input = parent_frame.spec.stdin })) { | |
| 333 | 332 | .input => |input| input, | |
| 334 | 333 | .closed => .closed, | |
| 335 | 334 | .output => parent_frame.spec.stdin, | |
| 336 | 335 | }; | |
| 337 | 336 | const stdout: execution_frame.OutputEndpoint = switch (fd_table.boundEndpoint(1) orelse | |
| 338 | - | @as(execution_frame.FdEndpoint, .{ .output = parent_frame.spec.stdout })) | |
| 339 | - | { | |
| 337 | + | @as(execution_frame.FdEndpoint, .{ .output = parent_frame.spec.stdout })) { | |
| 340 | 338 | .output => |output| output, | |
| 341 | 339 | .closed => .discard, | |
| 342 | 340 | .input => parent_frame.spec.stdout, | |
| 343 | 341 | }; | |
| 344 | 342 | const stderr: execution_frame.OutputEndpoint = switch (fd_table.boundEndpoint(2) orelse | |
| 345 | - | @as(execution_frame.FdEndpoint, .{ .output = parent_frame.spec.stderr })) | |
| 346 | - | { | |
| 343 | + | @as(execution_frame.FdEndpoint, .{ .output = parent_frame.spec.stderr })) { | |
| 347 | 344 | .output => |output| output, | |
| 348 | 345 | .closed => .discard, | |
| 349 | 346 | .input => parent_frame.spec.stderr, |
| ... | ... | @@ -488,8 +485,7 @@ fn captureSet( | |
| 488 | 485 | .command_substitution_stdout => .{ .channels = &.{ .pipeline_data, .command_substitution_stdout } }, | |
| 489 | 486 | }, | |
| 490 | 487 | .command_substitution_stdout => switch (second orelse | |
| 491 | - | return .{ .channels = &.{.command_substitution_stdout} }) | |
| 492 | - | { | |
| 488 | + | return .{ .channels = &.{.command_substitution_stdout} }) { | |
| 493 | 489 | .side_stdout => .{ .channels = &.{ .command_substitution_stdout, .side_stdout } }, | |
| 494 | 490 | .side_stderr => .{ .channels = &.{ .command_substitution_stdout, .side_stderr } }, | |
| 495 | 491 | .pipeline_data => .{ .channels = &.{ .command_substitution_stdout, .pipeline_data } }, |
| ... | ... | @@ -5172,24 +5168,24 @@ fn evaluateCommandSubstitutionBody( | |
| 5172 | 5168 | .simple => |plan| blk: { | |
| 5173 | 5169 | var input = EvaluationInput.empty(); | |
| 5174 | 5170 | break :blk evaluatePlanWithInput( | |
| 5175 | - | evaluator, | |
| 5176 | - | substitution_state, | |
| 5177 | - | substitution_context.withTarget(plan.target), | |
| 5178 | - | plan, | |
| 5179 | - | &input, | |
| 5180 | - | frame, | |
| 5181 | - | ); | |
| 5171 | + | evaluator, | |
| 5172 | + | substitution_state, | |
| 5173 | + | substitution_context.withTarget(plan.target), | |
| 5174 | + | plan, | |
| 5175 | + | &input, | |
| 5176 | + | frame, | |
| 5177 | + | ); | |
| 5182 | 5178 | }, | |
| 5183 | 5179 | .compound => |plan| blk: { | |
| 5184 | 5180 | var input = EvaluationInput.empty(); | |
| 5185 | 5181 | break :blk evaluateCompoundPlanWithInput( | |
| 5186 | - | evaluator, | |
| 5187 | - | substitution_state, | |
| 5188 | - | substitution_context.withTarget(plan.target), | |
| 5189 | - | plan, | |
| 5190 | - | &input, | |
| 5191 | - | frame, | |
| 5192 | - | ); | |
| 5182 | + | evaluator, | |
| 5183 | + | substitution_state, | |
| 5184 | + | substitution_context.withTarget(plan.target), | |
| 5185 | + | plan, | |
| 5186 | + | &input, | |
| 5187 | + | frame, | |
| 5188 | + | ); | |
| 5193 | 5189 | }, | |
| 5194 | 5190 | .pipeline => |plan| evaluatePipelinePlanWithFrame( | |
| 5195 | 5191 | evaluator, |
| ... | ... | @@ -5230,24 +5226,24 @@ fn evaluateCommandSubstitutionBodyPayload( | |
| 5230 | 5226 | .simple => |plan| blk: { | |
| 5231 | 5227 | var input = EvaluationInput.empty(); | |
| 5232 | 5228 | break :blk evaluatePlanWithInput( | |
| 5233 | - | evaluator, | |
| 5234 | - | substitution_state, | |
| 5235 | - | substitution_context.withTarget(plan.target), | |
| 5236 | - | plan, | |
| 5237 | - | &input, | |
| 5238 | - | frame, | |
| 5239 | - | ); | |
| 5229 | + | evaluator, | |
| 5230 | + | substitution_state, | |
| 5231 | + | substitution_context.withTarget(plan.target), | |
| 5232 | + | plan, | |
| 5233 | + | &input, | |
| 5234 | + | frame, | |
| 5235 | + | ); | |
| 5240 | 5236 | }, | |
| 5241 | 5237 | .compound => |plan| blk: { | |
| 5242 | 5238 | var input = EvaluationInput.empty(); | |
| 5243 | 5239 | break :blk evaluateCompoundPlanWithInput( | |
| 5244 | - | evaluator, | |
| 5245 | - | substitution_state, | |
| 5246 | - | substitution_context.withTarget(plan.target), | |
| 5247 | - | plan, | |
| 5248 | - | &input, | |
| 5249 | - | frame, | |
| 5250 | - | ); | |
| 5240 | + | evaluator, | |
| 5241 | + | substitution_state, | |
| 5242 | + | substitution_context.withTarget(plan.target), | |
| 5243 | + | plan, | |
| 5244 | + | &input, | |
| 5245 | + | frame, | |
| 5246 | + | ); | |
| 5251 | 5247 | }, | |
| 5252 | 5248 | .pipeline => |plan| evaluatePipelinePlanWithFrame( | |
| 5253 | 5249 | evaluator, |
+14
-1
src/shell/state.zig
#
| ... | ... | @@ -8,6 +8,7 @@ const command_plan = @import("command_plan.zig"); | |
| 8 | 8 | const context = @import("context.zig"); | |
| 9 | 9 | const runtime_process = @import("../runtime/process.zig"); | |
| 10 | 10 | const trap_semantics = @import("trap.zig"); | |
| 11 | + | const expand = @import("expand.zig"); | |
| 11 | 12 | ||
| 12 | 13 | pub const ExitStatus = u8; | |
| 13 | 14 | pub const TrapSignal = trap_semantics.Signal; |
| ... | ... | @@ -430,6 +430,12 @@ pub const BackgroundJobNotification = struct { | |
| 430 | 430 | } | |
| 431 | 431 | }; | |
| 432 | 432 | ||
| 433 | + | fn shellStateLookup(ctx: ?*const anyopaque, name: []const u8) ?[]const u8 { | |
| 434 | + | const state: *const ShellState = @ptrCast(@alignCast(ctx.?)); | |
| 435 | + | const vr = state.getVariable(name); | |
| 436 | + | return if (vr) |v| v.value else null; | |
| 437 | + | } | |
| 438 | + | ||
| 433 | 439 | pub const ShellState = struct { | |
| 434 | 440 | allocator: std.mem.Allocator, | |
| 435 | 441 | scope: Scope = .current_shell, |
| ... | ... | @@ -460,6 +466,13 @@ pub const ShellState = struct { | |
| 460 | 466 | running, | |
| 461 | 467 | }; | |
| 462 | 468 | ||
| 469 | + | pub fn envLookup(self: *const ShellState) expand.EnvLookup { | |
| 470 | + | return .{ | |
| 471 | + | .context = self, | |
| 472 | + | .lookupFn = shellStateLookup, | |
| 473 | + | }; | |
| 474 | + | } | |
| 475 | + | ||
| 463 | 476 | pub fn init(allocator: std.mem.Allocator) ShellState { | |
| 464 | 477 | return .{ .allocator = allocator }; | |
| 465 | 478 | } |