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 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-20 20:44:24 +00:00
parent 018358d8a1
commit 261f429fd1
2 changed files with 82 additions and 5 deletions

View File

@@ -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
// ========================================================================

View File

@@ -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);
});
});