forked from
ptr.pet/faunu
endpoint 2.0
dysnomia.ptr.pet
1use crate::completion::context::get_command_signature;
2use crate::completion::helpers::to_char_span;
3use crate::completion::types::{CompletionContext, CompletionKind, Suggestion};
4use crate::completion::variables::*;
5use crate::console_log;
6use nu_protocol::Span;
7use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
8use std::collections::HashSet;
9
10pub fn generate_command_suggestions(
11 input: &str,
12 working_set: &StateWorkingSet,
13 prefix: String,
14 span: Span,
15 parent_command: Option<String>,
16) -> Vec<Suggestion> {
17 console_log!(
18 "[completion] Generating Command suggestions with prefix: {prefix:?}, parent_command: {parent_command:?}"
19 );
20
21 let span = to_char_span(input, span);
22 let mut suggestions = Vec::new();
23 let mut cmd_count = 0;
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
44 for (_, name, desc, _) in cmds {
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
60 suggestions.push(Suggestion {
61 rendered: {
62 let name_colored = ansi_term::Color::Green.bold().paint(&display_name);
63 let desc_str = desc.as_deref().unwrap_or("<no description>");
64 format!("{name_colored} {desc_str}")
65 },
66 name: display_name,
67 description: desc.map(|d| d.to_string()),
68 span_start: span.start,
69 span_end: span.end,
70 });
71 cmd_count += 1;
72 }
73 console_log!("[completion] Found {cmd_count} command suggestions");
74 suggestions.sort();
75 suggestions
76}
77
78pub fn generate_argument_suggestions(
79 input: &str,
80 prefix: String,
81 span: Span,
82 root: &std::sync::Arc<vfs::VfsPath>,
83) -> Vec<Suggestion> {
84 console_log!("[completion] Generating Argument suggestions with prefix: {prefix:?}");
85 // File completion
86 let mut file_suggestions = generate_file_suggestions(&prefix, span, root, None, input);
87 console_log!(
88 "[completion] Found {file_count} file suggestions",
89 file_count = file_suggestions.len()
90 );
91 file_suggestions.sort();
92 file_suggestions
93}
94
95pub fn generate_flag_suggestions(
96 input: &str,
97 engine_guard: &EngineState,
98 prefix: String,
99 span: Span,
100 command_name: String,
101) -> Vec<Suggestion> {
102 console_log!(
103 "[completion] Generating Flag suggestions for command: {command_name:?}, prefix: {prefix:?}"
104 );
105
106 let mut suggestions = Vec::new();
107 if let Some(signature) = get_command_signature(engine_guard, &command_name) {
108 let span = to_char_span(input, span);
109 let mut flag_count = 0;
110
111 // Get switches from signature
112 // Signature has a named field that contains named arguments (including switches)
113 for flag in &signature.named {
114 // Check if this is a switch (has no argument)
115 // Switches have arg: None, named arguments have arg: Some(SyntaxShape)
116 let is_switch = flag.arg.is_none();
117
118 if is_switch {
119 let long_name = format!("--{}", flag.long);
120 let short_name = flag.short.map(|c| format!("-{}", c));
121
122 // Determine which flags to show based on prefix:
123 // - If prefix is empty or exactly "-", show all flags (both short and long)
124 // - If prefix starts with "--", only show long flags that match the prefix
125 // - If prefix starts with "-" (but not "--"), only show short flags that match the prefix
126 let show_all = prefix.is_empty() || prefix == "-";
127
128 // Helper to create a flag suggestion
129 let create_flag_suggestion = |flag_name: String| -> Suggestion {
130 Suggestion {
131 name: flag_name.clone(),
132 description: Some(flag.desc.clone()),
133 rendered: {
134 let flag_colored = ansi_term::Color::Cyan.bold().paint(&flag_name);
135 format!("{flag_colored} {}", flag.desc)
136 },
137 span_start: span.start,
138 span_end: span.end,
139 }
140 };
141
142 // Add long flag if it matches
143 let should_show_long = if show_all {
144 true // Show all flags when prefix is "-" or empty
145 } else if prefix.starts_with("--") {
146 long_name.starts_with(&prefix) // Only show long flags matching prefix
147 } else {
148 false // Don't show long flags if prefix is short flag format
149 };
150
151 if should_show_long {
152 suggestions.push(create_flag_suggestion(long_name));
153 flag_count += 1;
154 }
155
156 // Add short flag if it matches
157 if let Some(short) = &short_name {
158 let flag_char = flag.short.unwrap_or(' ');
159 let should_show_short = if show_all {
160 true // Show all flags when prefix is "-" or empty
161 } else if prefix.starts_with("-") && !prefix.starts_with("--") {
162 // For combined short flags like "-a" or "-af", suggest flags that can be appended
163 // Extract already used flags from prefix (e.g., "-a" -> ['a'], "-af" -> ['a', 'f'])
164 let used_flags: Vec<char> = prefix[1..].chars().collect();
165
166 // Show if this flag isn't already in the prefix
167 !used_flags.contains(&flag_char)
168 } else {
169 false // Don't show short flags if prefix is long flag format
170 };
171
172 if should_show_short {
173 // If prefix already contains flags (like "-a"), create combined suggestion (like "-af")
174 let suggestion_name = if prefix.len() > 1 && prefix.starts_with("-") {
175 format!("{}{}", prefix, flag_char)
176 } else {
177 short.clone()
178 };
179 suggestions.push(create_flag_suggestion(suggestion_name));
180 flag_count += 1;
181 }
182 }
183 }
184 }
185
186 console_log!("[completion] Found {flag_count} flag suggestions");
187 } else {
188 console_log!("[completion] Could not find signature for command: {command_name:?}");
189 }
190 suggestions.sort();
191 suggestions
192}
193
194pub fn generate_command_argument_suggestions(
195 input: &str,
196 engine_guard: &EngineState,
197 _working_set: &StateWorkingSet,
198 prefix: String,
199 span: Span,
200 command_name: String,
201 arg_index: usize,
202 root: &std::sync::Arc<vfs::VfsPath>,
203) -> Vec<Suggestion> {
204 console_log!(
205 "[completion] Generating CommandArgument suggestions for command: {command_name:?}, arg_index: {arg_index}, prefix: {prefix:?}"
206 );
207
208 let mut suggestions = Vec::new();
209
210 if let Some(signature) = get_command_signature(engine_guard, &command_name) {
211 // First, check if we're completing an argument for a flag
212 // Look backwards from the current position to find the previous flag
213 let text_before = if span.start < input.len() {
214 &input[..span.start]
215 } else {
216 ""
217 };
218 let text_before_trimmed = text_before.trim_end();
219
220 // Check if the last word before cursor is a flag
221 let last_word_start = text_before_trimmed
222 .rfind(|c: char| c.is_whitespace())
223 .map(|i| i + 1)
224 .unwrap_or(0);
225 let last_word = &text_before_trimmed[last_word_start..];
226
227 if last_word.starts_with('-') {
228 // We're after a flag - check if this flag accepts an argument
229 let flag_name = last_word.trim();
230 let is_long_flag = flag_name.starts_with("--");
231 let flag_to_match: Option<(bool, String)> = if is_long_flag {
232 // Long flag: --flag-name
233 flag_name.strip_prefix("--").map(|s| (true, s.to_string()))
234 } else {
235 // Short flag: -f (single character)
236 flag_name
237 .strip_prefix("-")
238 .and_then(|s| s.chars().next().map(|c| (false, c.to_string())))
239 };
240
241 if let Some((is_long, flag_name_to_match)) = flag_to_match {
242 // Find the flag in the signature
243 for flag in &signature.named {
244 let matches_flag = if is_long {
245 // Long flag
246 flag.long == flag_name_to_match
247 } else {
248 // Short flag - compare character
249 flag.short
250 .map(|c| c.to_string() == flag_name_to_match)
251 .unwrap_or(false)
252 };
253
254 if matches_flag {
255 // Found the flag - check if it accepts an argument
256 if let Some(flag_arg_shape) = &flag.arg {
257 // Flag accepts an argument - use its type
258 console_log!(
259 "[completion] Flag {flag_name:?} accepts argument of type {:?}",
260 flag_arg_shape
261 );
262 let mut add_file_suggestions = || {
263 let file_suggestions = generate_file_suggestions(
264 &prefix,
265 span,
266 root,
267 Some(flag.desc.clone()),
268 input,
269 );
270 let file_count = file_suggestions.len();
271 suggestions.extend(file_suggestions);
272 console_log!(
273 "[completion] Found {file_count} file suggestions for flag argument"
274 );
275 };
276 match flag_arg_shape {
277 nu_protocol::SyntaxShape::Filepath
278 | nu_protocol::SyntaxShape::Any => {
279 add_file_suggestions();
280 }
281 nu_protocol::SyntaxShape::OneOf(l)
282 if l.contains(&nu_protocol::SyntaxShape::Filepath) =>
283 {
284 add_file_suggestions();
285 }
286 _ => {
287 // Flag argument is not a filepath type
288 console_log!(
289 "[completion] Flag {flag_name:?} argument is type {:?}, not suggesting files",
290 flag_arg_shape
291 );
292 }
293 }
294 return suggestions;
295 } else {
296 // Flag doesn't accept an argument - fall through to positional argument check
297 console_log!(
298 "[completion] Flag {flag_name:?} doesn't accept an argument, checking positional arguments"
299 );
300 break;
301 }
302 }
303 }
304 }
305 }
306
307 // Not after a flag, or flag doesn't accept an argument - check positional arguments
308 // Get positional arguments from signature
309 // Check if argument is in required or optional positional
310 let required_count = signature.required_positional.len();
311
312 // Find the argument at the given index
313 let arg = if arg_index < signature.required_positional.len() {
314 signature.required_positional.get(arg_index)
315 } else {
316 let optional_index = arg_index - required_count;
317 signature.optional_positional.get(optional_index)
318 };
319
320 if let Some(arg) = arg {
321 let mut add_file_suggestions = || {
322 let file_suggestions =
323 generate_file_suggestions(&prefix, span, root, Some(arg.desc.clone()), input);
324 let file_count = file_suggestions.len();
325 suggestions.extend(file_suggestions);
326 console_log!(
327 "[completion] Found {file_count} file suggestions for argument {arg_index}"
328 );
329 };
330
331 match &arg.shape {
332 nu_protocol::SyntaxShape::Filepath | nu_protocol::SyntaxShape::Any => {
333 add_file_suggestions();
334 }
335 nu_protocol::SyntaxShape::OneOf(l)
336 if l.contains(&nu_protocol::SyntaxShape::Filepath) =>
337 {
338 add_file_suggestions();
339 }
340 _ => {
341 // For other types, don't suggest files
342 console_log!(
343 "[completion] Argument {arg_index} is type {:?}, not suggesting files",
344 arg.shape
345 );
346 }
347 }
348 } else {
349 // Argument index out of range - command doesn't accept that many positional arguments
350 // Don't suggest files since we know the type (it's not a valid argument)
351 console_log!(
352 "[completion] Argument index {arg_index} out of range, not suggesting files"
353 );
354 }
355 } else {
356 // No signature found, fall back to file completion
357 console_log!(
358 "[completion] Could not find signature for command: {command_name:?}, using file completion"
359 );
360 let file_suggestions = generate_file_suggestions(&prefix, span, root, None, input);
361 suggestions.extend(file_suggestions);
362 }
363 suggestions.sort();
364 suggestions
365}
366
367pub fn generate_variable_suggestions(
368 input: &str,
369 working_set: &StateWorkingSet,
370 prefix: String,
371 span: Span,
372 byte_pos: usize,
373) -> Vec<Suggestion> {
374 console_log!("[completion] Generating Variable suggestions with prefix: {prefix:?}");
375
376 // Collect all available variables
377 let variables = collect_variables(working_set, input, byte_pos);
378 let span = to_char_span(input, span);
379 let mut suggestions = Vec::new();
380 let mut var_count = 0;
381
382 for (var_name, var_id) in variables {
383 // Filter by prefix (variable name includes $, so we need to check after $)
384 if var_name.len() > 1 && var_name[1..].starts_with(&prefix) {
385 // Get variable type
386 let var_type = working_set.get_variable(var_id).ty.to_string();
387
388 suggestions.push(Suggestion {
389 name: var_name.clone(),
390 description: Some(var_type.clone()),
391 rendered: {
392 let var_colored = ansi_term::Color::Blue.bold().paint(&var_name);
393 format!("{var_colored} {var_type}")
394 },
395 span_start: span.start,
396 span_end: span.end,
397 });
398 var_count += 1;
399 }
400 }
401
402 console_log!("[completion] Found {var_count} variable suggestions");
403 suggestions.sort();
404 suggestions
405}
406
407pub fn generate_cell_path_suggestions(
408 input: &str,
409 working_set: &StateWorkingSet,
410 engine_guard: &EngineState,
411 stack_guard: &Stack,
412 prefix: String,
413 span: Span,
414 var_id: nu_protocol::VarId,
415 path_so_far: Vec<String>,
416) -> Vec<Suggestion> {
417 console_log!(
418 "[completion] Generating CellPath suggestions with prefix: {prefix:?}, path: {path_so_far:?}"
419 );
420
421 let mut suggestions = Vec::new();
422 // Evaluate the variable to get its value
423 if let Some(var_value) =
424 eval_variable_for_completion(var_id, working_set, engine_guard, stack_guard)
425 {
426 // Follow the path to get the value at the current level
427 let current_value = if path_so_far.is_empty() {
428 var_value
429 } else {
430 let path_refs: Vec<&str> = path_so_far.iter().map(|s| s.as_str()).collect();
431 follow_cell_path(&var_value, &path_refs).unwrap_or(var_value)
432 };
433
434 // Get columns/fields from the current value
435 let columns = get_columns_from_value(¤t_value);
436 let span = to_char_span(input, span);
437 let mut field_count = 0;
438
439 for (col_name, col_type) in columns {
440 // Filter by prefix
441 if col_name.starts_with(&prefix) {
442 let type_str = col_type.as_deref().unwrap_or("any");
443 suggestions.push(Suggestion {
444 name: col_name.clone(),
445 description: Some(type_str.to_string()),
446 rendered: {
447 let col_colored = ansi_term::Color::Yellow.paint(&col_name);
448 format!("{col_colored} {type_str}")
449 },
450 span_start: span.start,
451 span_end: span.end,
452 });
453 field_count += 1;
454 }
455 }
456
457 console_log!("[completion] Found {field_count} cell path suggestions");
458 } else {
459 // Variable couldn't be evaluated - this is expected for runtime variables
460 // We can't provide cell path completions without knowing the structure
461 console_log!(
462 "[completion] Could not evaluate variable {var_id:?} for cell path completion (runtime variable)"
463 );
464
465 // Try to get type information to provide better feedback
466 if let Ok(var_info) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
467 working_set.get_variable(var_id)
468 })) {
469 console_log!("[completion] Variable type: {ty:?}", ty = var_info.ty);
470 }
471 }
472 suggestions.sort();
473 suggestions
474}
475
476pub fn generate_file_suggestions(
477 prefix: &str,
478 span: Span,
479 root: &std::sync::Arc<vfs::VfsPath>,
480 description: Option<String>,
481 input: &str,
482) -> Vec<Suggestion> {
483 let (dir, file_prefix) = prefix
484 .rfind('/')
485 .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
486 .unwrap_or(("", prefix));
487
488 let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
489 .then(|| &dir[..dir.len() - 1])
490 .unwrap_or(dir);
491
492 let target_dir = if !dir.is_empty() {
493 match root.join(dir_to_join) {
494 Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
495 _ => None,
496 }
497 } else {
498 Some(root.join("").unwrap())
499 };
500
501 let mut file_suggestions = Vec::new();
502 if let Some(d) = target_dir {
503 if let Ok(iterator) = d.read_dir() {
504 let char_span = to_char_span(input, span);
505 for entry in iterator {
506 let name = entry.filename();
507 if name.starts_with(file_prefix) {
508 let full_completion = format!("{}{}", dir, name);
509 file_suggestions.push(Suggestion {
510 name: full_completion.clone(),
511 description: description.clone(),
512 rendered: full_completion,
513 span_start: char_span.start,
514 span_end: char_span.end,
515 });
516 }
517 }
518 }
519 }
520 file_suggestions
521}
522
523pub fn generate_suggestions(
524 input: &str,
525 contexts: HashSet<CompletionContext>,
526 working_set: &StateWorkingSet,
527 engine_guard: &EngineState,
528 stack_guard: &Stack,
529 root: &std::sync::Arc<vfs::VfsPath>,
530 byte_pos: usize,
531) -> Vec<Suggestion> {
532 console_log!("contexts: {contexts:?}");
533
534 let mut context_vec: Vec<_> = contexts.into_iter().collect();
535 context_vec.sort_by_key(|ctx| match &ctx.kind {
536 CompletionKind::Command { .. } => 0,
537 CompletionKind::Flag { .. } => 1,
538 CompletionKind::Variable => 2,
539 CompletionKind::CellPath { .. } => 3,
540 CompletionKind::CommandArgument { .. } => 4,
541 CompletionKind::Argument => 5,
542 });
543
544 let mut suggestions = Vec::new();
545 for context in context_vec.iter() {
546 let mut sug = match &context.kind {
547 CompletionKind::Command { parent_command } => generate_command_suggestions(
548 input,
549 working_set,
550 context.prefix.clone(),
551 context.span,
552 parent_command.clone(),
553 ),
554 CompletionKind::Argument => {
555 generate_argument_suggestions(input, context.prefix.clone(), context.span, root)
556 }
557 CompletionKind::Flag { command_name } => generate_flag_suggestions(
558 input,
559 engine_guard,
560 context.prefix.clone(),
561 context.span,
562 command_name.clone(),
563 ),
564 CompletionKind::CommandArgument {
565 command_name,
566 arg_index,
567 } => generate_command_argument_suggestions(
568 input,
569 engine_guard,
570 working_set,
571 context.prefix.clone(),
572 context.span,
573 command_name.clone(),
574 *arg_index,
575 root,
576 ),
577 CompletionKind::Variable => generate_variable_suggestions(
578 input,
579 working_set,
580 context.prefix.clone(),
581 context.span,
582 byte_pos,
583 ),
584 CompletionKind::CellPath {
585 var_id,
586 path_so_far,
587 } => generate_cell_path_suggestions(
588 input,
589 working_set,
590 engine_guard,
591 stack_guard,
592 context.prefix.clone(),
593 context.span,
594 *var_id,
595 path_so_far.clone(),
596 ),
597 };
598 suggestions.append(&mut sug);
599 }
600
601 suggestions
602}