Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 84 additions & 2 deletions src/cli/ReplLine.zig
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ const History = struct {
}

pub fn append(self: *History, input: []const u8) Allocator.Error!void {
if (self.entries.items.len > 0) {
const last = self.entries.items[self.entries.items.len - 1];
if (std.mem.eql(u8, last, input)) return;
}
const input_copy = try self.allocator.alloc(u8, input.len);
@memcpy(input_copy, input);
try self.entries.append(self.allocator, input_copy);
Expand Down Expand Up @@ -244,6 +248,7 @@ const LineState = struct {
in_buffer: [8]u8,
history: *History,
history_index: ?usize,
transient_line: ?[]const u8,
/// Set after a Ctrl-C so that a second consecutive Ctrl-C quits. Any other
/// input event clears it, so the two presses must be back-to-back.
ctrl_c_armed: bool,
Expand Down Expand Up @@ -330,6 +335,7 @@ fn historyBackward(state: *LineState) CommandError!void {
if (hist_len == 0) return;

if (state.history_index == null) {
state.transient_line = try state.temp.dupe(u8, state.line_buffer.items);
state.history_index = hist_len - 1;
} else if (state.history_index.? > 0) {
state.history_index = state.history_index.? - 1;
Expand Down Expand Up @@ -357,10 +363,15 @@ fn historyForward(state: *LineState) CommandError!void {
try state.line_buffer.appendSlice(state.temp, entry);
state.col_offset = entry.len;
} else {
// Past the end, clear line
// Past the end, restore transient draft line
state.history_index = null;
state.line_buffer.clearAndFree(state.temp);
state.col_offset = 0;
if (state.transient_line) |transient| {
try state.line_buffer.appendSlice(state.temp, transient);
state.col_offset = transient.len;
} else {
state.col_offset = 0;
}
}

try ansi_term.setCursorColumn(state.out, state.prompt_width);
Expand Down Expand Up @@ -494,6 +505,7 @@ fn helper(self: *ReplLine, outlive: Allocator, std_io: std.Io, prompt: []const u
.in_buffer = undefined,
.history = &self.history,
.history_index = null,
.transient_line = null,
.ctrl_c_armed = false,
};

Expand Down Expand Up @@ -968,3 +980,73 @@ test "writeAlignedToPrompt: original indentation is preserved on top of prompt i
test "writeAlignedToPrompt: trailing newline still emits indent for empty next line" {
try expectAlignedOutput("z = 5\n", 2, "z = 5\n ");
}

test "History: basic appending and deduplication" {
var history = History.init(testing.allocator);
defer history.deinit();

try history.append("x = 1");
try history.append("y = 2");
try history.append("y = 2"); // should be ignored as consecutive duplicate

try testing.expectEqual(@as(usize, 2), history.entries.items.len);
try testing.expectEqualStrings("x = 1", history.entries.items[0]);
try testing.expectEqualStrings("y = 2", history.entries.items[1]);
}

test "History: transient line preservation" {
var history = History.init(testing.allocator);
defer history.deinit();

try history.append("first command");
try history.append("second command");

var arena = base.SingleThreadArena.init(testing.allocator);
defer arena.deinit();
const temp = arena.allocator();

var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();

var state = LineState{
.outlive = testing.allocator,
.temp = temp,
.prompt = "> ",
.prompt_width = 2,
.out = &aw.writer,
.in = undefined, // not used in history functions
.col_offset = 0,
.line_buffer = std.ArrayList(u8).empty,
.bytes_read = 0,
.in_buffer = undefined,
.history = &history,
.history_index = null,
.transient_line = null,
.ctrl_c_armed = false,
};

// Simulate typing a transient command "third draft"
try state.line_buffer.appendSlice(temp, "third draft");
state.col_offset = 11;

// Navigate back to history (second command)
try historyBackward(&state);
try testing.expectEqualStrings("second command", state.line_buffer.items);
try testing.expectEqualStrings("third draft", state.transient_line.?);
try testing.expectEqual(@as(?usize, 1), state.history_index);

// Navigate further back (first command)
try historyBackward(&state);
try testing.expectEqualStrings("first command", state.line_buffer.items);
try testing.expectEqual(@as(?usize, 0), state.history_index);

// Navigate forward (second command)
try historyForward(&state);
try testing.expectEqualStrings("second command", state.line_buffer.items);
try testing.expectEqual(@as(?usize, 1), state.history_index);

// Navigate past the end (restores transient command "third draft")
try historyForward(&state);
try testing.expectEqualStrings("third draft", state.line_buffer.items);
try testing.expect(state.history_index == null);
}
2 changes: 2 additions & 0 deletions src/cli/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11232,6 +11232,7 @@ fn rocRepl(ctx: *CliCtx, repl_args: cli_args.ReplArgs) CliMainError!void {
}

if (pending.items.len == 0 and std.mem.findAny(u8, raw_line, "\n\r") != null) {
try reader.history.append(raw_line);
should_exit = try processReplInput(ctx, &session, raw_line, report_config, &had_diagnostics);
continue;
}
Expand All @@ -11242,6 +11243,7 @@ fn rocRepl(ctx: *CliCtx, repl_args: cli_args.ReplArgs) CliMainError!void {
switch (try session.inputStatus(pending.items)) {
.incomplete => {},
.complete, .invalid => {
try reader.history.append(pending.items);
should_exit = try processReplInput(ctx, &session, pending.items, report_config, &had_diagnostics);
pending.clearRetainingCapacity();
},
Expand Down