diff options
-rw-r--r-- | completions/bun.fish | 49 | ||||
-rw-r--r-- | src/cli.zig | 58 | ||||
-rw-r--r-- | src/cli/run_command.zig | 112 | ||||
-rw-r--r-- | src/cli/shell_completions.zig | 43 |
4 files changed, 259 insertions, 3 deletions
diff --git a/completions/bun.fish b/completions/bun.fish new file mode 100644 index 000000000..d3484a78c --- /dev/null +++ b/completions/bun.fish @@ -0,0 +1,49 @@ +function __fish__get_bun_bins + string split ' ' (bun getcompletes b) +end + +function __fish__get_bun_scripts + string split ' ' (bun getcompletes s) +end + + +set -l bun_builtin_cmds dev create help bun upgrade discord run +set -l bun_builtin_cmds_without_run dev create help bun upgrade discord +set -l bun_builtin_cmds_without_create dev help bun upgrade discord run + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds_without_run; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts); and __fish_use_subcommand" -a '(__fish__get_bun_bins)' -d 'package bin' + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds_without_run; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts); and __fish_use_subcommand" -a '(__fish__get_bun_scripts)' -d 'script' + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds_without_run; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts); and __fish_seen_subcommand_from run" -a '(__fish__get_bun_bins)' -d 'package bin' + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds_without_run; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts); and __fish_seen_subcommand_from run" -a '(__fish__get_bun_scripts)' -d 'script' + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts) and __fish_use_subcommand" -a 'run' -d 'Run a script or bin' + +complete -c bun -n "not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts);" -F -s 'u' -l 'origin' -r -d 'Server URL. Rewrites import paths' + +complete -c bun -n "not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts);" -F -s 'p' -l 'port' -r -d 'Port number to start server from' + +complete -c bun -n "not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts);" -F -s 'd' -l 'define' -r -d 'Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:\"development\"' + +complete -c bun -n "not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts);" -F -s 'e' -l 'external' -r -d 'Exclude module from transpilation (can use * wildcards). ex: -e react' + +complete -c bun -n "not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts);" -F -l 'use' -r -d 'Use a framework (ex: next)' + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts) and __fish_use_subcommand" -a 'dev' -d 'Start dev server' + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts); and __fish_use_subcommand" -F -a 'create' -d 'Create a new project from a template' + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds_without_create next react; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts); and __fish_seen_subcommand_from create" -f -a 'next' -d 'Create a new Next.js project' + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds_without_create next react; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts); and __fish_seen_subcommand_from create" -f -a 'react' -d 'Create a new React project' + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds_without_create; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts); and __fish_seen_subcommand_from create next" -F + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts); and __fish_use_subcommand" -x -a 'upgrade' -d 'Upgrade Bun to the latest version' + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts); and __fish_use_subcommand" -x -a '--help' -d 'See all commands and flags' + +complete -c bun -n "not __fish_seen_subcommand_from $bun_builtin_cmds; and not __fish_seen_subcommand_from (__fish__get_bun_bins); and not __fish_seen_subcommand_from (__fish__get_bun_scripts); and __fish_use_subcommand" -x -a 'discord' -d 'Open Bun\'s Discord server' + diff --git a/src/cli.zig b/src/cli.zig index 86e9409fb..5274deafc 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -36,6 +36,7 @@ const CreateCommand = @import("./cli/create_command.zig").CreateCommand; const CreateListExamplesCommand = @import("./cli/create_command.zig").CreateListExamplesCommand; const RunCommand = @import("./cli/run_command.zig").RunCommand; const UpgradeCommand = @import("./cli/upgrade_command.zig").UpgradeCommand; +const ShellCompletions = @import("./cli/shell_completions.zig"); var start_time: i128 = undefined; pub const Cli = struct { @@ -145,8 +146,8 @@ pub const Arguments = struct { clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, clap.parseParam("-i, --inject <STR>... Inject module at the top of every file") catch unreachable, clap.parseParam("-l, --loader <STR>... Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: jsx, js, json, tsx, ts, css") catch unreachable, - clap.parseParam("--origin <STR> Rewrite import paths to start with --origin. Default: \"\"") catch unreachable, - clap.parseParam("--port <STR> Port to serve Bun's dev server on. Default: \"3000\"") catch unreachable, + clap.parseParam("-u, --origin <STR> Rewrite import URLs to start with --origin. Default: \"\"") catch unreachable, + clap.parseParam("-p, --port <STR> Port to serve Bun's dev server on. Default: \"3000\"") catch unreachable, clap.parseParam("--silent Don't repeat the command for bun run") catch unreachable, // clap.parseParam("-o, --outdir <STR> Save output to directory (default: \"out\" if none provided and multiple entry points passed)") catch unreachable, @@ -463,6 +464,7 @@ const HelpCommand = struct { \\> <r> <b><green>run <r><d> test <r> Run a package.json script or executable<r> \\> <r> <b><cyan>create <r><d>next ./app<r> Start a new project from a template <d>(shorthand: c)<r> \\> <r> <b><blue>upgrade <r> Get the latest version of Bun + \\> <r> <b><d>completions<r> Install shell completions for tab-completion \\> <r> <b><d>discord <r> Open Bun's Discord server \\> <r> <b><d>help <r> Print this help menu \\ @@ -576,7 +578,7 @@ pub const Command = struct { } const first_arg_name = std.mem.span(next_arg); - const RootCommandMatcher = strings.ExactSizeMatcher(8); + const RootCommandMatcher = strings.ExactSizeMatcher(16); if (comptime FeatureFlags.dev_only) { return switch (RootCommandMatcher.match(first_arg_name)) { @@ -584,6 +586,8 @@ pub const Command = struct { RootCommandMatcher.case("bun") => .BunCommand, RootCommandMatcher.case("discord") => .DiscordCommand, RootCommandMatcher.case("upgrade") => .UpgradeCommand, + RootCommandMatcher.case("completions") => .InstallCompletionsCommand, + RootCommandMatcher.case("getcompletes") => .GetCompletionsCommand, RootCommandMatcher.case("c"), RootCommandMatcher.case("create") => .CreateCommand, RootCommandMatcher.case("b"), RootCommandMatcher.case("build") => .BuildCommand, @@ -608,6 +612,20 @@ pub const Command = struct { } } + pub const InstallCompletionsCommand = struct { + pub fn exec(ctx: *std.mem.Allocator) !void {} + }; + + const default_completions_list = [_]string{ + // "build", + "run", + "dev", + "create", + "bun", + "upgrade", + "discord", + }; + pub fn start(allocator: *std.mem.Allocator, log: *logger.Log) !void { const tag = which(allocator); switch (tag) { @@ -633,6 +651,38 @@ pub const Command = struct { try BuildCommand.exec(ctx); }, + .GetCompletionsCommand => { + const ctx = try Command.Context.create(allocator, log, .GetCompletionsCommand); + var filter = ctx.positionals; + + for (filter) |item, i| { + if (strings.eqlComptime(item, "getcompletes")) { + if (i + 1 < filter.len) { + filter = filter[i + 1 ..]; + } else { + filter = &[_]string{}; + } + + break; + } + } + + var completions = ShellCompletions{}; + + if (filter.len == 0) { + completions = try RunCommand.completions(ctx, &default_completions_list, .all); + } else if (strings.eqlComptime(filter[0], "s")) { + completions = try RunCommand.completions(ctx, null, .script); + } else if (strings.eqlComptime(filter[0], "b")) { + completions = try RunCommand.completions(ctx, null, .bin); + } else if (strings.eqlComptime(filter[0], "r")) { + completions = try RunCommand.completions(ctx, null, .all); + } + + completions.print(); + + return; + }, .CreateCommand => { const ctx = try Command.Context.create(allocator, log, .CreateCommand); var positionals: [2]string = undefined; @@ -715,5 +765,7 @@ pub const Command = struct { HelpCommand, CreateCommand, UpgradeCommand, + InstallCompletionsCommand, + GetCompletionsCommand, }; }; diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index fba8296a8..60688e5a9 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -33,6 +33,8 @@ const NpmArgs = struct { const yarn_commands: []u64 = @import("./list-of-yarn-commands.zig").all_yarn_commands; +const ShellCompletions = @import("./shell_completions.zig"); + pub const RunCommand = struct { const shells_to_search = &[_]string{ "bash", @@ -267,6 +269,116 @@ pub const RunCommand = struct { this_bundler.configureLinker(); } + pub const Filter = enum { + script, + bin, + all, + }; + + pub fn completions(ctx: Command.Context, default_completions: ?[]const string, comptime filter: Filter) !ShellCompletions { + var shell_out = ShellCompletions{}; + + if (default_completions) |defaults| { + shell_out.commands = defaults; + } + + var args = ctx.args; + args.node_modules_bundle_path = null; + args.node_modules_bundle_path_server = null; + args.generate_node_module_bundle = false; + + var this_bundler = bundler.Bundler.init(ctx.allocator, ctx.log, args, null, null) catch return shell_out; + this_bundler.options.env.behavior = Api.DotEnvBehavior.load_all; + this_bundler.options.env.prefix = ""; + this_bundler.env.quiet = true; + + this_bundler.resolver.care_about_bin_folder = true; + this_bundler.resolver.care_about_scripts = true; + defer { + this_bundler.resolver.care_about_bin_folder = false; + this_bundler.resolver.care_about_scripts = false; + } + this_bundler.configureLinker(); + + var root_dir_info = (this_bundler.resolver.readDirInfo(this_bundler.fs.top_level_dir) catch null) orelse return shell_out; + + { + this_bundler.env.loadProcess(); + + if (this_bundler.env.map.get("NODE_ENV")) |node_env| { + if (strings.eqlComptime(node_env, "production")) { + this_bundler.options.production = true; + } + } + } + + const ResultList = std.StringArrayHashMap(void); + + if (this_bundler.env.map.get("SHELL")) |shell| { + shell_out.shell = ShellCompletions.Shell.fromEnv(@TypeOf(shell), shell); + } + + var results = ResultList.init(ctx.allocator); + + if (default_completions) |defaults| { + try results.ensureUnusedCapacity(defaults.len); + for (defaults) |item| { + _ = results.getOrPutAssumeCapacity(item); + } + } + + if (filter == Filter.bin or filter == Filter.all) { + for (this_bundler.resolver.binDirs()) |bin_path| { + if (this_bundler.resolver.readDirInfo(bin_path) catch null) |bin_dir| { + if (bin_dir.getEntriesConst()) |entries| { + var iter = entries.data.iterator(); + var has_copied = false; + var path_slice: string = ""; + var dir_slice: string = ""; + while (iter.next()) |entry| { + if (entry.value.kind(&this_bundler.fs.fs) == .file) { + if (!has_copied) { + std.mem.copy(u8, &path_buf, entry.value.dir); + dir_slice = path_buf[0..entry.value.dir.len]; + if (!strings.endsWithChar(entry.value.dir, std.fs.path.sep)) { + dir_slice = path_buf[0 .. entry.value.dir.len + 1]; + } + has_copied = true; + } + + const base = entry.value.base(); + std.mem.copy(u8, path_buf[dir_slice.len..], base); + path_buf[dir_slice.len + base.len] = 0; + var slice = path_buf[0 .. dir_slice.len + base.len :0]; + std.os.accessZ(slice, std.os.X_OK) catch continue; + // we need to dupe because the string pay point to a pointer that only exists in the current scope + _ = try results.getOrPut(this_bundler.fs.filename_store.append(@TypeOf(base), base) catch continue); + } + } + } + } + } + } + + if (filter == Filter.script or filter == Filter.all) { + if (root_dir_info.enclosing_package_json) |package_json| { + if (package_json.scripts) |scripts| { + try results.ensureUnusedCapacity(scripts.count()); + for (scripts.keys()) |key| { + _ = results.getOrPutAssumeCapacity(key); + } + } + } + } + + var all_keys = results.keys(); + + strings.sortAsc(all_keys); + shell_out.commands = all_keys; + + return shell_out; + } + pub fn exec(ctx: Command.Context, comptime bin_dirs_only: bool, comptime log_errors: bool) !bool { // Step 1. Figure out what we're trying to run var positionals = ctx.positionals; diff --git a/src/cli/shell_completions.zig b/src/cli/shell_completions.zig new file mode 100644 index 000000000..f64e7befb --- /dev/null +++ b/src/cli/shell_completions.zig @@ -0,0 +1,43 @@ +const std = @import("std"); +usingnamespace @import("../global.zig"); + +pub const Shell = enum { + unknown, + bash, + zsh, + fish, + + pub fn fromEnv(comptime Type: type, SHELL: Type) Shell { + const basename = std.fs.path.basename(SHELL); + if (strings.eqlComptime(basename, "bash")) { + return Shell.bash; + } else if (strings.eqlComptime(basename, "zsh")) { + return Shell.zsh; + } else if (strings.eqlComptime(basename, "fish")) { + return Shell.fish; + } else { + return Shell.unknown; + } + } +}; + +commands: []const []const u8 = &[_][]u8{}, +flags: []const []const u8 = &[_][]u8{}, +shell: Shell = Shell.unknown, + +pub fn print(this: @This()) void { + defer Output.flush(); + var writer = Output.writer(); + + if (this.commands.len == 0) return; + + writer.writeAll(this.commands[0]) catch return; + + if (this.commands.len > 1) { + for (this.commands[1..]) |cmd, i| { + writer.writeAll(" ") catch return; + + writer.writeAll(cmd) catch return; + } + } +} |