Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
37 changes: 37 additions & 0 deletions cli/src/cli/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ impl CompleteWord {

let parsed = usage::parse::parse_partial(spec, &words)?;
debug!("parsed cmd: {}", parsed.cmd.full_cmd.join(" "));

// Check if previous token was a restart_token - if so, complete from first arg
let prev_token = if cword > 0 {
self.words.get(cword - 1).map(|s| s.as_str())
} else {
None
};
let after_restart_token = parsed
.cmd
.restart_token
.as_ref()
.is_some_and(|rt| prev_token == Some(rt.as_str()));

let mut choices = if ctoken == "-" {
let shorts = self.complete_short_flag_names(&parsed.available_flags, "");
let longs = self.complete_long_flag_names(&parsed.available_flags, "");
Expand All @@ -102,6 +115,13 @@ impl CompleteWord {
self.complete_short_flag_names(&parsed.available_flags, &ctoken)
} else if let Some(flag) = parsed.flag_awaiting_value.first() {
self.complete_arg(&ctx, spec, &parsed.cmd, flag.arg.as_ref().unwrap(), &ctoken)?
} else if after_restart_token {
// After a restart_token, complete from the first arg of the current command
let mut choices = vec![];
if let Some(arg) = parsed.cmd.args.first() {
choices.extend(self.complete_arg(&ctx, spec, &parsed.cmd, arg, &ctoken)?);
}
choices
} else {
let mut choices = vec![];
if let Some(arg) = parsed.cmd.args.get(parsed.args.len()) {
Expand All @@ -110,6 +130,23 @@ impl CompleteWord {
if !parsed.cmd.subcommands.is_empty() {
choices.extend(self.complete_subcommands(&parsed.cmd, &ctoken));
}
// If at root command with default_subcommand, also include completions from it
if parsed.cmd.name == spec.cmd.name {
if let Some(default_name) = &spec.default_subcommand {
if let Some(default_cmd) = spec.cmd.find_subcommand(default_name) {
// Include completions from default subcommand's first arg
if let Some(arg) = default_cmd.args.first() {
choices.extend(self.complete_arg(
&ctx,
spec,
default_cmd,
arg,
&ctoken,
)?);
}
}
}
}
choices
};
// Fallback to file completions if nothing is known about this argument and it's not a flag
Expand Down
164 changes: 164 additions & 0 deletions lib/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,22 @@ pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miett
}
} else {
// Found a word that's not a flag or subcommand
// Check if we should use the default_subcommand
if let Some(default_name) = &spec.default_subcommand {
if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
let mut subcommand = subcommand.clone();
// Pass prefix words (global flags before this) to mount
subcommand.mount(&prefix_words)?;
out.available_flags.retain(|_, f| f.global);
out.available_flags.extend(gather_flags(&subcommand));
out.cmds.push(subcommand.clone());
out.cmd = subcommand.clone();
prefix_words.clear();
// Don't remove the current word - it's an argument to the default subcommand
// Don't increment idx - let Phase 2 handle this word as a positional arg
break;
}
}
// This could be a positional argument, so stop subcommand search
break;
}
Expand All @@ -252,6 +268,18 @@ pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miett
while !input.is_empty() {
let mut w = input.pop_front().unwrap();

// Check for restart_token - resets argument parsing for multiple command invocations
// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
if let Some(ref restart_token) = out.cmd.restart_token {
if w == *restart_token {
// Reset argument parsing state
out.args.clear();
next_arg = out.cmd.args.first();
// Keep flags and continue parsing
continue;
}
Comment thread
cursor[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restart_token doesn't reset enable_flags after -- separator

When encountering a restart_token, the enable_flags variable is not reset to true. If the user previously used -- to disable flag parsing (e.g., mise run task1 -- extra ::: --verbose task2), flags after the restart token will still be treated as positional arguments instead of flags. Since the restart token represents a new independent command invocation, enable_flags needs to be reset to true alongside the other state resets to allow proper flag parsing in subsequent invocations.

Fix in Cursor Fix in Web

}

if w == "--" {
enable_flags = false;
continue;
Expand Down Expand Up @@ -987,4 +1015,140 @@ mod tests {
_ => panic!("Expected String, got {:?}", value),
}
}

#[test]
fn test_default_subcommand() {
// Test that default_subcommand routes to the specified subcommand
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);

let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
default_subcommand: Some("run".to_string()),
..Default::default()
};

// "test mytask" should be parsed as if it were "test run mytask"
let input = vec!["test".to_string(), "mytask".to_string()];
let parsed = parse(&spec, &input).unwrap();

// Should have two commands: root and "run"
assert_eq!(parsed.cmds.len(), 2);
assert_eq!(parsed.cmds[1].name, "run");

// Should have parsed the task argument
assert_eq!(parsed.args.len(), 1);
let arg = parsed.args.keys().next().unwrap();
assert_eq!(arg.name, "task");
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "mytask");
}

#[test]
fn test_default_subcommand_explicit_still_works() {
// Test that explicit subcommand takes precedence
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.build();
let other_cmd = SpecCommand::builder()
.name("other")
.arg(SpecArg::builder().name("other_arg").build())
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);
cmd.subcommands.insert("other".to_string(), other_cmd);

let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
default_subcommand: Some("run".to_string()),
..Default::default()
};

// "test other foo" should use "other" subcommand, not default
let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
let parsed = parse(&spec, &input).unwrap();

// Should have used "other" subcommand
assert_eq!(parsed.cmds.len(), 2);
assert_eq!(parsed.cmds[1].name, "other");
}

#[test]
fn test_restart_token() {
// Test that restart_token resets argument parsing
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.restart_token(":::".to_string())
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);

let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};

// "test run task1 ::: task2" - should end up with task2 as the arg
let input = vec![
"test".to_string(),
"run".to_string(),
"task1".to_string(),
":::".to_string(),
"task2".to_string(),
];
let parsed = parse(&spec, &input).unwrap();

// After restart, args were cleared and task2 was parsed
assert_eq!(parsed.args.len(), 1);
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "task2");
}

#[test]
fn test_restart_token_multiple() {
// Test multiple restart tokens
let run_cmd = SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.restart_token(":::".to_string())
.build();
let mut cmd = SpecCommand::builder().name("test").build();
cmd.subcommands.insert("run".to_string(), run_cmd);

let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};

// "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
let input = vec![
"test".to_string(),
"run".to_string(),
"task1".to_string(),
":::".to_string(),
"task2".to_string(),
":::".to_string(),
"task3".to_string(),
];
let parsed = parse(&spec, &input).unwrap();

// After multiple restarts, args were cleared and task3 was parsed
assert_eq!(parsed.args.len(), 1);
let value = parsed.args.values().next().unwrap();
assert_eq!(value.to_string(), "task3");
}
}
7 changes: 7 additions & 0 deletions lib/src/spec/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,13 @@ impl SpecCommandBuilder {
self
}

/// Set restart token for resetting argument parsing
/// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
pub fn restart_token(mut self, token: impl Into<String>) -> Self {
self.inner.restart_token = Some(token.into());
self
}

/// Build the final SpecCommand
pub fn build(mut self) -> SpecCommand {
self.inner.usage = self.inner.usage();
Expand Down
16 changes: 16 additions & 0 deletions lib/src/spec/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ pub struct SpecCommand {
pub hide: bool,
#[serde(skip_serializing_if = "is_false")]
pub subcommand_required: bool,
/// Token that resets argument parsing, allowing multiple command invocations.
/// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
#[serde(skip_serializing_if = "Option::is_none")]
pub restart_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -69,6 +73,7 @@ impl Default for SpecCommand {
deprecated: None,
hide: false,
subcommand_required: false,
restart_token: None,
help: None,
help_long: None,
help_md: None,
Expand Down Expand Up @@ -154,6 +159,7 @@ impl SpecCommand {
"after_help_md" => cmd.after_help_md = Some(v.ensure_string()?),
"subcommand_required" => cmd.subcommand_required = v.ensure_bool()?,
"hide" => cmd.hide = v.ensure_bool()?,
"restart_token" => cmd.restart_token = Some(v.ensure_string()?),
"deprecated" => {
cmd.deprecated = match v.value.as_bool() {
Some(true) => Some("deprecated".to_string()),
Expand Down Expand Up @@ -226,6 +232,9 @@ impl SpecCommand {
cmd.subcommand_required = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
}
"hide" => cmd.hide = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?,
"restart_token" => {
cmd.restart_token = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
}
"deprecated" => {
cmd.deprecated = match child.arg(0)?.value.as_bool() {
Some(true) => Some("deprecated".to_string()),
Expand Down Expand Up @@ -341,6 +350,9 @@ impl SpecCommand {
}
self.hide = other.hide;
self.subcommand_required = other.subcommand_required;
if other.restart_token.is_some() {
self.restart_token = other.restart_token;
}
for (name, cmd) in other.subcommands {
self.subcommands.insert(name, cmd);
}
Expand Down Expand Up @@ -412,6 +424,10 @@ impl From<&SpecCommand> for KdlNode {
node.entries_mut()
.push(KdlEntry::new_prop("subcommand_required", true));
}
if let Some(restart_token) = &cmd.restart_token {
node.entries_mut()
.push(KdlEntry::new_prop("restart_token", restart_token.clone()));
}
if !cmd.aliases.is_empty() {
let mut aliases = KdlNode::new("alias");
for alias in &cmd.aliases {
Expand Down
15 changes: 15 additions & 0 deletions lib/src/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ pub struct Spec {
pub min_usage_version: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub examples: Vec<SpecExample>,
/// Default subcommand to use when first non-flag argument is not a known subcommand.
/// This enables "naked" command syntax like `mise foo` instead of `mise run foo`.
#[serde(skip_serializing_if = "Option::is_none")]
pub default_subcommand: Option<String>,
}

impl Spec {
Expand Down Expand Up @@ -140,6 +144,9 @@ impl Spec {
check_usage_version(&v);
schema.min_usage_version = Some(v);
}
"default_subcommand" => {
schema.default_subcommand = Some(node.arg(0)?.ensure_string()?)
}
"example" => {
let code = node.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?;
let mut example = SpecExample::new(code.trim().to_string());
Expand Down Expand Up @@ -224,6 +231,9 @@ impl Spec {
if !other.examples.is_empty() {
self.examples.extend(other.examples);
}
if other.default_subcommand.is_some() {
self.default_subcommand = other.default_subcommand;
}
self.cmd.merge(other.cmd);
}
}
Expand Down Expand Up @@ -353,6 +363,11 @@ impl Display for Spec {
node.push(KdlEntry::new(min_usage_version.clone()));
nodes.push(node);
}
if let Some(default_subcommand) = &self.default_subcommand {
let mut node = KdlNode::new("default_subcommand");
node.push(KdlEntry::new(default_subcommand.clone()));
nodes.push(node);
}
if !self.usage.is_empty() {
let mut node = KdlNode::new("usage");
node.push(KdlEntry::new(self.usage.clone()));
Expand Down