From 261f429fd1e95aa4e90241e2c0975ec8e08611dc Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Tue, 20 Jan 2026 20:44:24 +0000 Subject: [PATCH] feat(repl): add JSC-based autocomplete for object properties The REPL now provides intelligent autocomplete for object properties by dynamically querying the JSC runtime. When typing `obj.` and pressing Tab, the REPL will show available properties from the actual object. Features: - Property completion for any object (e.g., `Bun.`, `console.`) - Navigates nested paths (e.g., `Bun.file.`) - Includes both own properties and prototype chain Also adds tests for class persistence and destructuring to verify the full AST transforms work correctly. Co-Authored-By: Claude Opus 4.5 --- src/cli/repl_command.zig | 49 ++++++++++++++++++++++++++++++++++++---- test/cli/repl.test.ts | 38 +++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/cli/repl_command.zig b/src/cli/repl_command.zig index dca7266538..8fd1e6068a 100644 --- a/src/cli/repl_command.zig +++ b/src/cli/repl_command.zig @@ -674,14 +674,34 @@ pub const Repl = struct { // Check if this is a property access if (strings.lastIndexOfChar(word, '.')) |dot_pos| { - // Property completion + // Property completion - get object properties from JSC const obj_name = word[0..dot_pos]; const prop_prefix = word[dot_pos + 1 ..]; - // Get object from global - _ = obj_name; - _ = prop_prefix; - // TODO: Get object properties from JSC + // Try to get the object by evaluating the path + const obj_value = self.getObjectForPath(obj_name) orelse return completions.toOwnedSlice(self.allocator); + + // 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 completions.toOwnedSlice(self.allocator); + 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)) { + try completions.append(self.allocator, name_str); + } else { + self.allocator.free(name_str); + } + } + } + } } else { // Global completion // Add JavaScript globals @@ -750,6 +770,25 @@ pub const Repl = struct { 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 // ======================================================================== diff --git a/test/cli/repl.test.ts b/test/cli/repl.test.ts index ead9114117..251e83d0ea 100644 --- a/test/cli/repl.test.ts +++ b/test/cli/repl.test.ts @@ -266,4 +266,42 @@ describe("bun repl", () => { expect(stdout).toContain("42"); expect(exitCode).toBe(0); }); + + test("class declarations persist", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "repl"], + env: bunEnv, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + + proc.stdin.write("class Calculator { add(a, b) { return a + b } }\n"); + proc.stdin.write("new Calculator().add(3, 7)\n"); + proc.stdin.end(); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toContain("10"); + expect(exitCode).toBe(0); + }); + + test("destructuring works", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "repl"], + env: bunEnv, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + + proc.stdin.write("const { a, b } = { a: 1, b: 2 }\n"); + proc.stdin.write("a + b\n"); + proc.stdin.end(); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toContain("3"); + expect(exitCode).toBe(0); + }); });