endpoint 2.0 dysnomia.ptr.pet
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 602 lines 24 kB view raw
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(&current_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}