···183183 );
184184185185 if is_empty {
186186- // Empty block/closure or just whitespace - command context
187187- console_log!("[completion] {shape_name} is empty, setting Command context");
188188- Some(CompletionContext::Command {
189189- prefix: String::new(),
190190- span: adjusted_span,
191191- })
186186+ // Empty block/closure or just whitespace
187187+ // Check if there's a command shape before this closure/block shape
188188+ // If so, we might be completing after that command
189189+ let mut found_command: Option<String> = None;
190190+ for i in (0..current_idx).rev() {
191191+ if let Some((prev_span, prev_shape)) = shapes.get(i) {
192192+ let prev_local_span = to_local_span(*prev_span, global_offset);
193193+ // Check if this shape is before the current closure and is a command
194194+ if prev_local_span.end <= local_span.start {
195195+ if is_command_shape(input, prev_shape, prev_local_span) {
196196+ let cmd_text = safe_slice(input, prev_local_span);
197197+ let cmd_full = cmd_text.trim().to_string();
198198+199199+ // Extract the full command text - if it contains spaces, it might be a subcommand
200200+ // We'll use the first word for parent_command to show subcommands
201201+ // The suggestion generator will filter appropriately
202202+ let cmd_first_word = extract_command_name(cmd_text).to_string();
203203+204204+ // If the command contains spaces, it's likely a full command (subcommand)
205205+ // In that case, we shouldn't show subcommands
206206+ if cmd_full.contains(' ') && cmd_full != cmd_first_word {
207207+ // It's a full command (subcommand), don't show subcommands
208208+ console_log!(
209209+ "[completion] {shape_name} is empty but found full command {cmd_full:?} before it, not showing completions"
210210+ );
211211+ return None;
212212+ }
213213+214214+ // Use the first word to show subcommands
215215+ found_command = Some(cmd_first_word);
216216+ console_log!(
217217+ "[completion] {shape_name} is empty but found command {found_command:?} before it"
218218+ );
219219+ break;
220220+ }
221221+ }
222222+ }
223223+ }
224224+225225+ if let Some(cmd_name) = found_command {
226226+ // We found a command before the closure, show subcommands of that command
227227+ console_log!(
228228+ "[completion] {shape_name} is empty, showing subcommands of {cmd_name:?}"
229229+ );
230230+ Some(CompletionContext::Command {
231231+ prefix: String::new(),
232232+ span: adjusted_span,
233233+ parent_command: Some(cmd_name),
234234+ })
235235+ } else {
236236+ // Truly empty - show all commands
237237+ console_log!("[completion] {shape_name} is empty, setting Command context");
238238+ Some(CompletionContext::Command {
239239+ prefix: String::new(),
240240+ span: adjusted_span,
241241+ parent_command: None,
242242+ })
243243+ }
192244 } else if let Some(last_sep_pos) = last_sep_pos_in_prefix {
193245 // After a separator - command context
194246 let after_sep = prefix[last_sep_pos..].trim_start();
···198250 Some(CompletionContext::Command {
199251 prefix: after_sep.to_string(),
200252 span: Span::new(span.start + last_sep_pos, span.end),
253253+ parent_command: None,
201254 })
202255 } else {
203256 console_log!(
···468521 return Some(CompletionContext::Command {
469522 prefix: String::new(),
470523 span: adjusted_span,
524524+ parent_command: None,
471525 });
472526 }
473527 } else {
···559613 return Some(CompletionContext::Command {
560614 prefix: full_prefix,
561615 span: full_span,
616616+ parent_command: None,
562617 });
563618 }
564619 FlatShape::Block | FlatShape::Closure => {
···696751 "[completion] Found command shape {shape:?} at {local_span:?}, has_separator_after_command={has_separator_after_command}"
697752 );
698753 if !has_separator_after_command {
699699- // Extract the command text
754754+ // Extract the command text (full command including subcommands)
700755 let cmd = safe_slice(input, local_span);
701701- let cmd_name = extract_command_name(cmd).to_string();
756756+ let cmd_full = cmd.trim().to_string();
757757+ let cmd_first_word = extract_command_name(cmd).to_string();
702758703759 // Check if we're right after the command (only whitespace between command and cursor)
704760 let text_after_command = if local_span.end < input.len() {
···710766711767 // If we're right after a command, check if it has positional arguments
712768 if is_right_after_command {
769769+ // Check if the command text contains spaces (indicating it's a subcommand like "attr category")
770770+ let is_subcommand = cmd_full.contains(' ') && cmd_full != cmd_first_word;
771771+772772+ // First, try the full command name (e.g., "attr category")
773773+ // If that doesn't exist, fall back to the first word (e.g., "attr")
774774+ let full_cmd_exists =
775775+ get_command_signature(engine_guard, &cmd_full).is_some();
776776+ let cmd_name = if full_cmd_exists {
777777+ cmd_full.clone()
778778+ } else {
779779+ cmd_first_word.clone()
780780+ };
781781+713782 if let Some(signature) = get_command_signature(engine_guard, &cmd_name) {
714783 // Check if command has any positional arguments
715784 let has_positional_args = !signature.required_positional.is_empty()
···747816 arg_index: arg_count,
748817 });
749818 } else {
750750- // No positional arguments, don't show any completions
819819+ // No positional arguments
820820+ // If this is a subcommand (contains spaces), don't show subcommands
821821+ // Only show subcommands if we're using just the base command (single word)
822822+ if is_subcommand && full_cmd_exists {
823823+ console_log!(
824824+ "[completion] Command {cmd_name:?} is a subcommand with no positional args, not showing completions"
825825+ );
826826+ return None;
827827+ } else {
828828+ // Show subcommands of the base command
829829+ console_log!(
830830+ "[completion] Command {cmd_name:?} has no positional args, showing subcommands"
831831+ );
832832+ return Some(CompletionContext::Command {
833833+ prefix: String::new(),
834834+ span: Span::new(byte_pos, byte_pos),
835835+ parent_command: Some(cmd_first_word),
836836+ });
837837+ }
838838+ }
839839+ } else {
840840+ // Couldn't find signature
841841+ // If this is a subcommand, don't show completions
842842+ // Otherwise, show subcommands of the first word
843843+ if is_subcommand && full_cmd_exists {
751844 console_log!(
752752- "[completion] Command {cmd_name:?} has no positional args, not showing completions"
845845+ "[completion] Could not find signature for subcommand {cmd_name:?}, not showing completions"
753846 );
754754- // Leave context as None to show no completions
755847 return None;
848848+ } else {
849849+ console_log!(
850850+ "[completion] Could not find signature for {cmd_name:?}, showing subcommands"
851851+ );
852852+ return Some(CompletionContext::Command {
853853+ prefix: String::new(),
854854+ span: Span::new(byte_pos, byte_pos),
855855+ parent_command: Some(cmd_first_word),
856856+ });
756857 }
757757- } else {
758758- // Couldn't find signature, don't show completions
759759- console_log!(
760760- "[completion] Could not find signature for {cmd_name:?}, not showing completions"
761761- );
762762- // Leave context as None to show no completions
763763- return None;
764858 }
765859 } else {
766860 // Not right after command, complete the command itself
···768862 return Some(CompletionContext::Command {
769863 prefix: cmd.to_string(),
770864 span: local_span,
865865+ parent_command: None,
771866 });
772867 }
773868 }
···827922 Some(CompletionContext::Command {
828923 prefix: last_word.to_string(),
829924 span: Span::new(last_word_start, byte_pos),
925925+ parent_command: None,
830926 })
831927 } else {
832928 // Check if this is a variable or cell path (starts with $)
+185-22
src/completion/suggestions.rs
···1212 working_set: &StateWorkingSet,
1313 prefix: String,
1414 span: Span,
1515+ parent_command: Option<String>,
1516) -> Vec<Suggestion> {
1616- console_log!("[completion] Generating Command suggestions with prefix: {prefix:?}");
1717- // Command completion
1818- let cmds =
1919- working_set.find_commands_by_predicate(|value| value.starts_with(prefix.as_bytes()), true);
1717+ console_log!(
1818+ "[completion] Generating Command suggestions with prefix: {prefix:?}, parent_command: {parent_command:?}"
1919+ );
20202121 let span = to_char_span(input, span);
2222 let mut suggestions = Vec::new();
2323 let mut cmd_count = 0;
24242525+ // Determine search prefix and name extraction logic
2626+ let (search_prefix, parent_prefix_opt) = if let Some(parent) = &parent_command {
2727+ // Show only subcommands of the parent command
2828+ // Subcommands are commands that start with "parent_command " (with space)
2929+ let parent_prefix = format!("{} ", parent);
3030+ let search_prefix = if prefix.is_empty() {
3131+ parent_prefix.clone()
3232+ } else {
3333+ format!("{}{}", parent_prefix, prefix)
3434+ };
3535+ (search_prefix, Some(parent_prefix))
3636+ } else {
3737+ // Regular command completion - show all commands
3838+ (prefix.clone(), None)
3939+ };
4040+4141+ let cmds = working_set
4242+ .find_commands_by_predicate(|value| value.starts_with(search_prefix.as_bytes()), true);
4343+2544 for (_, name, desc, _) in cmds {
2645 let name_str = String::from_utf8_lossy(&name).to_string();
4646+4747+ // Extract the command name to display
4848+ // For subcommands, extract just the subcommand name (part after "parent_command ")
4949+ // For regular commands, use the full command name
5050+ let display_name = if let Some(parent_prefix) = &parent_prefix_opt {
5151+ if let Some(subcommand_name) = name_str.strip_prefix(parent_prefix) {
5252+ subcommand_name.to_string()
5353+ } else {
5454+ continue; // Skip if it doesn't match the parent prefix
5555+ }
5656+ } else {
5757+ name_str
5858+ };
5959+2760 suggestions.push(Suggestion {
2861 rendered: {
2929- let name_colored = ansi_term::Color::Green.bold().paint(&name_str);
6262+ let name_colored = ansi_term::Color::Green.bold().paint(&display_name);
3063 let desc_str = desc.as_deref().unwrap_or("<no description>");
3164 format!("{name_colored} {desc_str}")
3265 },
3333- name: name_str,
3434- description: desc,
6666+ name: display_name,
6767+ description: desc.map(|d| d.to_string()),
3568 is_command: true,
3669 span_start: span.start,
3770 span_end: span.end,
···120153121154 // Add short flag if it matches
122155 if let Some(short) = &short_name {
156156+ let flag_char = flag.short.unwrap_or(' ');
123157 let should_show_short = if show_all {
124158 true // Show all flags when prefix is "-" or empty
125159 } else if prefix.starts_with("-") && !prefix.starts_with("--") {
126126- short.starts_with(&prefix) // Only show short flags matching prefix
160160+ // For combined short flags like "-a" or "-af", suggest flags that can be appended
161161+ // Extract already used flags from prefix (e.g., "-a" -> ['a'], "-af" -> ['a', 'f'])
162162+ let used_flags: Vec<char> = prefix[1..].chars().collect();
163163+164164+ // Show if this flag isn't already in the prefix
165165+ !used_flags.contains(&flag_char)
127166 } else {
128167 false // Don't show short flags if prefix is long flag format
129168 };
130169131170 if should_show_short {
132132- suggestions.push(create_flag_suggestion(short.clone()));
171171+ // If prefix already contains flags (like "-a"), create combined suggestion (like "-af")
172172+ let suggestion_name = if prefix.len() > 1 && prefix.starts_with("-") {
173173+ format!("{}{}", prefix, flag_char)
174174+ } else {
175175+ short.clone()
176176+ };
177177+ suggestions.push(create_flag_suggestion(suggestion_name));
133178 flag_count += 1;
134179 }
135180 }
···146191pub fn generate_command_argument_suggestions(
147192 input: &str,
148193 engine_guard: &EngineState,
194194+ working_set: &StateWorkingSet,
149195 prefix: String,
150196 span: Span,
151197 command_name: String,
···158204159205 let mut suggestions = Vec::new();
160206 if let Some(signature) = get_command_signature(engine_guard, &command_name) {
207207+ // First, check if we're completing an argument for a flag
208208+ // Look backwards from the current position to find the previous flag
209209+ let text_before = if span.start < input.len() {
210210+ &input[..span.start]
211211+ } else {
212212+ ""
213213+ };
214214+ let text_before_trimmed = text_before.trim_end();
215215+216216+ // Check if the last word before cursor is a flag
217217+ let last_word_start = text_before_trimmed
218218+ .rfind(|c: char| c.is_whitespace())
219219+ .map(|i| i + 1)
220220+ .unwrap_or(0);
221221+ let last_word = &text_before_trimmed[last_word_start..];
222222+223223+ if last_word.starts_with('-') {
224224+ // We're after a flag - check if this flag accepts an argument
225225+ let flag_name = last_word.trim();
226226+ let is_long_flag = flag_name.starts_with("--");
227227+ let flag_to_match: Option<(bool, String)> = if is_long_flag {
228228+ // Long flag: --flag-name
229229+ flag_name.strip_prefix("--").map(|s| (true, s.to_string()))
230230+ } else {
231231+ // Short flag: -f (single character)
232232+ flag_name
233233+ .strip_prefix("-")
234234+ .and_then(|s| s.chars().next().map(|c| (false, c.to_string())))
235235+ };
236236+237237+ if let Some((is_long, flag_name_to_match)) = flag_to_match {
238238+ // Find the flag in the signature
239239+ for flag in &signature.named {
240240+ let matches_flag = if is_long {
241241+ // Long flag
242242+ flag.long == flag_name_to_match
243243+ } else {
244244+ // Short flag - compare character
245245+ flag.short
246246+ .map(|c| c.to_string() == flag_name_to_match)
247247+ .unwrap_or(false)
248248+ };
249249+250250+ if matches_flag {
251251+ // Found the flag - check if it accepts an argument
252252+ if let Some(flag_arg_shape) = &flag.arg {
253253+ // Flag accepts an argument - use its type
254254+ console_log!(
255255+ "[completion] Flag {flag_name:?} accepts argument of type {:?}",
256256+ flag_arg_shape
257257+ );
258258+ match flag_arg_shape {
259259+ nu_protocol::SyntaxShape::Filepath
260260+ | nu_protocol::SyntaxShape::Any => {
261261+ // File/directory completion for flag argument
262262+ let file_suggestions = generate_file_suggestions(
263263+ &prefix,
264264+ span,
265265+ root,
266266+ Some(flag.desc.clone()),
267267+ input,
268268+ );
269269+ let file_count = file_suggestions.len();
270270+ suggestions.extend(file_suggestions);
271271+ console_log!(
272272+ "[completion] Found {file_count} file suggestions for flag argument"
273273+ );
274274+ }
275275+ _ => {
276276+ // Flag argument is not a filepath type
277277+ console_log!(
278278+ "[completion] Flag {flag_name:?} argument is type {:?}, not suggesting files",
279279+ flag_arg_shape
280280+ );
281281+ }
282282+ }
283283+ return suggestions;
284284+ } else {
285285+ // Flag doesn't accept an argument - fall through to positional argument check
286286+ console_log!(
287287+ "[completion] Flag {flag_name:?} doesn't accept an argument, checking positional arguments"
288288+ );
289289+ break;
290290+ }
291291+ }
292292+ }
293293+ }
294294+ }
295295+296296+ // Not after a flag, or flag doesn't accept an argument - check positional arguments
161297 // Get positional arguments from signature
162162- // Combine required and optional positional arguments
163163- let mut all_positional = Vec::new();
164164- all_positional.extend_from_slice(&signature.required_positional);
165165- all_positional.extend_from_slice(&signature.optional_positional);
298298+ // Check if argument is in required or optional positional
299299+ let required_count = signature.required_positional.len();
300300+ let is_optional = arg_index >= required_count;
166301167302 // Find the argument at the given index
168168- if let Some(arg) = all_positional.get(arg_index) {
303303+ let arg = if arg_index < signature.required_positional.len() {
304304+ signature.required_positional.get(arg_index)
305305+ } else {
306306+ let optional_index = arg_index - required_count;
307307+ signature.optional_positional.get(optional_index)
308308+ };
309309+310310+ if let Some(arg) = arg {
169311 // Check the SyntaxShape to determine completion type
170312 // Only suggest files/dirs for Filepath type (or "any" when type is unknown)
171313 match &arg.shape {
···183325 console_log!(
184326 "[completion] Found {file_count} file suggestions for argument {arg_index}"
185327 );
328328+329329+ // If the argument is optional and of type Any or Filepath, also show subcommands
330330+ if is_optional {
331331+ console_log!(
332332+ "[completion] Argument {arg_index} is optional and of type {:?}, also showing subcommands",
333333+ arg.shape
334334+ );
335335+ let subcommand_suggestions = generate_command_suggestions(
336336+ input,
337337+ working_set,
338338+ prefix.clone(),
339339+ span,
340340+ Some(command_name.clone()),
341341+ );
342342+ let subcommand_count = subcommand_suggestions.len();
343343+ suggestions.extend(subcommand_suggestions);
344344+ console_log!(
345345+ "[completion] Found {subcommand_count} subcommand suggestions"
346346+ );
347347+ }
186348 }
187349 _ => {
188350 // For other types, don't suggest files
···193355 }
194356 }
195357 } else {
196196- // Argument index out of range, fall back to file completion
358358+ // Argument index out of range - command doesn't accept that many positional arguments
359359+ // Don't suggest files since we know the type (it's not a valid argument)
197360 console_log!(
198198- "[completion] Argument index {arg_index} out of range, using file completion"
361361+ "[completion] Argument index {arg_index} out of range, not suggesting files"
199362 );
200200- // Use the same file completion logic as Argument context
201201- let file_suggestions = generate_file_suggestions(&prefix, span, root, None, input);
202202- suggestions.extend(file_suggestions);
203363 }
204364 } else {
205365 // No signature found, fall back to file completion
···333493 console_log!("context: {context:?}");
334494335495 match context {
336336- Some(CompletionContext::Command { prefix, span }) => {
337337- generate_command_suggestions(input, working_set, prefix, span)
338338- }
496496+ Some(CompletionContext::Command {
497497+ prefix,
498498+ span,
499499+ parent_command,
500500+ }) => generate_command_suggestions(input, working_set, prefix, span, parent_command),
339501 Some(CompletionContext::Argument { prefix, span }) => {
340502 generate_argument_suggestions(input, prefix, span, root)
341503 }
···352514 }) => generate_command_argument_suggestions(
353515 input,
354516 engine_guard,
517517+ working_set,
355518 prefix,
356519 span,
357520 command_name,
+1
src/completion/types.rs
···3333 Command {
3434 prefix: String,
3535 span: Span,
3636+ parent_command: Option<String>, // If Some, only show subcommands of this command
3637 },
3738 Argument {
3839 prefix: String,