+114
-18
src/completion/context.rs
+114
-18
src/completion/context.rs
···
183
183
);
184
184
185
185
if is_empty {
186
-
// Empty block/closure or just whitespace - command context
187
-
console_log!("[completion] {shape_name} is empty, setting Command context");
188
-
Some(CompletionContext::Command {
189
-
prefix: String::new(),
190
-
span: adjusted_span,
191
-
})
186
+
// Empty block/closure or just whitespace
187
+
// Check if there's a command shape before this closure/block shape
188
+
// If so, we might be completing after that command
189
+
let mut found_command: Option<String> = None;
190
+
for i in (0..current_idx).rev() {
191
+
if let Some((prev_span, prev_shape)) = shapes.get(i) {
192
+
let prev_local_span = to_local_span(*prev_span, global_offset);
193
+
// Check if this shape is before the current closure and is a command
194
+
if prev_local_span.end <= local_span.start {
195
+
if is_command_shape(input, prev_shape, prev_local_span) {
196
+
let cmd_text = safe_slice(input, prev_local_span);
197
+
let cmd_full = cmd_text.trim().to_string();
198
+
199
+
// Extract the full command text - if it contains spaces, it might be a subcommand
200
+
// We'll use the first word for parent_command to show subcommands
201
+
// The suggestion generator will filter appropriately
202
+
let cmd_first_word = extract_command_name(cmd_text).to_string();
203
+
204
+
// If the command contains spaces, it's likely a full command (subcommand)
205
+
// In that case, we shouldn't show subcommands
206
+
if cmd_full.contains(' ') && cmd_full != cmd_first_word {
207
+
// It's a full command (subcommand), don't show subcommands
208
+
console_log!(
209
+
"[completion] {shape_name} is empty but found full command {cmd_full:?} before it, not showing completions"
210
+
);
211
+
return None;
212
+
}
213
+
214
+
// Use the first word to show subcommands
215
+
found_command = Some(cmd_first_word);
216
+
console_log!(
217
+
"[completion] {shape_name} is empty but found command {found_command:?} before it"
218
+
);
219
+
break;
220
+
}
221
+
}
222
+
}
223
+
}
224
+
225
+
if let Some(cmd_name) = found_command {
226
+
// We found a command before the closure, show subcommands of that command
227
+
console_log!(
228
+
"[completion] {shape_name} is empty, showing subcommands of {cmd_name:?}"
229
+
);
230
+
Some(CompletionContext::Command {
231
+
prefix: String::new(),
232
+
span: adjusted_span,
233
+
parent_command: Some(cmd_name),
234
+
})
235
+
} else {
236
+
// Truly empty - show all commands
237
+
console_log!("[completion] {shape_name} is empty, setting Command context");
238
+
Some(CompletionContext::Command {
239
+
prefix: String::new(),
240
+
span: adjusted_span,
241
+
parent_command: None,
242
+
})
243
+
}
192
244
} else if let Some(last_sep_pos) = last_sep_pos_in_prefix {
193
245
// After a separator - command context
194
246
let after_sep = prefix[last_sep_pos..].trim_start();
···
198
250
Some(CompletionContext::Command {
199
251
prefix: after_sep.to_string(),
200
252
span: Span::new(span.start + last_sep_pos, span.end),
253
+
parent_command: None,
201
254
})
202
255
} else {
203
256
console_log!(
···
468
521
return Some(CompletionContext::Command {
469
522
prefix: String::new(),
470
523
span: adjusted_span,
524
+
parent_command: None,
471
525
});
472
526
}
473
527
} else {
···
559
613
return Some(CompletionContext::Command {
560
614
prefix: full_prefix,
561
615
span: full_span,
616
+
parent_command: None,
562
617
});
563
618
}
564
619
FlatShape::Block | FlatShape::Closure => {
···
696
751
"[completion] Found command shape {shape:?} at {local_span:?}, has_separator_after_command={has_separator_after_command}"
697
752
);
698
753
if !has_separator_after_command {
699
-
// Extract the command text
754
+
// Extract the command text (full command including subcommands)
700
755
let cmd = safe_slice(input, local_span);
701
-
let cmd_name = extract_command_name(cmd).to_string();
756
+
let cmd_full = cmd.trim().to_string();
757
+
let cmd_first_word = extract_command_name(cmd).to_string();
702
758
703
759
// Check if we're right after the command (only whitespace between command and cursor)
704
760
let text_after_command = if local_span.end < input.len() {
···
710
766
711
767
// If we're right after a command, check if it has positional arguments
712
768
if is_right_after_command {
769
+
// Check if the command text contains spaces (indicating it's a subcommand like "attr category")
770
+
let is_subcommand = cmd_full.contains(' ') && cmd_full != cmd_first_word;
771
+
772
+
// First, try the full command name (e.g., "attr category")
773
+
// If that doesn't exist, fall back to the first word (e.g., "attr")
774
+
let full_cmd_exists =
775
+
get_command_signature(engine_guard, &cmd_full).is_some();
776
+
let cmd_name = if full_cmd_exists {
777
+
cmd_full.clone()
778
+
} else {
779
+
cmd_first_word.clone()
780
+
};
781
+
713
782
if let Some(signature) = get_command_signature(engine_guard, &cmd_name) {
714
783
// Check if command has any positional arguments
715
784
let has_positional_args = !signature.required_positional.is_empty()
···
747
816
arg_index: arg_count,
748
817
});
749
818
} else {
750
-
// No positional arguments, don't show any completions
819
+
// No positional arguments
820
+
// If this is a subcommand (contains spaces), don't show subcommands
821
+
// Only show subcommands if we're using just the base command (single word)
822
+
if is_subcommand && full_cmd_exists {
823
+
console_log!(
824
+
"[completion] Command {cmd_name:?} is a subcommand with no positional args, not showing completions"
825
+
);
826
+
return None;
827
+
} else {
828
+
// Show subcommands of the base command
829
+
console_log!(
830
+
"[completion] Command {cmd_name:?} has no positional args, showing subcommands"
831
+
);
832
+
return Some(CompletionContext::Command {
833
+
prefix: String::new(),
834
+
span: Span::new(byte_pos, byte_pos),
835
+
parent_command: Some(cmd_first_word),
836
+
});
837
+
}
838
+
}
839
+
} else {
840
+
// Couldn't find signature
841
+
// If this is a subcommand, don't show completions
842
+
// Otherwise, show subcommands of the first word
843
+
if is_subcommand && full_cmd_exists {
751
844
console_log!(
752
-
"[completion] Command {cmd_name:?} has no positional args, not showing completions"
845
+
"[completion] Could not find signature for subcommand {cmd_name:?}, not showing completions"
753
846
);
754
-
// Leave context as None to show no completions
755
847
return None;
848
+
} else {
849
+
console_log!(
850
+
"[completion] Could not find signature for {cmd_name:?}, showing subcommands"
851
+
);
852
+
return Some(CompletionContext::Command {
853
+
prefix: String::new(),
854
+
span: Span::new(byte_pos, byte_pos),
855
+
parent_command: Some(cmd_first_word),
856
+
});
756
857
}
757
-
} else {
758
-
// Couldn't find signature, don't show completions
759
-
console_log!(
760
-
"[completion] Could not find signature for {cmd_name:?}, not showing completions"
761
-
);
762
-
// Leave context as None to show no completions
763
-
return None;
764
858
}
765
859
} else {
766
860
// Not right after command, complete the command itself
···
768
862
return Some(CompletionContext::Command {
769
863
prefix: cmd.to_string(),
770
864
span: local_span,
865
+
parent_command: None,
771
866
});
772
867
}
773
868
}
···
827
922
Some(CompletionContext::Command {
828
923
prefix: last_word.to_string(),
829
924
span: Span::new(last_word_start, byte_pos),
925
+
parent_command: None,
830
926
})
831
927
} else {
832
928
// Check if this is a variable or cell path (starts with $)
+185
-22
src/completion/suggestions.rs
+185
-22
src/completion/suggestions.rs
···
12
12
working_set: &StateWorkingSet,
13
13
prefix: String,
14
14
span: Span,
15
+
parent_command: Option<String>,
15
16
) -> Vec<Suggestion> {
16
-
console_log!("[completion] Generating Command suggestions with prefix: {prefix:?}");
17
-
// Command completion
18
-
let cmds =
19
-
working_set.find_commands_by_predicate(|value| value.starts_with(prefix.as_bytes()), true);
17
+
console_log!(
18
+
"[completion] Generating Command suggestions with prefix: {prefix:?}, parent_command: {parent_command:?}"
19
+
);
20
20
21
21
let span = to_char_span(input, span);
22
22
let mut suggestions = Vec::new();
23
23
let mut cmd_count = 0;
24
24
25
+
// Determine search prefix and name extraction logic
26
+
let (search_prefix, parent_prefix_opt) = if let Some(parent) = &parent_command {
27
+
// Show only subcommands of the parent command
28
+
// Subcommands are commands that start with "parent_command " (with space)
29
+
let parent_prefix = format!("{} ", parent);
30
+
let search_prefix = if prefix.is_empty() {
31
+
parent_prefix.clone()
32
+
} else {
33
+
format!("{}{}", parent_prefix, prefix)
34
+
};
35
+
(search_prefix, Some(parent_prefix))
36
+
} else {
37
+
// Regular command completion - show all commands
38
+
(prefix.clone(), None)
39
+
};
40
+
41
+
let cmds = working_set
42
+
.find_commands_by_predicate(|value| value.starts_with(search_prefix.as_bytes()), true);
43
+
25
44
for (_, name, desc, _) in cmds {
26
45
let name_str = String::from_utf8_lossy(&name).to_string();
46
+
47
+
// Extract the command name to display
48
+
// For subcommands, extract just the subcommand name (part after "parent_command ")
49
+
// For regular commands, use the full command name
50
+
let display_name = if let Some(parent_prefix) = &parent_prefix_opt {
51
+
if let Some(subcommand_name) = name_str.strip_prefix(parent_prefix) {
52
+
subcommand_name.to_string()
53
+
} else {
54
+
continue; // Skip if it doesn't match the parent prefix
55
+
}
56
+
} else {
57
+
name_str
58
+
};
59
+
27
60
suggestions.push(Suggestion {
28
61
rendered: {
29
-
let name_colored = ansi_term::Color::Green.bold().paint(&name_str);
62
+
let name_colored = ansi_term::Color::Green.bold().paint(&display_name);
30
63
let desc_str = desc.as_deref().unwrap_or("<no description>");
31
64
format!("{name_colored} {desc_str}")
32
65
},
33
-
name: name_str,
34
-
description: desc,
66
+
name: display_name,
67
+
description: desc.map(|d| d.to_string()),
35
68
is_command: true,
36
69
span_start: span.start,
37
70
span_end: span.end,
···
120
153
121
154
// Add short flag if it matches
122
155
if let Some(short) = &short_name {
156
+
let flag_char = flag.short.unwrap_or(' ');
123
157
let should_show_short = if show_all {
124
158
true // Show all flags when prefix is "-" or empty
125
159
} else if prefix.starts_with("-") && !prefix.starts_with("--") {
126
-
short.starts_with(&prefix) // Only show short flags matching prefix
160
+
// For combined short flags like "-a" or "-af", suggest flags that can be appended
161
+
// Extract already used flags from prefix (e.g., "-a" -> ['a'], "-af" -> ['a', 'f'])
162
+
let used_flags: Vec<char> = prefix[1..].chars().collect();
163
+
164
+
// Show if this flag isn't already in the prefix
165
+
!used_flags.contains(&flag_char)
127
166
} else {
128
167
false // Don't show short flags if prefix is long flag format
129
168
};
130
169
131
170
if should_show_short {
132
-
suggestions.push(create_flag_suggestion(short.clone()));
171
+
// If prefix already contains flags (like "-a"), create combined suggestion (like "-af")
172
+
let suggestion_name = if prefix.len() > 1 && prefix.starts_with("-") {
173
+
format!("{}{}", prefix, flag_char)
174
+
} else {
175
+
short.clone()
176
+
};
177
+
suggestions.push(create_flag_suggestion(suggestion_name));
133
178
flag_count += 1;
134
179
}
135
180
}
···
146
191
pub fn generate_command_argument_suggestions(
147
192
input: &str,
148
193
engine_guard: &EngineState,
194
+
working_set: &StateWorkingSet,
149
195
prefix: String,
150
196
span: Span,
151
197
command_name: String,
···
158
204
159
205
let mut suggestions = Vec::new();
160
206
if let Some(signature) = get_command_signature(engine_guard, &command_name) {
207
+
// First, check if we're completing an argument for a flag
208
+
// Look backwards from the current position to find the previous flag
209
+
let text_before = if span.start < input.len() {
210
+
&input[..span.start]
211
+
} else {
212
+
""
213
+
};
214
+
let text_before_trimmed = text_before.trim_end();
215
+
216
+
// Check if the last word before cursor is a flag
217
+
let last_word_start = text_before_trimmed
218
+
.rfind(|c: char| c.is_whitespace())
219
+
.map(|i| i + 1)
220
+
.unwrap_or(0);
221
+
let last_word = &text_before_trimmed[last_word_start..];
222
+
223
+
if last_word.starts_with('-') {
224
+
// We're after a flag - check if this flag accepts an argument
225
+
let flag_name = last_word.trim();
226
+
let is_long_flag = flag_name.starts_with("--");
227
+
let flag_to_match: Option<(bool, String)> = if is_long_flag {
228
+
// Long flag: --flag-name
229
+
flag_name.strip_prefix("--").map(|s| (true, s.to_string()))
230
+
} else {
231
+
// Short flag: -f (single character)
232
+
flag_name
233
+
.strip_prefix("-")
234
+
.and_then(|s| s.chars().next().map(|c| (false, c.to_string())))
235
+
};
236
+
237
+
if let Some((is_long, flag_name_to_match)) = flag_to_match {
238
+
// Find the flag in the signature
239
+
for flag in &signature.named {
240
+
let matches_flag = if is_long {
241
+
// Long flag
242
+
flag.long == flag_name_to_match
243
+
} else {
244
+
// Short flag - compare character
245
+
flag.short
246
+
.map(|c| c.to_string() == flag_name_to_match)
247
+
.unwrap_or(false)
248
+
};
249
+
250
+
if matches_flag {
251
+
// Found the flag - check if it accepts an argument
252
+
if let Some(flag_arg_shape) = &flag.arg {
253
+
// Flag accepts an argument - use its type
254
+
console_log!(
255
+
"[completion] Flag {flag_name:?} accepts argument of type {:?}",
256
+
flag_arg_shape
257
+
);
258
+
match flag_arg_shape {
259
+
nu_protocol::SyntaxShape::Filepath
260
+
| nu_protocol::SyntaxShape::Any => {
261
+
// File/directory completion for flag argument
262
+
let file_suggestions = generate_file_suggestions(
263
+
&prefix,
264
+
span,
265
+
root,
266
+
Some(flag.desc.clone()),
267
+
input,
268
+
);
269
+
let file_count = file_suggestions.len();
270
+
suggestions.extend(file_suggestions);
271
+
console_log!(
272
+
"[completion] Found {file_count} file suggestions for flag argument"
273
+
);
274
+
}
275
+
_ => {
276
+
// Flag argument is not a filepath type
277
+
console_log!(
278
+
"[completion] Flag {flag_name:?} argument is type {:?}, not suggesting files",
279
+
flag_arg_shape
280
+
);
281
+
}
282
+
}
283
+
return suggestions;
284
+
} else {
285
+
// Flag doesn't accept an argument - fall through to positional argument check
286
+
console_log!(
287
+
"[completion] Flag {flag_name:?} doesn't accept an argument, checking positional arguments"
288
+
);
289
+
break;
290
+
}
291
+
}
292
+
}
293
+
}
294
+
}
295
+
296
+
// Not after a flag, or flag doesn't accept an argument - check positional arguments
161
297
// Get positional arguments from signature
162
-
// Combine required and optional positional arguments
163
-
let mut all_positional = Vec::new();
164
-
all_positional.extend_from_slice(&signature.required_positional);
165
-
all_positional.extend_from_slice(&signature.optional_positional);
298
+
// Check if argument is in required or optional positional
299
+
let required_count = signature.required_positional.len();
300
+
let is_optional = arg_index >= required_count;
166
301
167
302
// Find the argument at the given index
168
-
if let Some(arg) = all_positional.get(arg_index) {
303
+
let arg = if arg_index < signature.required_positional.len() {
304
+
signature.required_positional.get(arg_index)
305
+
} else {
306
+
let optional_index = arg_index - required_count;
307
+
signature.optional_positional.get(optional_index)
308
+
};
309
+
310
+
if let Some(arg) = arg {
169
311
// Check the SyntaxShape to determine completion type
170
312
// Only suggest files/dirs for Filepath type (or "any" when type is unknown)
171
313
match &arg.shape {
···
183
325
console_log!(
184
326
"[completion] Found {file_count} file suggestions for argument {arg_index}"
185
327
);
328
+
329
+
// If the argument is optional and of type Any or Filepath, also show subcommands
330
+
if is_optional {
331
+
console_log!(
332
+
"[completion] Argument {arg_index} is optional and of type {:?}, also showing subcommands",
333
+
arg.shape
334
+
);
335
+
let subcommand_suggestions = generate_command_suggestions(
336
+
input,
337
+
working_set,
338
+
prefix.clone(),
339
+
span,
340
+
Some(command_name.clone()),
341
+
);
342
+
let subcommand_count = subcommand_suggestions.len();
343
+
suggestions.extend(subcommand_suggestions);
344
+
console_log!(
345
+
"[completion] Found {subcommand_count} subcommand suggestions"
346
+
);
347
+
}
186
348
}
187
349
_ => {
188
350
// For other types, don't suggest files
···
193
355
}
194
356
}
195
357
} else {
196
-
// Argument index out of range, fall back to file completion
358
+
// Argument index out of range - command doesn't accept that many positional arguments
359
+
// Don't suggest files since we know the type (it's not a valid argument)
197
360
console_log!(
198
-
"[completion] Argument index {arg_index} out of range, using file completion"
361
+
"[completion] Argument index {arg_index} out of range, not suggesting files"
199
362
);
200
-
// Use the same file completion logic as Argument context
201
-
let file_suggestions = generate_file_suggestions(&prefix, span, root, None, input);
202
-
suggestions.extend(file_suggestions);
203
363
}
204
364
} else {
205
365
// No signature found, fall back to file completion
···
333
493
console_log!("context: {context:?}");
334
494
335
495
match context {
336
-
Some(CompletionContext::Command { prefix, span }) => {
337
-
generate_command_suggestions(input, working_set, prefix, span)
338
-
}
496
+
Some(CompletionContext::Command {
497
+
prefix,
498
+
span,
499
+
parent_command,
500
+
}) => generate_command_suggestions(input, working_set, prefix, span, parent_command),
339
501
Some(CompletionContext::Argument { prefix, span }) => {
340
502
generate_argument_suggestions(input, prefix, span, root)
341
503
}
···
352
514
}) => generate_command_argument_suggestions(
353
515
input,
354
516
engine_guard,
517
+
working_set,
355
518
prefix,
356
519
span,
357
520
command_name,