diff --git a/src/completion/context.rs b/src/completion/context.rs index 02a07e6..5b96f07 100644 --- a/src/completion/context.rs +++ b/src/completion/context.rs @@ -183,12 +183,64 @@ pub fn handle_block_or_closure( ); if is_empty { - // Empty block/closure or just whitespace - command context - console_log!("[completion] {shape_name} is empty, setting Command context"); - Some(CompletionContext::Command { - prefix: String::new(), - span: adjusted_span, - }) + // Empty block/closure or just whitespace + // Check if there's a command shape before this closure/block shape + // If so, we might be completing after that command + let mut found_command: Option = None; + for i in (0..current_idx).rev() { + if let Some((prev_span, prev_shape)) = shapes.get(i) { + let prev_local_span = to_local_span(*prev_span, global_offset); + // Check if this shape is before the current closure and is a command + if prev_local_span.end <= local_span.start { + if is_command_shape(input, prev_shape, prev_local_span) { + let cmd_text = safe_slice(input, prev_local_span); + let cmd_full = cmd_text.trim().to_string(); + + // Extract the full command text - if it contains spaces, it might be a subcommand + // We'll use the first word for parent_command to show subcommands + // The suggestion generator will filter appropriately + let cmd_first_word = extract_command_name(cmd_text).to_string(); + + // If the command contains spaces, it's likely a full command (subcommand) + // In that case, we shouldn't show subcommands + if cmd_full.contains(' ') && cmd_full != cmd_first_word { + // It's a full command (subcommand), don't show subcommands + console_log!( + "[completion] {shape_name} is empty but found full command {cmd_full:?} before it, not showing completions" + ); + return None; + } + + // Use the first word to show subcommands + found_command = Some(cmd_first_word); + console_log!( + "[completion] {shape_name} is empty but found command {found_command:?} before it" + ); + break; + } + } + } + } + + if let Some(cmd_name) = found_command { + // We found a command before the closure, show subcommands of that command + console_log!( + "[completion] {shape_name} is empty, showing subcommands of {cmd_name:?}" + ); + Some(CompletionContext::Command { + prefix: String::new(), + span: adjusted_span, + parent_command: Some(cmd_name), + }) + } else { + // Truly empty - show all commands + console_log!("[completion] {shape_name} is empty, setting Command context"); + Some(CompletionContext::Command { + prefix: String::new(), + span: adjusted_span, + parent_command: None, + }) + } } else if let Some(last_sep_pos) = last_sep_pos_in_prefix { // After a separator - command context let after_sep = prefix[last_sep_pos..].trim_start(); @@ -198,6 +250,7 @@ pub fn handle_block_or_closure( Some(CompletionContext::Command { prefix: after_sep.to_string(), span: Span::new(span.start + last_sep_pos, span.end), + parent_command: None, }) } else { console_log!( @@ -468,6 +521,7 @@ pub fn determine_context_from_shape( return Some(CompletionContext::Command { prefix: String::new(), span: adjusted_span, + parent_command: None, }); } } else { @@ -559,6 +613,7 @@ pub fn determine_context_from_shape( return Some(CompletionContext::Command { prefix: full_prefix, span: full_span, + parent_command: None, }); } FlatShape::Block | FlatShape::Closure => { @@ -696,9 +751,10 @@ pub fn determine_context_fallback( "[completion] Found command shape {shape:?} at {local_span:?}, has_separator_after_command={has_separator_after_command}" ); if !has_separator_after_command { - // Extract the command text + // Extract the command text (full command including subcommands) let cmd = safe_slice(input, local_span); - let cmd_name = extract_command_name(cmd).to_string(); + let cmd_full = cmd.trim().to_string(); + let cmd_first_word = extract_command_name(cmd).to_string(); // Check if we're right after the command (only whitespace between command and cursor) let text_after_command = if local_span.end < input.len() { @@ -710,6 +766,19 @@ pub fn determine_context_fallback( // If we're right after a command, check if it has positional arguments if is_right_after_command { + // Check if the command text contains spaces (indicating it's a subcommand like "attr category") + let is_subcommand = cmd_full.contains(' ') && cmd_full != cmd_first_word; + + // First, try the full command name (e.g., "attr category") + // If that doesn't exist, fall back to the first word (e.g., "attr") + let full_cmd_exists = + get_command_signature(engine_guard, &cmd_full).is_some(); + let cmd_name = if full_cmd_exists { + cmd_full.clone() + } else { + cmd_first_word.clone() + }; + if let Some(signature) = get_command_signature(engine_guard, &cmd_name) { // Check if command has any positional arguments let has_positional_args = !signature.required_positional.is_empty() @@ -747,20 +816,45 @@ pub fn determine_context_fallback( arg_index: arg_count, }); } else { - // No positional arguments, don't show any completions + // No positional arguments + // If this is a subcommand (contains spaces), don't show subcommands + // Only show subcommands if we're using just the base command (single word) + if is_subcommand && full_cmd_exists { + console_log!( + "[completion] Command {cmd_name:?} is a subcommand with no positional args, not showing completions" + ); + return None; + } else { + // Show subcommands of the base command + console_log!( + "[completion] Command {cmd_name:?} has no positional args, showing subcommands" + ); + return Some(CompletionContext::Command { + prefix: String::new(), + span: Span::new(byte_pos, byte_pos), + parent_command: Some(cmd_first_word), + }); + } + } + } else { + // Couldn't find signature + // If this is a subcommand, don't show completions + // Otherwise, show subcommands of the first word + if is_subcommand && full_cmd_exists { console_log!( - "[completion] Command {cmd_name:?} has no positional args, not showing completions" + "[completion] Could not find signature for subcommand {cmd_name:?}, not showing completions" ); - // Leave context as None to show no completions return None; + } else { + console_log!( + "[completion] Could not find signature for {cmd_name:?}, showing subcommands" + ); + return Some(CompletionContext::Command { + prefix: String::new(), + span: Span::new(byte_pos, byte_pos), + parent_command: Some(cmd_first_word), + }); } - } else { - // Couldn't find signature, don't show completions - console_log!( - "[completion] Could not find signature for {cmd_name:?}, not showing completions" - ); - // Leave context as None to show no completions - return None; } } else { // Not right after command, complete the command itself @@ -768,6 +862,7 @@ pub fn determine_context_fallback( return Some(CompletionContext::Command { prefix: cmd.to_string(), span: local_span, + parent_command: None, }); } } @@ -827,6 +922,7 @@ pub fn determine_context_fallback( Some(CompletionContext::Command { prefix: last_word.to_string(), span: Span::new(last_word_start, byte_pos), + parent_command: None, }) } else { // Check if this is a variable or cell path (starts with $) diff --git a/src/completion/suggestions.rs b/src/completion/suggestions.rs index 93a545c..2ca8da6 100644 --- a/src/completion/suggestions.rs +++ b/src/completion/suggestions.rs @@ -12,26 +12,59 @@ pub fn generate_command_suggestions( working_set: &StateWorkingSet, prefix: String, span: Span, + parent_command: Option, ) -> Vec { - console_log!("[completion] Generating Command suggestions with prefix: {prefix:?}"); - // Command completion - let cmds = - working_set.find_commands_by_predicate(|value| value.starts_with(prefix.as_bytes()), true); + console_log!( + "[completion] Generating Command suggestions with prefix: {prefix:?}, parent_command: {parent_command:?}" + ); let span = to_char_span(input, span); let mut suggestions = Vec::new(); let mut cmd_count = 0; + // Determine search prefix and name extraction logic + let (search_prefix, parent_prefix_opt) = if let Some(parent) = &parent_command { + // Show only subcommands of the parent command + // Subcommands are commands that start with "parent_command " (with space) + let parent_prefix = format!("{} ", parent); + let search_prefix = if prefix.is_empty() { + parent_prefix.clone() + } else { + format!("{}{}", parent_prefix, prefix) + }; + (search_prefix, Some(parent_prefix)) + } else { + // Regular command completion - show all commands + (prefix.clone(), None) + }; + + let cmds = working_set + .find_commands_by_predicate(|value| value.starts_with(search_prefix.as_bytes()), true); + for (_, name, desc, _) in cmds { let name_str = String::from_utf8_lossy(&name).to_string(); + + // Extract the command name to display + // For subcommands, extract just the subcommand name (part after "parent_command ") + // For regular commands, use the full command name + let display_name = if let Some(parent_prefix) = &parent_prefix_opt { + if let Some(subcommand_name) = name_str.strip_prefix(parent_prefix) { + subcommand_name.to_string() + } else { + continue; // Skip if it doesn't match the parent prefix + } + } else { + name_str + }; + suggestions.push(Suggestion { rendered: { - let name_colored = ansi_term::Color::Green.bold().paint(&name_str); + let name_colored = ansi_term::Color::Green.bold().paint(&display_name); let desc_str = desc.as_deref().unwrap_or(""); format!("{name_colored} {desc_str}") }, - name: name_str, - description: desc, + name: display_name, + description: desc.map(|d| d.to_string()), is_command: true, span_start: span.start, span_end: span.end, @@ -120,16 +153,28 @@ pub fn generate_flag_suggestions( // Add short flag if it matches if let Some(short) = &short_name { + let flag_char = flag.short.unwrap_or(' '); let should_show_short = if show_all { true // Show all flags when prefix is "-" or empty } else if prefix.starts_with("-") && !prefix.starts_with("--") { - short.starts_with(&prefix) // Only show short flags matching prefix + // For combined short flags like "-a" or "-af", suggest flags that can be appended + // Extract already used flags from prefix (e.g., "-a" -> ['a'], "-af" -> ['a', 'f']) + let used_flags: Vec = prefix[1..].chars().collect(); + + // Show if this flag isn't already in the prefix + !used_flags.contains(&flag_char) } else { false // Don't show short flags if prefix is long flag format }; if should_show_short { - suggestions.push(create_flag_suggestion(short.clone())); + // If prefix already contains flags (like "-a"), create combined suggestion (like "-af") + let suggestion_name = if prefix.len() > 1 && prefix.starts_with("-") { + format!("{}{}", prefix, flag_char) + } else { + short.clone() + }; + suggestions.push(create_flag_suggestion(suggestion_name)); flag_count += 1; } } @@ -146,6 +191,7 @@ pub fn generate_flag_suggestions( pub fn generate_command_argument_suggestions( input: &str, engine_guard: &EngineState, + working_set: &StateWorkingSet, prefix: String, span: Span, command_name: String, @@ -158,14 +204,110 @@ pub fn generate_command_argument_suggestions( let mut suggestions = Vec::new(); if let Some(signature) = get_command_signature(engine_guard, &command_name) { + // First, check if we're completing an argument for a flag + // Look backwards from the current position to find the previous flag + let text_before = if span.start < input.len() { + &input[..span.start] + } else { + "" + }; + let text_before_trimmed = text_before.trim_end(); + + // Check if the last word before cursor is a flag + let last_word_start = text_before_trimmed + .rfind(|c: char| c.is_whitespace()) + .map(|i| i + 1) + .unwrap_or(0); + let last_word = &text_before_trimmed[last_word_start..]; + + if last_word.starts_with('-') { + // We're after a flag - check if this flag accepts an argument + let flag_name = last_word.trim(); + let is_long_flag = flag_name.starts_with("--"); + let flag_to_match: Option<(bool, String)> = if is_long_flag { + // Long flag: --flag-name + flag_name.strip_prefix("--").map(|s| (true, s.to_string())) + } else { + // Short flag: -f (single character) + flag_name + .strip_prefix("-") + .and_then(|s| s.chars().next().map(|c| (false, c.to_string()))) + }; + + if let Some((is_long, flag_name_to_match)) = flag_to_match { + // Find the flag in the signature + for flag in &signature.named { + let matches_flag = if is_long { + // Long flag + flag.long == flag_name_to_match + } else { + // Short flag - compare character + flag.short + .map(|c| c.to_string() == flag_name_to_match) + .unwrap_or(false) + }; + + if matches_flag { + // Found the flag - check if it accepts an argument + if let Some(flag_arg_shape) = &flag.arg { + // Flag accepts an argument - use its type + console_log!( + "[completion] Flag {flag_name:?} accepts argument of type {:?}", + flag_arg_shape + ); + match flag_arg_shape { + nu_protocol::SyntaxShape::Filepath + | nu_protocol::SyntaxShape::Any => { + // File/directory completion for flag argument + let file_suggestions = generate_file_suggestions( + &prefix, + span, + root, + Some(flag.desc.clone()), + input, + ); + let file_count = file_suggestions.len(); + suggestions.extend(file_suggestions); + console_log!( + "[completion] Found {file_count} file suggestions for flag argument" + ); + } + _ => { + // Flag argument is not a filepath type + console_log!( + "[completion] Flag {flag_name:?} argument is type {:?}, not suggesting files", + flag_arg_shape + ); + } + } + return suggestions; + } else { + // Flag doesn't accept an argument - fall through to positional argument check + console_log!( + "[completion] Flag {flag_name:?} doesn't accept an argument, checking positional arguments" + ); + break; + } + } + } + } + } + + // Not after a flag, or flag doesn't accept an argument - check positional arguments // Get positional arguments from signature - // Combine required and optional positional arguments - let mut all_positional = Vec::new(); - all_positional.extend_from_slice(&signature.required_positional); - all_positional.extend_from_slice(&signature.optional_positional); + // Check if argument is in required or optional positional + let required_count = signature.required_positional.len(); + let is_optional = arg_index >= required_count; // Find the argument at the given index - if let Some(arg) = all_positional.get(arg_index) { + let arg = if arg_index < signature.required_positional.len() { + signature.required_positional.get(arg_index) + } else { + let optional_index = arg_index - required_count; + signature.optional_positional.get(optional_index) + }; + + if let Some(arg) = arg { // Check the SyntaxShape to determine completion type // Only suggest files/dirs for Filepath type (or "any" when type is unknown) match &arg.shape { @@ -183,6 +325,26 @@ pub fn generate_command_argument_suggestions( console_log!( "[completion] Found {file_count} file suggestions for argument {arg_index}" ); + + // If the argument is optional and of type Any or Filepath, also show subcommands + if is_optional { + console_log!( + "[completion] Argument {arg_index} is optional and of type {:?}, also showing subcommands", + arg.shape + ); + let subcommand_suggestions = generate_command_suggestions( + input, + working_set, + prefix.clone(), + span, + Some(command_name.clone()), + ); + let subcommand_count = subcommand_suggestions.len(); + suggestions.extend(subcommand_suggestions); + console_log!( + "[completion] Found {subcommand_count} subcommand suggestions" + ); + } } _ => { // For other types, don't suggest files @@ -193,13 +355,11 @@ pub fn generate_command_argument_suggestions( } } } else { - // Argument index out of range, fall back to file completion + // Argument index out of range - command doesn't accept that many positional arguments + // Don't suggest files since we know the type (it's not a valid argument) console_log!( - "[completion] Argument index {arg_index} out of range, using file completion" + "[completion] Argument index {arg_index} out of range, not suggesting files" ); - // Use the same file completion logic as Argument context - let file_suggestions = generate_file_suggestions(&prefix, span, root, None, input); - suggestions.extend(file_suggestions); } } else { // No signature found, fall back to file completion @@ -333,9 +493,11 @@ pub fn generate_suggestions( console_log!("context: {context:?}"); match context { - Some(CompletionContext::Command { prefix, span }) => { - generate_command_suggestions(input, working_set, prefix, span) - } + Some(CompletionContext::Command { + prefix, + span, + parent_command, + }) => generate_command_suggestions(input, working_set, prefix, span, parent_command), Some(CompletionContext::Argument { prefix, span }) => { generate_argument_suggestions(input, prefix, span, root) } @@ -352,6 +514,7 @@ pub fn generate_suggestions( }) => generate_command_argument_suggestions( input, engine_guard, + working_set, prefix, span, command_name, diff --git a/src/completion/types.rs b/src/completion/types.rs index 4769ba5..0121884 100644 --- a/src/completion/types.rs +++ b/src/completion/types.rs @@ -33,6 +33,7 @@ pub enum CompletionContext { Command { prefix: String, span: Span, + parent_command: Option, // If Some, only show subcommands of this command }, Argument { prefix: String,