mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
1. Fix blank line handling in readLineSimple - blank lines in piped input are now treated as valid input instead of EOF. Only return null when a true EOF is encountered with no pending data. 2. Replace std.fs.path.join with bun.path.join in saveHistory for cross-platform path construction. 3. Fix enterEditorMode to use cross-platform APIs - replace std.posix.STDIN_FILENO and std.posix.read with bun.FileDescriptor.stdin() and bun.sys.read() for Windows compatibility. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1679 lines
62 KiB
Zig
1679 lines
62 KiB
Zig
//! Bun REPL - A powerful, feature-rich interactive JavaScript/TypeScript shell
|
|
//!
|
|
//! Features:
|
|
//! - Syntax highlighting with QuickAndDirtyJavaScriptSyntaxHighlighter
|
|
//! - Smart autocomplete for globals, Bun APIs, and object properties
|
|
//! - Persistent history with fuzzy search
|
|
//! - Multi-line editing with bracket matching
|
|
//! - REPL commands (.help, .editor, .load, .save, .clear, .exit)
|
|
//! - TypeScript/JSX support via Bun's transpiler with replMode
|
|
//! - Shell mode ($`...`) for running commands
|
|
//! - Inline package installation
|
|
//! - Pretty-printed output with colors
|
|
//! - Top-level await support
|
|
//! - Error formatting with source context
|
|
|
|
// ============================================================================
|
|
// REPL State
|
|
// ============================================================================
|
|
|
|
/// The main REPL state structure
|
|
pub const Repl = struct {
|
|
allocator: Allocator,
|
|
vm: *VirtualMachine,
|
|
global: *JSGlobalObject,
|
|
|
|
// Line editing state
|
|
line_buffer: ArrayList(u8),
|
|
cursor_pos: usize = 0,
|
|
history: History,
|
|
|
|
// Multi-line state
|
|
multiline_buffer: ArrayList(u8),
|
|
is_multiline: bool = false,
|
|
bracket_depth: BracketDepth = .{},
|
|
|
|
// Display state
|
|
prompt_len: usize = 0,
|
|
terminal_width: u16 = 80,
|
|
terminal_height: u16 = 24,
|
|
enable_colors: bool = true,
|
|
|
|
// REPL options
|
|
show_timing: bool = false,
|
|
quiet_mode: bool = false,
|
|
line_number: u32 = 1,
|
|
|
|
// Execution context - stores declared variables across lines
|
|
context_object: JSValue = .js_undefined,
|
|
|
|
const Self = @This();
|
|
|
|
pub fn init(allocator: Allocator, vm: *VirtualMachine) !*Self {
|
|
const repl = try allocator.create(Self);
|
|
repl.* = .{
|
|
.allocator = allocator,
|
|
.vm = vm,
|
|
.global = vm.global,
|
|
.line_buffer = .{},
|
|
.history = try History.init(allocator),
|
|
.multiline_buffer = .{},
|
|
};
|
|
|
|
// Get terminal size
|
|
repl.updateTerminalSize();
|
|
|
|
// Check color support
|
|
repl.enable_colors = Output.enable_ansi_colors_stdout;
|
|
|
|
// Create the REPL context object for variable persistence
|
|
repl.context_object = try repl.createContext();
|
|
|
|
return repl;
|
|
}
|
|
|
|
pub fn deinit(self: *Self) void {
|
|
self.line_buffer.deinit(self.allocator);
|
|
self.multiline_buffer.deinit(self.allocator);
|
|
self.history.deinit();
|
|
self.allocator.destroy(self);
|
|
}
|
|
|
|
fn createContext(self: *Self) !JSValue {
|
|
// Create a context object that will hold REPL-declared variables
|
|
// This allows variables to persist across REPL lines
|
|
const ctx = JSValue.createEmptyObject(self.global, 0);
|
|
|
|
// Add common globals to context
|
|
// The actual execution will use vm.runInContext which merges this with globalThis
|
|
|
|
return ctx;
|
|
}
|
|
|
|
fn updateTerminalSize(self: *Self) void {
|
|
if (Output.terminal_size.col > 0) {
|
|
self.terminal_width = Output.terminal_size.col;
|
|
self.terminal_height = Output.terminal_size.row;
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// Input Handling
|
|
// ========================================================================
|
|
|
|
/// Read a line of input with editing support
|
|
pub fn readLine(self: *Self) !?[]const u8 {
|
|
self.line_buffer.clearRetainingCapacity();
|
|
self.cursor_pos = 0;
|
|
|
|
// Print prompt
|
|
self.printPrompt();
|
|
|
|
const stdin_fd = bun.FileDescriptor.stdin();
|
|
|
|
// On Windows or when TTY mode unavailable, use simple line reading
|
|
if (bun.Environment.isWindows) {
|
|
return self.readLineSimple(stdin_fd);
|
|
}
|
|
|
|
// Try to enter raw mode for character-by-character input (POSIX only)
|
|
const maybe_termios = std.posix.tcgetattr(stdin_fd.cast());
|
|
|
|
if (maybe_termios) |original_termios| {
|
|
// TTY mode - use interactive line editing
|
|
return self.readLineTTY(stdin_fd, original_termios);
|
|
} else |_| {
|
|
// Non-TTY mode (piped input) - use simple line reading
|
|
return self.readLineSimple(stdin_fd);
|
|
}
|
|
}
|
|
|
|
/// Simple line reading for non-TTY input (piped)
|
|
fn readLineSimple(self: *Self, stdin_fd: bun.FileDescriptor) !?[]const u8 {
|
|
var buf: [1]u8 = undefined;
|
|
var saw_eof = false;
|
|
|
|
while (true) {
|
|
const read_result = bun.sys.read(stdin_fd, &buf);
|
|
const bytes_read = switch (read_result) {
|
|
.result => |n| n,
|
|
.err => |e| {
|
|
if (e.getErrno() == .AGAIN) continue;
|
|
return e.toZigErr();
|
|
},
|
|
};
|
|
|
|
if (bytes_read == 0) {
|
|
// EOF - only return null if we have no data and this is a true EOF
|
|
saw_eof = true;
|
|
break;
|
|
}
|
|
|
|
if (buf[0] == '\n' or buf[0] == '\r') {
|
|
// End of line - return what we have (even if empty, to allow blank lines)
|
|
break;
|
|
}
|
|
|
|
try self.line_buffer.append(self.allocator, buf[0]);
|
|
}
|
|
|
|
// Only return null on true EOF with no pending data
|
|
if (saw_eof and self.line_buffer.items.len == 0) {
|
|
return null;
|
|
}
|
|
|
|
// Return the line (may be empty for blank lines, which is valid input)
|
|
return try self.allocator.dupe(u8, self.line_buffer.items);
|
|
}
|
|
|
|
/// Interactive line reading with TTY support (POSIX only)
|
|
fn readLineTTY(self: *Self, stdin_fd: bun.FileDescriptor, original_termios: std.posix.termios) !?[]const u8 {
|
|
var raw = original_termios;
|
|
|
|
// Disable canonical mode and echo
|
|
raw.lflag.ICANON = false;
|
|
raw.lflag.ECHO = false;
|
|
raw.lflag.ISIG = false; // Disable Ctrl-C signal
|
|
|
|
// Set minimum characters and timeout
|
|
raw.cc[@intFromEnum(std.posix.V.MIN)] = 1;
|
|
raw.cc[@intFromEnum(std.posix.V.TIME)] = 0;
|
|
|
|
try std.posix.tcsetattr(stdin_fd.cast(), .NOW, raw);
|
|
defer std.posix.tcsetattr(stdin_fd.cast(), .NOW, original_termios) catch {};
|
|
|
|
var buf: [32]u8 = undefined;
|
|
|
|
while (true) {
|
|
const read_result = bun.sys.read(stdin_fd, &buf);
|
|
const bytes_read = switch (read_result) {
|
|
.result => |n| n,
|
|
.err => |e| {
|
|
if (e.getErrno() == .AGAIN) continue;
|
|
return e.toZigErr();
|
|
},
|
|
};
|
|
|
|
if (bytes_read == 0) {
|
|
// EOF
|
|
if (self.line_buffer.items.len == 0) {
|
|
return null;
|
|
}
|
|
break;
|
|
}
|
|
|
|
const input = buf[0..bytes_read];
|
|
|
|
// Handle escape sequences
|
|
if (input[0] == 0x1b) {
|
|
if (bytes_read >= 3 and input[1] == '[') {
|
|
switch (input[2]) {
|
|
'A' => self.historyPrev(), // Up arrow
|
|
'B' => self.historyNext(), // Down arrow
|
|
'C' => self.moveCursorRight(), // Right arrow
|
|
'D' => self.moveCursorLeft(), // Left arrow
|
|
'H' => self.moveCursorHome(), // Home
|
|
'F' => self.moveCursorEnd(), // End
|
|
'3' => if (bytes_read >= 4 and input[3] == '~') self.deleteChar(), // Delete
|
|
else => {},
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
switch (input[0]) {
|
|
0x03 => {
|
|
// Ctrl-C
|
|
Output.print("\n", .{});
|
|
if (self.line_buffer.items.len > 0 or self.is_multiline) {
|
|
// Cancel current input
|
|
self.line_buffer.clearRetainingCapacity();
|
|
self.multiline_buffer.clearRetainingCapacity();
|
|
self.is_multiline = false;
|
|
self.bracket_depth = .{};
|
|
self.printPrompt();
|
|
continue;
|
|
}
|
|
// Empty line - exit hint
|
|
Output.pretty("<d>(To exit, press Ctrl+D or type .exit)<r>\n", .{});
|
|
self.printPrompt();
|
|
continue;
|
|
},
|
|
0x04 => {
|
|
// Ctrl-D (EOF)
|
|
if (self.line_buffer.items.len == 0 and !self.is_multiline) {
|
|
Output.print("\n", .{});
|
|
return null;
|
|
}
|
|
self.deleteChar();
|
|
},
|
|
0x09 => {
|
|
// Tab - autocomplete
|
|
try self.handleAutocomplete();
|
|
},
|
|
0x0A, 0x0D => {
|
|
// Enter
|
|
Output.print("\n", .{});
|
|
|
|
// Check if we need to continue on next line
|
|
const line = self.line_buffer.items;
|
|
self.updateBracketDepth(line);
|
|
|
|
if (self.needsContinuation()) {
|
|
// Continue to next line
|
|
try self.multiline_buffer.appendSlice(self.allocator, line);
|
|
try self.multiline_buffer.append(self.allocator, '\n');
|
|
self.line_buffer.clearRetainingCapacity();
|
|
self.cursor_pos = 0;
|
|
self.is_multiline = true;
|
|
self.printPrompt();
|
|
continue;
|
|
}
|
|
|
|
// Complete input
|
|
if (self.is_multiline) {
|
|
try self.multiline_buffer.appendSlice(self.allocator, line);
|
|
const result = try self.allocator.dupe(u8, self.multiline_buffer.items);
|
|
self.multiline_buffer.clearRetainingCapacity();
|
|
self.is_multiline = false;
|
|
self.bracket_depth = .{};
|
|
return result;
|
|
}
|
|
|
|
if (line.len == 0) {
|
|
self.printPrompt();
|
|
continue;
|
|
}
|
|
|
|
return try self.allocator.dupe(u8, line);
|
|
},
|
|
0x7F, 0x08 => {
|
|
// Backspace
|
|
self.backspace();
|
|
},
|
|
0x01 => self.moveCursorHome(), // Ctrl-A
|
|
0x05 => self.moveCursorEnd(), // Ctrl-E
|
|
0x0B => self.killToEnd(), // Ctrl-K
|
|
0x15 => self.killLine(), // Ctrl-U
|
|
0x17 => self.killWord(), // Ctrl-W
|
|
0x0C => self.clearScreen(), // Ctrl-L
|
|
0x12 => try self.reverseSearch(), // Ctrl-R
|
|
else => {
|
|
// Regular character input
|
|
if (input[0] >= 0x20 and input[0] < 0x7F) {
|
|
try self.insertChar(input[0]);
|
|
} else if (input[0] >= 0x80) {
|
|
// UTF-8 sequence
|
|
try self.line_buffer.appendSlice(self.allocator, input);
|
|
self.cursor_pos += input.len;
|
|
self.refreshLine();
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
return try self.allocator.dupe(u8, self.line_buffer.items);
|
|
}
|
|
|
|
fn insertChar(self: *Self, c: u8) !void {
|
|
if (self.cursor_pos >= self.line_buffer.items.len) {
|
|
try self.line_buffer.append(self.allocator, c);
|
|
} else {
|
|
try self.line_buffer.insert(self.allocator, self.cursor_pos, c);
|
|
}
|
|
self.cursor_pos += 1;
|
|
self.refreshLine();
|
|
}
|
|
|
|
fn backspace(self: *Self) void {
|
|
if (self.cursor_pos > 0) {
|
|
_ = self.line_buffer.orderedRemove(self.cursor_pos - 1);
|
|
self.cursor_pos -= 1;
|
|
self.refreshLine();
|
|
}
|
|
}
|
|
|
|
fn deleteChar(self: *Self) void {
|
|
if (self.cursor_pos < self.line_buffer.items.len) {
|
|
_ = self.line_buffer.orderedRemove(self.cursor_pos);
|
|
self.refreshLine();
|
|
}
|
|
}
|
|
|
|
fn moveCursorLeft(self: *Self) void {
|
|
if (self.cursor_pos > 0) {
|
|
self.cursor_pos -= 1;
|
|
Output.print("\x1b[D", .{});
|
|
}
|
|
}
|
|
|
|
fn moveCursorRight(self: *Self) void {
|
|
if (self.cursor_pos < self.line_buffer.items.len) {
|
|
self.cursor_pos += 1;
|
|
Output.print("\x1b[C", .{});
|
|
}
|
|
}
|
|
|
|
fn moveCursorHome(self: *Self) void {
|
|
if (self.cursor_pos > 0) {
|
|
Output.print("\x1b[{d}D", .{self.cursor_pos});
|
|
self.cursor_pos = 0;
|
|
}
|
|
}
|
|
|
|
fn moveCursorEnd(self: *Self) void {
|
|
const remaining = self.line_buffer.items.len - self.cursor_pos;
|
|
if (remaining > 0) {
|
|
Output.print("\x1b[{d}C", .{remaining});
|
|
self.cursor_pos = self.line_buffer.items.len;
|
|
}
|
|
}
|
|
|
|
fn killToEnd(self: *Self) void {
|
|
self.line_buffer.shrinkRetainingCapacity(self.cursor_pos);
|
|
self.refreshLine();
|
|
}
|
|
|
|
fn killLine(self: *Self) void {
|
|
self.line_buffer.clearRetainingCapacity();
|
|
self.cursor_pos = 0;
|
|
self.refreshLine();
|
|
}
|
|
|
|
fn killWord(self: *Self) void {
|
|
if (self.cursor_pos == 0) return;
|
|
|
|
var new_pos = self.cursor_pos;
|
|
// Skip trailing spaces
|
|
while (new_pos > 0 and self.line_buffer.items[new_pos - 1] == ' ') {
|
|
new_pos -= 1;
|
|
}
|
|
// Skip word characters
|
|
while (new_pos > 0 and self.line_buffer.items[new_pos - 1] != ' ') {
|
|
new_pos -= 1;
|
|
}
|
|
|
|
// Remove the word
|
|
const items_to_remove = self.cursor_pos - new_pos;
|
|
var i: usize = 0;
|
|
while (i < items_to_remove) : (i += 1) {
|
|
_ = self.line_buffer.orderedRemove(new_pos);
|
|
}
|
|
self.cursor_pos = new_pos;
|
|
self.refreshLine();
|
|
}
|
|
|
|
fn clearScreen(self: *Self) void {
|
|
Output.print("\x1b[2J\x1b[H", .{});
|
|
self.printPrompt();
|
|
self.refreshLine();
|
|
}
|
|
|
|
fn refreshLine(self: *Self) void {
|
|
// Clear current line and rewrite with syntax highlighting
|
|
Output.print("\r\x1b[K", .{}); // Move to start, clear to end
|
|
|
|
self.printPromptInline();
|
|
|
|
// Print with syntax highlighting
|
|
if (self.enable_colors and self.line_buffer.items.len > 0) {
|
|
const highlighter = fmt.fmtJavaScript(self.line_buffer.items, .{
|
|
.enable_colors = true,
|
|
.check_for_unhighlighted_write = true,
|
|
});
|
|
Output.print("{f}", .{highlighter});
|
|
} else {
|
|
Output.print("{s}", .{self.line_buffer.items});
|
|
}
|
|
|
|
// Move cursor to correct position
|
|
const cursor_offset = self.line_buffer.items.len - self.cursor_pos;
|
|
if (cursor_offset > 0) {
|
|
Output.print("\x1b[{d}D", .{cursor_offset});
|
|
}
|
|
|
|
Output.flush();
|
|
}
|
|
|
|
// ========================================================================
|
|
// Prompt Display
|
|
// ========================================================================
|
|
|
|
fn printPrompt(self: *Self) void {
|
|
self.printPromptInline();
|
|
Output.flush();
|
|
}
|
|
|
|
fn printPromptInline(self: *Self) void {
|
|
if (self.is_multiline) {
|
|
// Continuation prompt
|
|
Output.pretty("<cyan>...<r> ", .{});
|
|
self.prompt_len = 4;
|
|
} else {
|
|
// Main prompt with line number
|
|
Output.pretty("<b><green>bun<r><d>:<r><yellow>{d}<r><b><green>><r> ", .{self.line_number});
|
|
// Calculate prompt length (approximate)
|
|
self.prompt_len = 8 + digitCount(self.line_number);
|
|
}
|
|
}
|
|
|
|
fn digitCount(n: u32) usize {
|
|
if (n == 0) return 1;
|
|
var count: usize = 0;
|
|
var x = n;
|
|
while (x > 0) : (x /= 10) {
|
|
count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Bracket Matching / Multi-line
|
|
// ========================================================================
|
|
|
|
const BracketDepth = struct {
|
|
parens: i32 = 0, // ()
|
|
brackets: i32 = 0, // []
|
|
braces: i32 = 0, // {}
|
|
template: i32 = 0, // ``
|
|
in_string: bool = false,
|
|
string_char: u8 = 0,
|
|
};
|
|
|
|
fn updateBracketDepth(self: *Self, line: []const u8) void {
|
|
var i: usize = 0;
|
|
while (i < line.len) : (i += 1) {
|
|
const c = line[i];
|
|
|
|
// Handle string state
|
|
if (self.bracket_depth.in_string) {
|
|
if (c == '\\' and i + 1 < line.len) {
|
|
i += 1; // Skip escaped character
|
|
continue;
|
|
}
|
|
if (c == self.bracket_depth.string_char) {
|
|
self.bracket_depth.in_string = false;
|
|
// Decrement template depth when closing a template literal
|
|
if (c == '`') {
|
|
self.bracket_depth.template = @max(0, self.bracket_depth.template - 1);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
switch (c) {
|
|
'"', '\'', '`' => {
|
|
self.bracket_depth.in_string = true;
|
|
self.bracket_depth.string_char = c;
|
|
if (c == '`') self.bracket_depth.template += 1;
|
|
},
|
|
'(' => self.bracket_depth.parens += 1,
|
|
')' => self.bracket_depth.parens = @max(0, self.bracket_depth.parens - 1),
|
|
'[' => self.bracket_depth.brackets += 1,
|
|
']' => self.bracket_depth.brackets = @max(0, self.bracket_depth.brackets - 1),
|
|
'{' => self.bracket_depth.braces += 1,
|
|
'}' => self.bracket_depth.braces = @max(0, self.bracket_depth.braces - 1),
|
|
'/' => {
|
|
// Check for comments
|
|
if (i + 1 < line.len) {
|
|
if (line[i + 1] == '/') {
|
|
// Line comment - rest of line is comment
|
|
break;
|
|
} else if (line[i + 1] == '*') {
|
|
// Block comment start - for now skip to end
|
|
// TODO: proper block comment tracking
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn needsContinuation(self: *Self) bool {
|
|
if (self.bracket_depth.in_string) return true;
|
|
if (self.bracket_depth.parens > 0) return true;
|
|
if (self.bracket_depth.brackets > 0) return true;
|
|
if (self.bracket_depth.braces > 0) return true;
|
|
if (self.bracket_depth.template > 0) return true;
|
|
|
|
// Check for trailing operators that expect continuation
|
|
const line = strings.trim(self.line_buffer.items, " \t");
|
|
if (line.len == 0) return false;
|
|
|
|
const last_char = line[line.len - 1];
|
|
return switch (last_char) {
|
|
',', '+', '-', '*', '/', '%', '&', '|', '^', '!', '=', '<', '>', '?', ':' => true,
|
|
'\\' => true, // Line continuation
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
// ========================================================================
|
|
// History
|
|
// ========================================================================
|
|
|
|
fn historyPrev(self: *Self) void {
|
|
if (self.history.prev()) |entry| {
|
|
self.line_buffer.clearRetainingCapacity();
|
|
self.line_buffer.appendSlice(self.allocator, entry) catch return;
|
|
self.cursor_pos = self.line_buffer.items.len;
|
|
self.refreshLine();
|
|
}
|
|
}
|
|
|
|
fn historyNext(self: *Self) void {
|
|
if (self.history.next()) |entry| {
|
|
self.line_buffer.clearRetainingCapacity();
|
|
self.line_buffer.appendSlice(self.allocator, entry) catch return;
|
|
self.cursor_pos = self.line_buffer.items.len;
|
|
self.refreshLine();
|
|
} else {
|
|
self.line_buffer.clearRetainingCapacity();
|
|
self.cursor_pos = 0;
|
|
self.refreshLine();
|
|
}
|
|
}
|
|
|
|
fn reverseSearch(_: *Self) !void {
|
|
// TODO: Implement incremental reverse search (Ctrl-R)
|
|
Output.pretty("\n<d>reverse-i-search is not yet implemented<r>\n", .{});
|
|
Output.flush();
|
|
}
|
|
|
|
// ========================================================================
|
|
// Autocomplete
|
|
// ========================================================================
|
|
|
|
fn handleAutocomplete(self: *Self) !void {
|
|
const line = self.line_buffer.items[0..self.cursor_pos];
|
|
if (line.len == 0) return;
|
|
|
|
// Find the word being completed
|
|
var word_start: usize = line.len;
|
|
while (word_start > 0) {
|
|
const c = line[word_start - 1];
|
|
if (!isIdentifierChar(c) and c != '.') break;
|
|
word_start -= 1;
|
|
}
|
|
|
|
const word = line[word_start..];
|
|
if (word.len == 0) return;
|
|
|
|
// Get completions
|
|
const result = try self.getCompletions(word);
|
|
defer self.freeCompletions(result);
|
|
|
|
const completions = result.items;
|
|
if (completions.len == 0) return;
|
|
|
|
// For property completions (e.g., "Bun.ver"), completions contain just the property
|
|
// name (e.g., "version"), not the full path. Compute the prefix length to match.
|
|
const prefix_len = if (strings.lastIndexOfChar(word, '.')) |dot_pos|
|
|
word.len - dot_pos - 1 // Length of part after the dot
|
|
else
|
|
word.len; // Global completion - use full word length
|
|
|
|
if (completions.len == 1) {
|
|
// Single completion - insert it
|
|
const completion = completions[0];
|
|
// Safe bounds check for suffix
|
|
const suffix = if (prefix_len < completion.len)
|
|
completion[prefix_len..]
|
|
else
|
|
""; // Completion is shorter or equal to prefix
|
|
if (suffix.len > 0) {
|
|
try self.line_buffer.appendSlice(self.allocator, suffix);
|
|
self.cursor_pos += suffix.len;
|
|
self.refreshLine();
|
|
}
|
|
} else {
|
|
// Multiple completions - show them
|
|
Output.print("\n", .{});
|
|
|
|
// Find common prefix among completions
|
|
var common_len = completions[0].len;
|
|
for (completions[1..]) |c| {
|
|
var i: usize = 0;
|
|
while (i < common_len and i < c.len and completions[0][i] == c[i]) : (i += 1) {}
|
|
common_len = i;
|
|
}
|
|
|
|
// Insert common prefix if longer than current typed prefix
|
|
if (common_len > prefix_len) {
|
|
const prefix = completions[0][prefix_len..common_len];
|
|
try self.line_buffer.appendSlice(self.allocator, prefix);
|
|
self.cursor_pos += prefix.len;
|
|
}
|
|
|
|
// Display completions in columns
|
|
const max_width = blk: {
|
|
var max: usize = 0;
|
|
for (completions) |c| {
|
|
if (c.len > max) max = c.len;
|
|
}
|
|
break :blk max + 2;
|
|
};
|
|
|
|
const cols = @max(1, self.terminal_width / @as(u16, @intCast(max_width)));
|
|
var col: usize = 0;
|
|
|
|
for (completions) |c| {
|
|
Output.pretty("<cyan>{s}<r>", .{c});
|
|
col += 1;
|
|
if (col >= cols) {
|
|
Output.print("\n", .{});
|
|
col = 0;
|
|
} else {
|
|
// Pad to column width
|
|
var padding = max_width - c.len;
|
|
while (padding > 0) : (padding -= 1) {
|
|
Output.print(" ", .{});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (col > 0) Output.print("\n", .{});
|
|
|
|
self.printPrompt();
|
|
self.refreshLine();
|
|
}
|
|
}
|
|
|
|
/// Result of getCompletions - tracks whether strings need freeing
|
|
const CompletionsResult = struct {
|
|
items: []const []const u8,
|
|
/// If true, caller must free each string in items
|
|
owns_strings: bool,
|
|
};
|
|
|
|
fn getCompletions(self: *Self, word: []const u8) !CompletionsResult {
|
|
var completions: ArrayList([]const u8) = .{};
|
|
|
|
// Check if this is a property access
|
|
if (strings.lastIndexOfChar(word, '.')) |dot_pos| {
|
|
// Property completion - get object properties from JSC
|
|
const obj_name = word[0..dot_pos];
|
|
const prop_prefix = word[dot_pos + 1 ..];
|
|
|
|
// Try to get the object by evaluating the path
|
|
const obj_value = self.getObjectForPath(obj_name) orelse return .{
|
|
.items = try completions.toOwnedSlice(self.allocator),
|
|
.owns_strings = true,
|
|
};
|
|
|
|
// Get property names from JSC
|
|
if (obj_value.isObject()) {
|
|
if (obj_value.getObject()) |js_obj| {
|
|
var prop_iter = jsc.JSPropertyIterator(.{
|
|
.skip_empty_name = true,
|
|
.include_value = false,
|
|
.own_properties_only = false, // Include prototype properties
|
|
}).init(self.global, js_obj) catch return .{
|
|
.items = try completions.toOwnedSlice(self.allocator),
|
|
.owns_strings = true,
|
|
};
|
|
defer prop_iter.deinit();
|
|
|
|
while (prop_iter.next() catch null) |name| {
|
|
const name_str = name.toOwnedSlice(self.allocator) catch continue;
|
|
// Filter by prefix
|
|
if (prop_prefix.len == 0 or strings.startsWith(name_str, prop_prefix)) {
|
|
completions.append(self.allocator, name_str) catch |err| {
|
|
self.allocator.free(name_str);
|
|
// Free all previously added strings on error
|
|
for (completions.items) |s| {
|
|
self.allocator.free(s);
|
|
}
|
|
completions.deinit(self.allocator);
|
|
return err;
|
|
};
|
|
} else {
|
|
self.allocator.free(name_str);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return .{
|
|
.items = try completions.toOwnedSlice(self.allocator),
|
|
.owns_strings = true,
|
|
};
|
|
} else {
|
|
// Global completion
|
|
// Add JavaScript globals
|
|
const js_globals = [_][]const u8{
|
|
"Array", "ArrayBuffer", "BigInt", "BigInt64Array",
|
|
"BigUint64Array", "Boolean", "DataView", "Date",
|
|
"Error", "EvalError", "Float32Array", "Float64Array",
|
|
"Function", "Infinity", "Int16Array", "Int32Array",
|
|
"Int8Array", "JSON", "Map", "Math",
|
|
"NaN", "Number", "Object", "Promise",
|
|
"Proxy", "RangeError", "ReferenceError", "Reflect",
|
|
"RegExp", "Set", "SharedArrayBuffer", "String",
|
|
"Symbol", "SyntaxError", "TypeError", "URIError",
|
|
"Uint16Array", "Uint32Array", "Uint8Array", "Uint8ClampedArray",
|
|
"WeakMap", "WeakSet", "WeakRef", "FinalizationRegistry",
|
|
"console", "undefined", "null", "true",
|
|
"false", "globalThis", "eval", "isFinite",
|
|
"isNaN", "parseFloat", "parseInt", "decodeURI",
|
|
"decodeURIComponent", "encodeURI", "encodeURIComponent",
|
|
};
|
|
|
|
// Bun globals
|
|
const bun_globals = [_][]const u8{
|
|
"Bun", "fetch", "Request", "Response",
|
|
"Headers", "FormData", "URL", "URLSearchParams",
|
|
"Blob", "File", "FileReader", "WebSocket",
|
|
"Worker", "crypto", "performance", "navigator",
|
|
"location", "atob", "btoa", "setTimeout",
|
|
"setInterval", "clearTimeout", "clearInterval", "setImmediate",
|
|
"clearImmediate", "queueMicrotask", "structuredClone", "TextEncoder",
|
|
"TextDecoder", "AbortController", "AbortSignal", "Event",
|
|
"EventTarget", "CustomEvent", "MessageChannel", "MessagePort",
|
|
"BroadcastChannel", "ReadableStream", "WritableStream", "TransformStream",
|
|
"ByteLengthQueuingStrategy", "CountQueuingStrategy", "CompressionStream", "DecompressionStream",
|
|
};
|
|
|
|
// Keywords
|
|
const keywords = [_][]const u8{
|
|
"async", "await", "break", "case",
|
|
"catch", "class", "const", "continue",
|
|
"debugger", "default", "delete", "do",
|
|
"else", "export", "extends", "finally",
|
|
"for", "function", "if", "import",
|
|
"in", "instanceof", "let", "new",
|
|
"return", "static", "super", "switch",
|
|
"this", "throw", "try", "typeof",
|
|
"var", "void", "while", "with",
|
|
"yield",
|
|
};
|
|
|
|
// Add matching globals
|
|
inline for (js_globals ++ bun_globals ++ keywords) |name| {
|
|
if (strings.startsWith(name, word)) {
|
|
try completions.append(self.allocator, name);
|
|
}
|
|
}
|
|
}
|
|
|
|
return .{
|
|
.items = try completions.toOwnedSlice(self.allocator),
|
|
.owns_strings = false, // Static strings don't need freeing
|
|
};
|
|
}
|
|
|
|
fn freeCompletions(self: *Self, result: CompletionsResult) void {
|
|
if (result.owns_strings) {
|
|
for (result.items) |s| {
|
|
self.allocator.free(s);
|
|
}
|
|
}
|
|
self.allocator.free(result.items);
|
|
}
|
|
|
|
fn isIdentifierChar(c: u8) bool {
|
|
return (c >= 'a' and c <= 'z') or
|
|
(c >= 'A' and c <= 'Z') or
|
|
(c >= '0' and c <= '9') or
|
|
c == '_' or c == '$';
|
|
}
|
|
|
|
/// Resolve an object path like "Bun.file" or "console" to a JSValue
|
|
fn getObjectForPath(self: *Self, path: []const u8) ?JSValue {
|
|
if (path.len == 0) return null;
|
|
|
|
// Split path by dots and navigate
|
|
var current = self.global.toJSValue();
|
|
var it = std.mem.splitScalar(u8, path, '.');
|
|
|
|
while (it.next()) |segment| {
|
|
if (segment.len == 0) continue;
|
|
|
|
// Get property from current object
|
|
const prop = current.get(self.global, segment) catch return null;
|
|
current = prop orelse return null;
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Execution
|
|
// ========================================================================
|
|
|
|
pub fn eval(self: *Self, code: []const u8) !void {
|
|
// Add to history
|
|
self.history.add(code);
|
|
|
|
// Check for REPL commands
|
|
if (code.len > 0 and code[0] == '.') {
|
|
try self.handleReplCommand(code);
|
|
return;
|
|
}
|
|
|
|
// Check for shell command
|
|
if (strings.startsWith(code, "$`") or strings.startsWith(code, "$ `")) {
|
|
try self.runShellCommand(code);
|
|
return;
|
|
}
|
|
|
|
const start_time = std.time.nanoTimestamp();
|
|
|
|
// Transform code using the transpiler with replMode
|
|
const transformed = try self.transformCode(code);
|
|
defer self.allocator.free(transformed);
|
|
|
|
if (transformed.len == 0) {
|
|
return;
|
|
}
|
|
|
|
// Execute the transformed code
|
|
const wrapper_result = try self.executeCode(transformed);
|
|
|
|
// The REPL transforms wrap the last expression in { value: expr }
|
|
// For async code (top-level await), this is a Promise<{ value: result }>
|
|
// We need to handle both sync and async cases
|
|
|
|
// Check if result is a Promise using asPromise()
|
|
if (wrapper_result.asPromise()) |_| {
|
|
// Await the promise by running the event loop until it resolves
|
|
self.awaitAndPrintResult(wrapper_result, start_time);
|
|
} else {
|
|
const end_time = std.time.nanoTimestamp();
|
|
const elapsed_ms = @as(f64, @floatFromInt(end_time - start_time)) / 1_000_000.0;
|
|
|
|
// Extract the actual result value from the wrapper object
|
|
const result = if (wrapper_result.isObject()) blk: {
|
|
const val = wrapper_result.get(self.global, "value") catch break :blk wrapper_result;
|
|
break :blk val orelse wrapper_result;
|
|
} else wrapper_result;
|
|
|
|
// Print result (skip undefined for cleaner output)
|
|
if (!result.isUndefined()) {
|
|
self.printResult(result);
|
|
}
|
|
|
|
// Print timing if enabled
|
|
if (self.show_timing) {
|
|
Output.pretty("<d>({d:.2}ms)<r>\n", .{elapsed_ms});
|
|
}
|
|
}
|
|
|
|
self.line_number += 1;
|
|
}
|
|
|
|
fn transformCode(self: *Self, code: []const u8) ![]const u8 {
|
|
// Use Bun's transpiler with replMode for full AST-based transformation:
|
|
// - Hoists let/const/var/function/class declarations as var for persistence
|
|
// - Wraps code in IIFE to create proper scope
|
|
// - Wraps top-level await in async IIFE
|
|
// - Captures last expression result in { value: expr } object
|
|
|
|
const vm = self.vm;
|
|
const transpiler = &vm.transpiler;
|
|
|
|
// Enable REPL mode for this parse
|
|
const prev_repl_mode = transpiler.options.repl_mode;
|
|
const prev_dce = transpiler.options.dead_code_elimination;
|
|
transpiler.options.repl_mode = true;
|
|
transpiler.options.dead_code_elimination = false; // DCE would remove pure expressions like `42`
|
|
defer {
|
|
transpiler.options.repl_mode = prev_repl_mode;
|
|
transpiler.options.dead_code_elimination = prev_dce;
|
|
}
|
|
|
|
// Pre-process: wrap potential object literals in parentheses
|
|
// If code starts with { and doesn't end with ; it might be an object literal
|
|
const processed_code = if (isLikelyObjectLiteral(code))
|
|
std.fmt.allocPrint(self.allocator, "({s})", .{code}) catch code
|
|
else
|
|
code;
|
|
defer if (processed_code.ptr != code.ptr) self.allocator.free(processed_code);
|
|
|
|
// Create source for parsing
|
|
const source = logger.Source.initPathString("<repl>", processed_code);
|
|
|
|
// Parse options
|
|
const parse_opts = Transpiler.ParseOptions{
|
|
.allocator = self.allocator,
|
|
.dirname_fd = .invalid,
|
|
.path = source.path,
|
|
.loader = .js,
|
|
.jsx = transpiler.options.jsx,
|
|
.macro_remappings = .{},
|
|
.virtual_source = &source,
|
|
};
|
|
|
|
// Parse the code with REPL transforms
|
|
const parse_result = transpiler.parse(parse_opts, null) orelse {
|
|
// Check for parse errors
|
|
if (transpiler.log.errors > 0) {
|
|
// Print errors with source location context if available
|
|
for (transpiler.log.msgs.items) |msg| {
|
|
if (msg.kind == .err) {
|
|
if (msg.data.location) |loc| {
|
|
if (loc.line > 0) {
|
|
// Include line and column information
|
|
Output.pretty("<red>Parse error<r> <d>[{d}:{d}]<r><red>: {s}<r>\n", .{
|
|
loc.line,
|
|
loc.column + 1, // Convert 0-based to 1-based
|
|
msg.data.text,
|
|
});
|
|
} else {
|
|
Output.pretty("<red>Parse error: {s}<r>\n", .{msg.data.text});
|
|
}
|
|
} else {
|
|
Output.pretty("<red>Parse error: {s}<r>\n", .{msg.data.text});
|
|
}
|
|
}
|
|
}
|
|
transpiler.log.msgs.clearRetainingCapacity();
|
|
}
|
|
return error.ParseError;
|
|
};
|
|
|
|
// Print the transformed AST back to code
|
|
var buffer_writer = js_printer.BufferWriter.init(self.allocator);
|
|
var printer = js_printer.BufferPrinter.init(buffer_writer);
|
|
|
|
_ = transpiler.print(parse_result, @TypeOf(&printer), &printer, .esm_ascii) catch |err| {
|
|
Output.pretty("<red>Print error: {s}<r>\n", .{@errorName(err)});
|
|
return error.PrintError;
|
|
};
|
|
|
|
buffer_writer = printer.ctx;
|
|
const result = buffer_writer.written;
|
|
|
|
// Copy to owned slice since buffer_writer may be reused
|
|
return try self.allocator.dupe(u8, result);
|
|
}
|
|
|
|
/// Check if code looks like an object literal (starts with { and doesn't end with ;)
|
|
fn isLikelyObjectLiteral(code: []const u8) bool {
|
|
// Skip leading whitespace
|
|
var start: usize = 0;
|
|
while (start < code.len and (code[start] == ' ' or code[start] == '\t' or code[start] == '\n' or code[start] == '\r')) {
|
|
start += 1;
|
|
}
|
|
|
|
// Check if starts with {
|
|
if (start >= code.len or code[start] != '{') {
|
|
return false;
|
|
}
|
|
|
|
// Skip trailing whitespace
|
|
var end: usize = code.len;
|
|
while (end > 0 and (code[end - 1] == ' ' or code[end - 1] == '\t' or code[end - 1] == '\n' or code[end - 1] == '\r')) {
|
|
end -= 1;
|
|
}
|
|
|
|
// Check if ends with semicolon - if so, it's likely a block statement
|
|
if (end > 0 and code[end - 1] == ';') {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
fn executeCode(self: *Self, code: []const u8) bun.JSError!JSValue {
|
|
// Get the global eval function
|
|
const eval_fn = self.global.toJSValue().get(self.global, "eval") catch |err| {
|
|
return err;
|
|
} orelse return .js_undefined;
|
|
|
|
// Convert code string to JS string
|
|
const code_js = ZigString.init(code).toJS(self.global);
|
|
|
|
// Call eval with the code
|
|
const result = eval_fn.call(self.global, self.global.toJSValue(), &[_]JSValue{code_js}) catch |err| {
|
|
// Handle JavaScript exception
|
|
if (self.global.hasException()) {
|
|
const exception = self.global.tryTakeException() orelse return err;
|
|
self.printException(exception);
|
|
return .js_undefined;
|
|
}
|
|
return err;
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
fn printException(self: *Self, exception: JSValue) void {
|
|
_ = self;
|
|
// Print the exception with colors
|
|
const vm = VirtualMachine.get();
|
|
vm.printErrorLikeObjectToConsole(exception.toError() orelse exception);
|
|
}
|
|
|
|
fn printResult(self: *Self, value: JSValue) void {
|
|
// Pretty-print the result using JSValue's print method
|
|
value.print(self.global, .Log, .Log);
|
|
}
|
|
|
|
fn awaitAndPrintResult(self: *Self, promise_value: JSValue, start_time: i128) void {
|
|
// Get the Promise object
|
|
const promise = promise_value.asPromise() orelse {
|
|
// Not a JSPromise, might be a thenable - just print the raw value
|
|
self.printResult(promise_value);
|
|
return;
|
|
};
|
|
|
|
// Wait for the promise using the VM's event loop handling
|
|
// This properly handles edge cases like execution being forbidden
|
|
self.vm.waitForPromise(.{ .normal = promise });
|
|
|
|
const end_time = std.time.nanoTimestamp();
|
|
const elapsed_ms = @as(f64, @floatFromInt(end_time - start_time)) / 1_000_000.0;
|
|
|
|
// Get the result based on promise status
|
|
const resolved_value = promise.result(self.vm.jsc_vm);
|
|
|
|
if (promise.status() == .rejected) {
|
|
// Print rejection reason as error
|
|
self.printException(resolved_value);
|
|
} else {
|
|
// Extract the actual result value from the wrapper object { value: result }
|
|
const result = if (resolved_value.isObject()) blk: {
|
|
const val = resolved_value.get(self.global, "value") catch break :blk resolved_value;
|
|
break :blk val orelse resolved_value;
|
|
} else resolved_value;
|
|
|
|
// Print result (skip undefined for cleaner output)
|
|
if (!result.isUndefined()) {
|
|
self.printResult(result);
|
|
}
|
|
}
|
|
|
|
// Print timing if enabled
|
|
if (self.show_timing) {
|
|
Output.pretty("<d>({d:.2}ms)<r>\n", .{elapsed_ms});
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// REPL Commands
|
|
// ========================================================================
|
|
|
|
fn handleReplCommand(self: *Self, cmd: []const u8) !void {
|
|
const trimmed = strings.trim(cmd, " \t");
|
|
|
|
if (strings.eqlComptime(trimmed, ".help") or strings.eqlComptime(trimmed, ".h")) {
|
|
self.printHelp();
|
|
} else if (strings.eqlComptime(trimmed, ".exit") or strings.eqlComptime(trimmed, ".q")) {
|
|
Global.exit(0);
|
|
} else if (strings.eqlComptime(trimmed, ".clear")) {
|
|
self.clearScreen();
|
|
self.line_number = 1;
|
|
} else if (strings.startsWith(trimmed, ".load ")) {
|
|
const path = strings.trim(trimmed[6..], " \t");
|
|
self.loadFile(path);
|
|
} else if (strings.startsWith(trimmed, ".save ")) {
|
|
const path = strings.trim(trimmed[6..], " \t");
|
|
try self.saveHistory(path);
|
|
} else if (strings.eqlComptime(trimmed, ".editor")) {
|
|
try self.enterEditorMode();
|
|
} else if (strings.eqlComptime(trimmed, ".timing")) {
|
|
self.show_timing = !self.show_timing;
|
|
Output.pretty("<d>Timing {s}<r>\n", .{if (self.show_timing) "enabled" else "disabled"});
|
|
} else if (strings.startsWith(trimmed, ".install ")) {
|
|
const pkg = strings.trim(trimmed[9..], " \t");
|
|
try self.installPackage(pkg);
|
|
} else if (strings.startsWith(trimmed, ".i ")) {
|
|
const pkg = strings.trim(trimmed[3..], " \t");
|
|
try self.installPackage(pkg);
|
|
} else {
|
|
Output.pretty("<red>Unknown REPL command: {s}<r>\n", .{trimmed});
|
|
Output.pretty("<d>Type .help for available commands<r>\n", .{});
|
|
}
|
|
}
|
|
|
|
fn printHelp(self: *Self) void {
|
|
_ = self;
|
|
Output.pretty(
|
|
\\<b><magenta>Bun REPL Commands<r>
|
|
\\
|
|
\\ <cyan>.help<r>, <cyan>.h<r> Show this help message
|
|
\\ <cyan>.exit<r>, <cyan>.q<r> Exit the REPL
|
|
\\ <cyan>.clear<r> Clear the screen and reset line number
|
|
\\ <cyan>.editor<r> Enter multi-line editor mode (Ctrl-D to finish)
|
|
\\ <cyan>.load<r> FILE Load and execute a JavaScript/TypeScript file
|
|
\\ <cyan>.save<r> FILE Save REPL history to a file
|
|
\\ <cyan>.timing<r> Toggle execution timing display
|
|
\\ <cyan>.install<r> PKG Install a package from npm (alias: .i)
|
|
\\
|
|
\\<b>Keyboard Shortcuts<r>
|
|
\\
|
|
\\ <cyan>Tab<r> Autocomplete
|
|
\\ <cyan>Ctrl-C<r> Cancel current input / Exit on empty line
|
|
\\ <cyan>Ctrl-D<r> Exit (on empty line) / Delete character
|
|
\\ <cyan>Ctrl-L<r> Clear screen
|
|
\\ <cyan>Ctrl-R<r> Reverse history search
|
|
\\ <cyan>Ctrl-A<r> Move to beginning of line
|
|
\\ <cyan>Ctrl-E<r> Move to end of line
|
|
\\ <cyan>Ctrl-K<r> Kill to end of line
|
|
\\ <cyan>Ctrl-U<r> Kill entire line
|
|
\\ <cyan>Ctrl-W<r> Kill previous word
|
|
\\ <cyan>Up/Down<r> Navigate history
|
|
\\
|
|
\\<b>Special Features<r>
|
|
\\
|
|
\\ Top-level await is supported
|
|
\\ TypeScript/JSX is automatically transpiled
|
|
\\ Shell commands: $\`command\` or await $\`command\`
|
|
\\ Variables persist across REPL lines
|
|
\\
|
|
, .{});
|
|
}
|
|
|
|
fn loadFile(self: *Self, path: []const u8) void {
|
|
// Resolve to absolute path first
|
|
const abs_path = std.fs.cwd().realpathAlloc(self.allocator, path) catch |err| {
|
|
Output.pretty("<red>Error resolving path '{s}': {s}<r>\n", .{ path, @errorName(err) });
|
|
return;
|
|
};
|
|
defer self.allocator.free(abs_path);
|
|
|
|
const file = std.fs.openFileAbsolute(abs_path, .{}) catch |err| {
|
|
Output.pretty("<red>Error loading file '{s}': {s}<r>\n", .{ abs_path, @errorName(err) });
|
|
return;
|
|
};
|
|
defer file.close();
|
|
|
|
const content = file.readToEndAlloc(self.allocator, 10 * 1024 * 1024) catch |err| {
|
|
Output.pretty("<red>Error reading file: {s}<r>\n", .{@errorName(err)});
|
|
return;
|
|
};
|
|
defer self.allocator.free(content);
|
|
|
|
Output.pretty("<d>Loading {s}...<r>\n", .{abs_path});
|
|
// Execute the file content
|
|
self.evalDirect(content);
|
|
}
|
|
|
|
fn evalDirect(self: *Self, code: []const u8) void {
|
|
// Add to history
|
|
self.history.add(code);
|
|
|
|
const start_time = std.time.nanoTimestamp();
|
|
|
|
// Transform code using the transpiler with replMode
|
|
const transformed = self.transformCode(code) catch |err| {
|
|
Output.pretty("<red>Transform error: {s}<r>\n", .{@errorName(err)});
|
|
return;
|
|
};
|
|
defer self.allocator.free(transformed);
|
|
|
|
if (transformed.len == 0) {
|
|
return;
|
|
}
|
|
|
|
// Execute the transformed code
|
|
const wrapper_result = self.executeCode(transformed) catch |err| {
|
|
Output.pretty("<red>Execution error: {s}<r>\n", .{@errorName(err)});
|
|
return;
|
|
};
|
|
|
|
// The REPL transforms wrap the last expression in { value: expr }
|
|
// For async code (top-level await), this is a Promise<{ value: result }>
|
|
// We need to handle both sync and async cases
|
|
|
|
// Check if result is a Promise using asPromise()
|
|
if (wrapper_result.asPromise()) |_| {
|
|
// Await the promise by running the event loop until it resolves
|
|
self.awaitAndPrintResult(wrapper_result, start_time);
|
|
} else {
|
|
const end_time = std.time.nanoTimestamp();
|
|
const elapsed_ms = @as(f64, @floatFromInt(end_time - start_time)) / 1_000_000.0;
|
|
|
|
// Extract the actual result value from the wrapper object
|
|
const result = if (wrapper_result.isObject()) blk: {
|
|
const val = wrapper_result.get(self.global, "value") catch break :blk wrapper_result;
|
|
break :blk val orelse wrapper_result;
|
|
} else wrapper_result;
|
|
|
|
// Print result
|
|
if (!result.isUndefined()) {
|
|
self.printResult(result);
|
|
}
|
|
|
|
// Print timing if enabled
|
|
if (self.show_timing) {
|
|
Output.pretty("<d>({d:.2}ms)<r>\n", .{elapsed_ms});
|
|
}
|
|
}
|
|
|
|
self.line_number += 1;
|
|
}
|
|
|
|
fn saveHistory(self: *Self, path: []const u8) !void {
|
|
// Resolve to absolute path if relative
|
|
const abs_path = if (std.fs.path.isAbsolute(path))
|
|
path
|
|
else blk: {
|
|
break :blk std.fs.cwd().realpathAlloc(self.allocator, path) catch |err| {
|
|
// If the file doesn't exist yet, construct an absolute path
|
|
const cwd = std.fs.cwd().realpathAlloc(self.allocator, ".") catch {
|
|
Output.pretty("<red>Error resolving current directory<r>\n", .{});
|
|
return err;
|
|
};
|
|
defer self.allocator.free(cwd);
|
|
// Use bun.path.join for cross-platform path construction
|
|
const parts = [_][]const u8{ cwd, path };
|
|
const joined = bun.path.join(&parts, .auto);
|
|
break :blk self.allocator.dupe(u8, joined) catch {
|
|
Output.pretty("<red>Error constructing path<r>\n", .{});
|
|
return err;
|
|
};
|
|
};
|
|
};
|
|
defer if (!std.fs.path.isAbsolute(path)) self.allocator.free(abs_path);
|
|
|
|
try self.history.saveToFile(abs_path);
|
|
Output.pretty("<d>History saved to {s}<r>\n", .{abs_path});
|
|
}
|
|
|
|
fn enterEditorMode(self: *Self) !void {
|
|
Output.pretty("<d>// Entering editor mode. Press Ctrl-D to execute, Ctrl-C to cancel.<r>\n", .{});
|
|
|
|
var editor_buffer: ArrayList(u8) = .{};
|
|
defer editor_buffer.deinit(self.allocator);
|
|
|
|
// Use cross-platform stdin file descriptor
|
|
const stdin_fd = bun.FileDescriptor.stdin();
|
|
var line_buf: [4096]u8 = undefined;
|
|
var line_pos: usize = 0;
|
|
|
|
while (true) {
|
|
Output.print(" ", .{});
|
|
Output.flush();
|
|
|
|
// Read line manually using cross-platform bun.sys.read
|
|
line_pos = 0;
|
|
var saw_eof = false;
|
|
while (true) {
|
|
var char_buf: [1]u8 = undefined;
|
|
const read_result = bun.sys.read(stdin_fd, &char_buf);
|
|
const n = switch (read_result) {
|
|
.result => |bytes| bytes,
|
|
.err => |e| {
|
|
if (e.getErrno() == .AGAIN) continue;
|
|
break; // Error reading, treat as EOF
|
|
},
|
|
};
|
|
if (n == 0) {
|
|
saw_eof = true;
|
|
break; // EOF
|
|
}
|
|
|
|
if (char_buf[0] == '\n') break;
|
|
if (line_pos < line_buf.len) {
|
|
line_buf[line_pos] = char_buf[0];
|
|
line_pos += 1;
|
|
}
|
|
}
|
|
|
|
if (saw_eof and line_pos == 0) break; // EOF with no data on this line
|
|
|
|
try editor_buffer.appendSlice(self.allocator, line_buf[0..line_pos]);
|
|
try editor_buffer.append(self.allocator, '\n');
|
|
|
|
// If we saw EOF, stop reading more lines
|
|
if (saw_eof) break;
|
|
}
|
|
|
|
Output.print("\n", .{});
|
|
|
|
if (editor_buffer.items.len > 0) {
|
|
self.evalDirect(editor_buffer.items);
|
|
}
|
|
}
|
|
|
|
fn installPackage(self: *Self, pkg: []const u8) !void {
|
|
if (pkg.len == 0) {
|
|
Output.pretty("<red>Usage: .install [package-name]<r>\n", .{});
|
|
return;
|
|
}
|
|
|
|
Output.pretty("<d>Installing {s}...<r>\n", .{pkg});
|
|
|
|
// Use Bun's package manager to install
|
|
// TODO: Call InstallCommand or use the package manager API directly
|
|
_ = self;
|
|
Output.pretty("<yellow>Package installation not yet implemented<r>\n", .{});
|
|
}
|
|
|
|
fn runShellCommand(self: *Self, code: []const u8) !void {
|
|
// Transform $`command` into await Bun.$`command` and execute it
|
|
// The shell template tag returns a ShellPromise which gets awaited
|
|
|
|
const start_time = std.time.nanoTimestamp();
|
|
|
|
// Parse the shell command from the input
|
|
// Formats: $`command` or $ `command`
|
|
var cmd_start: usize = 0;
|
|
if (strings.startsWith(code, "$`")) {
|
|
cmd_start = 1; // Skip the $, keep the backtick
|
|
} else if (strings.startsWith(code, "$ `")) {
|
|
cmd_start = 2; // Skip "$ ", keep the backtick
|
|
} else {
|
|
Output.pretty("<red>Invalid shell command syntax. Use $`command`<r>\n", .{});
|
|
return;
|
|
}
|
|
|
|
// Build the JavaScript code to execute the shell command
|
|
// Using Bun.$ template literal with await for automatic promise handling
|
|
const shell_code = code[cmd_start..];
|
|
|
|
// Wrap in await Bun.$ - use the same transformation as normal code
|
|
const js_code = try std.fmt.allocPrint(self.allocator, "await Bun.${s}", .{shell_code});
|
|
defer self.allocator.free(js_code);
|
|
|
|
// Transform code using the transpiler with replMode (for top-level await support)
|
|
const transformed = self.transformCode(js_code) catch |err| {
|
|
Output.pretty("<red>Shell command transformation failed: {s}<r>\n", .{@errorName(err)});
|
|
return;
|
|
};
|
|
defer self.allocator.free(transformed);
|
|
|
|
if (transformed.len == 0) {
|
|
return;
|
|
}
|
|
|
|
// Execute the transformed code
|
|
const wrapper_result = self.executeCode(transformed) catch |err| {
|
|
Output.pretty("<red>Shell command failed: {s}<r>\n", .{@errorName(err)});
|
|
return;
|
|
};
|
|
|
|
// Handle async result (the shell command returns a promise)
|
|
if (wrapper_result.asPromise()) |_| {
|
|
self.awaitAndPrintResult(wrapper_result, start_time);
|
|
} else {
|
|
const end_time = std.time.nanoTimestamp();
|
|
const elapsed_ms = @as(f64, @floatFromInt(end_time - start_time)) / 1_000_000.0;
|
|
|
|
// Extract the actual result value from the wrapper object
|
|
const result = if (wrapper_result.isObject()) blk: {
|
|
const val = wrapper_result.get(self.global, "value") catch break :blk wrapper_result;
|
|
break :blk val orelse wrapper_result;
|
|
} else wrapper_result;
|
|
|
|
// Print result
|
|
if (!result.isUndefined()) {
|
|
self.printResult(result);
|
|
}
|
|
|
|
if (self.show_timing) {
|
|
Output.pretty("<d>({d:.2}ms)<r>\n", .{elapsed_ms});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// History Management
|
|
// ============================================================================
|
|
|
|
pub const History = struct {
|
|
entries: ArrayList([]const u8),
|
|
position: usize = 0,
|
|
allocator: Allocator,
|
|
history_path: ?[]const u8 = null,
|
|
|
|
const MAX_ENTRIES = 10000;
|
|
const HISTORY_FILE_NAME = ".bun_repl_history";
|
|
|
|
pub fn init(allocator: Allocator) !History {
|
|
var history = History{
|
|
.entries = .{},
|
|
.allocator = allocator,
|
|
};
|
|
|
|
// Determine history file path
|
|
history.history_path = history.getHistoryPath();
|
|
|
|
// Try to load history from file
|
|
history.loadFromFile() catch {};
|
|
|
|
return history;
|
|
}
|
|
|
|
pub fn deinit(self: *History) void {
|
|
// Save history before exit
|
|
if (self.history_path) |path| {
|
|
self.saveToFile(path) catch {};
|
|
}
|
|
|
|
for (self.entries.items) |entry| {
|
|
self.allocator.free(entry);
|
|
}
|
|
self.entries.deinit(self.allocator);
|
|
|
|
if (self.history_path) |path| {
|
|
self.allocator.free(path);
|
|
}
|
|
}
|
|
|
|
fn getHistoryPath(self: *History) ?[]const u8 {
|
|
// Try to get home directory from environment
|
|
const home = bun.getenvZ("HOME") orelse bun.getenvZ("USERPROFILE") orelse return null;
|
|
|
|
// Build path: $HOME/.bun_repl_history using Bun's path helper for cross-platform support
|
|
// bun.path.join returns a slice into a thread-local buffer, so we need to dupe it
|
|
const parts = [_][]const u8{ home, HISTORY_FILE_NAME };
|
|
const joined = bun.path.join(&parts, .auto);
|
|
const path = self.allocator.dupe(u8, joined) catch return null;
|
|
return path;
|
|
}
|
|
|
|
pub fn add(self: *History, entry: []const u8) void {
|
|
// Don't add empty entries or duplicates of the last entry
|
|
if (entry.len == 0) return;
|
|
if (self.entries.items.len > 0 and
|
|
std.mem.eql(u8, self.entries.items[self.entries.items.len - 1], entry))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Remove oldest entries if at capacity
|
|
while (self.entries.items.len >= MAX_ENTRIES) {
|
|
const old = self.entries.orderedRemove(0);
|
|
self.allocator.free(old);
|
|
}
|
|
|
|
const copy = self.allocator.dupe(u8, entry) catch return;
|
|
self.entries.append(self.allocator, copy) catch {
|
|
self.allocator.free(copy);
|
|
return;
|
|
};
|
|
|
|
self.position = self.entries.items.len;
|
|
}
|
|
|
|
pub fn prev(self: *History) ?[]const u8 {
|
|
if (self.entries.items.len == 0) return null;
|
|
if (self.position > 0) {
|
|
self.position -= 1;
|
|
}
|
|
return self.entries.items[self.position];
|
|
}
|
|
|
|
pub fn next(self: *History) ?[]const u8 {
|
|
// Guard against empty history to avoid underflow
|
|
if (self.entries.items.len == 0) {
|
|
self.position = 0;
|
|
return null;
|
|
}
|
|
if (self.position >= self.entries.items.len - 1) {
|
|
self.position = self.entries.items.len;
|
|
return null;
|
|
}
|
|
self.position += 1;
|
|
return self.entries.items[self.position];
|
|
}
|
|
|
|
fn loadFromFile(self: *History) !void {
|
|
const path = self.history_path orelse return;
|
|
|
|
// Use absolute path for reading from home directory
|
|
const file = std.fs.openFileAbsolute(path, .{}) catch return;
|
|
defer file.close();
|
|
|
|
const content = file.readToEndAlloc(self.allocator, 1024 * 1024) catch return;
|
|
defer self.allocator.free(content);
|
|
|
|
var it = std.mem.splitScalar(u8, content, '\n');
|
|
while (it.next()) |line| {
|
|
if (line.len > 0) {
|
|
const copy = try self.allocator.dupe(u8, line);
|
|
try self.entries.append(self.allocator, copy);
|
|
}
|
|
}
|
|
|
|
self.position = self.entries.items.len;
|
|
}
|
|
|
|
pub fn saveToFile(self: *History, path: []const u8) !void {
|
|
// Use absolute path for writing to home directory
|
|
const file = std.fs.createFileAbsolute(path, .{}) catch |err| {
|
|
// Warn about fallback and skip saving to avoid writing to unexpected location
|
|
Output.pretty("<d>Warning: could not save history to '{s}': {s}<r>\n", .{ path, @errorName(err) });
|
|
return err;
|
|
};
|
|
defer file.close();
|
|
|
|
for (self.entries.items) |entry| {
|
|
_ = try file.write(entry);
|
|
_ = try file.write("\n");
|
|
}
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Command Entry Point
|
|
// ============================================================================
|
|
|
|
pub fn exec(ctx: Command.Context) !void {
|
|
// Print welcome banner
|
|
printBanner();
|
|
|
|
// Initialize JSC
|
|
jsc.initialize(false);
|
|
|
|
js_ast.Expr.Data.Store.create();
|
|
js_ast.Stmt.Data.Store.create();
|
|
|
|
var arena = Arena.init();
|
|
errdefer arena.deinit();
|
|
|
|
// Initialize VirtualMachine
|
|
const vm = VirtualMachine.init(.{
|
|
.allocator = arena.allocator(),
|
|
.log = ctx.log,
|
|
.args = ctx.args,
|
|
.is_main_thread = true,
|
|
.smol = ctx.runtime_options.smol,
|
|
.debugger = ctx.runtime_options.debugger,
|
|
.eval = true, // REPL evaluates code
|
|
}) catch |err| {
|
|
js_ast.Expr.Data.Store.reset();
|
|
js_ast.Stmt.Data.Store.reset();
|
|
return err;
|
|
};
|
|
|
|
// Configure event loop and global
|
|
vm.regular_event_loop.global = vm.global;
|
|
vm.event_loop.ensureWaker();
|
|
|
|
// Configure transpiler options
|
|
const b = &vm.transpiler;
|
|
vm.preload = ctx.preloads;
|
|
vm.argv = ctx.passthrough;
|
|
vm.arena = &arena;
|
|
vm.allocator = arena.allocator();
|
|
b.options.install = ctx.install;
|
|
b.resolver.opts.install = ctx.install;
|
|
b.resolver.opts.global_cache = ctx.debug.global_cache;
|
|
b.resolver.opts.prefer_offline_install = (ctx.debug.offline_mode_setting orelse .online) == .offline;
|
|
b.resolver.opts.prefer_latest_install = (ctx.debug.offline_mode_setting orelse .online) == .latest;
|
|
b.options.global_cache = b.resolver.opts.global_cache;
|
|
b.options.prefer_offline_install = b.resolver.opts.prefer_offline_install;
|
|
b.options.prefer_latest_install = b.resolver.opts.prefer_latest_install;
|
|
b.resolver.env_loader = b.env;
|
|
b.options.env.behavior = .load_all_without_inlining;
|
|
|
|
// Configure defines for the transpiler
|
|
b.configureDefines() catch {
|
|
bun.bun_js.failWithBuildError(vm);
|
|
};
|
|
|
|
// Load environment
|
|
AsyncHTTP.loadEnv(vm.allocator, vm.log, b.env);
|
|
vm.loadExtraEnvAndSourceCodePrinter();
|
|
vm.is_main_thread = true;
|
|
jsc.VirtualMachine.is_main_thread_vm = true;
|
|
|
|
// Acquire JSC API lock - all JS operations must happen within the lock
|
|
const api_lock = vm.jsc_vm.getAPILock();
|
|
defer api_lock.release();
|
|
|
|
// Create REPL instance
|
|
const repl = try Repl.init(ctx.allocator, vm);
|
|
defer repl.deinit();
|
|
|
|
// Main REPL loop
|
|
while (true) {
|
|
const line = repl.readLine() catch |err| {
|
|
Output.pretty("<red>Error reading input: {s}<r>\n", .{@errorName(err)});
|
|
continue;
|
|
};
|
|
|
|
if (line) |input| {
|
|
defer ctx.allocator.free(input);
|
|
|
|
repl.eval(input) catch |err| {
|
|
Output.pretty("<red>Error: {s}<r>\n", .{@errorName(err)});
|
|
};
|
|
} else {
|
|
// EOF - exit
|
|
break;
|
|
}
|
|
}
|
|
|
|
Output.pretty("<d>Goodbye!<r>\n", .{});
|
|
}
|
|
|
|
fn printBanner() void {
|
|
Output.pretty(
|
|
\\<b><magenta>Bun<r> <b>v{s}<r> REPL
|
|
\\<d>Type .help for available commands<r>
|
|
\\
|
|
, .{Global.package_json_version});
|
|
Output.flush();
|
|
}
|
|
|
|
const fmt = @import("../fmt.zig");
|
|
const Command = @import("../cli.zig").Command;
|
|
|
|
const bun = @import("bun");
|
|
const Global = bun.Global;
|
|
const Output = bun.Output;
|
|
const js_ast = bun.ast;
|
|
const js_printer = bun.js_printer;
|
|
const logger = bun.logger;
|
|
const options = bun.options;
|
|
const strings = bun.strings;
|
|
const Arena = bun.allocators.MimallocArena;
|
|
const AsyncHTTP = bun.http.AsyncHTTP;
|
|
const Transpiler = bun.transpiler.Transpiler;
|
|
|
|
const jsc = bun.jsc;
|
|
const JSGlobalObject = jsc.JSGlobalObject;
|
|
const JSValue = jsc.JSValue;
|
|
const VirtualMachine = jsc.VirtualMachine;
|
|
const ZigString = jsc.ZigString;
|
|
|
|
const std = @import("std");
|
|
const ArrayList = std.ArrayListUnmanaged;
|
|
const Allocator = std.mem.Allocator;
|