at main 51 kB view raw
1use crate::completion::helpers::*; 2use crate::completion::types::{CompletionContext, CompletionKind}; 3use crate::console_log; 4use nu_parser::FlatShape; 5use nu_protocol::engine::{EngineState, StateWorkingSet}; 6use nu_protocol::{Signature, Span}; 7 8pub fn find_command_and_arg_index( 9 input: &str, 10 shapes: &[(Span, FlatShape)], 11 current_idx: usize, 12 current_local_span: Span, 13 global_offset: usize, 14) -> Option<(String, usize)> { 15 let mut command_name: Option<String> = None; 16 let mut arg_count = 0; 17 18 // Look backwards through shapes to find the command 19 for i in (0..current_idx).rev() { 20 if let Some((prev_span, prev_shape)) = shapes.get(i) { 21 let prev_local_span = to_local_span(*prev_span, global_offset); 22 23 // Check if there's a separator between this shape and the next one 24 let next_shape_start = if i + 1 < shapes.len() { 25 to_local_span(shapes[i + 1].0, global_offset).start 26 } else { 27 current_local_span.start 28 }; 29 30 if has_separator_between(input, prev_local_span.end, next_shape_start) { 31 break; // Stop at separator 32 } 33 34 if is_command_shape(input, prev_shape, prev_local_span) { 35 // Found the command 36 let cmd_text = safe_slice(input, prev_local_span); 37 let cmd_name = extract_command_name(cmd_text); 38 command_name = Some(cmd_name.to_string()); 39 break; 40 } else { 41 // This is an argument - count it if it's not a flag 42 let arg_text = safe_slice(input, prev_local_span); 43 let trimmed_arg = arg_text.trim(); 44 // Don't count flags (starting with -) or empty arguments 45 if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') { 46 arg_count += 1; 47 } 48 } 49 } 50 } 51 52 command_name.map(|name| (name, arg_count)) 53} 54 55pub fn build_command_prefix( 56 input: &str, 57 shapes: &[(Span, FlatShape)], 58 current_idx: usize, 59 current_local_span: Span, 60 current_prefix: &str, 61 global_offset: usize, 62) -> (String, Span) { 63 let mut span_start = current_local_span.start; 64 65 // Look backwards through shapes to find previous command words 66 for i in (0..current_idx).rev() { 67 if let Some((prev_span, prev_shape)) = shapes.get(i) { 68 let prev_local_span = to_local_span(*prev_span, global_offset); 69 70 if is_command_shape(input, prev_shape, prev_local_span) { 71 // Check if there's a separator between this shape and the next one 72 let next_shape_start = if i + 1 < shapes.len() { 73 to_local_span(shapes[i + 1].0, global_offset).start 74 } else { 75 current_local_span.start 76 }; 77 78 // Check if there's a separator (pipe, semicolon, etc.) between shapes 79 // Whitespace is fine, but separators indicate a new command 80 if has_separator_between(input, prev_local_span.end, next_shape_start) { 81 break; // Stop at separator 82 } 83 84 // Update span start to include this command word 85 span_start = prev_local_span.start; 86 } else { 87 // Not a command shape, stop looking backwards 88 break; 89 } 90 } 91 } 92 93 // Extract the full prefix from the input, preserving exact spacing 94 let span_end = current_local_span.end; 95 let full_prefix = if span_start < input.len() { 96 safe_slice(input, Span::new(span_start, span_end)).to_string() 97 } else { 98 current_prefix.to_string() 99 }; 100 101 (full_prefix, Span::new(span_start, span_end)) 102} 103 104pub fn get_command_signature(engine_guard: &EngineState, cmd_name: &str) -> Option<Signature> { 105 engine_guard 106 .find_decl(cmd_name.as_bytes(), &[]) 107 .map(|id| engine_guard.get_decl(id).signature()) 108} 109 110/// Creates CommandArgument context(s), and optionally adds a Command context for subcommands 111/// if we're at argument index 0 and the command has subcommands. 112pub fn create_command_argument_contexts( 113 command_name: String, 114 arg_index: usize, 115 prefix: String, 116 span: Span, 117 working_set: &StateWorkingSet, 118 _engine_guard: &EngineState, 119) -> Vec<CompletionContext> { 120 let mut contexts = Vec::new(); 121 122 // Always add the CommandArgument context 123 contexts.push(CompletionContext { 124 kind: CompletionKind::CommandArgument { 125 command_name: command_name.clone(), 126 arg_index, 127 }, 128 prefix: prefix.clone(), 129 span, 130 }); 131 132 // If we're at argument index 0, check if the command has subcommands 133 if arg_index == 0 { 134 // Check if command has subcommands 135 // Subcommands are commands that start with "command_name " (with space) 136 let parent_prefix = format!("{} ", command_name); 137 let subcommands = working_set 138 .find_commands_by_predicate(|value| value.starts_with(parent_prefix.as_bytes()), true); 139 140 if !subcommands.is_empty() { 141 // Command has subcommands - add a Command context for subcommands 142 console_log!( 143 "[completion] Command {command_name:?} has subcommands, adding Command context for subcommands" 144 ); 145 contexts.push(CompletionContext { 146 kind: CompletionKind::Command { 147 parent_command: Some(command_name), 148 }, 149 prefix, 150 span, 151 }); 152 } 153 } 154 155 contexts 156} 157 158pub fn determine_flag_or_argument_context( 159 input: &str, 160 shapes: &[(Span, FlatShape)], 161 prefix: &str, 162 idx: usize, 163 local_span: Span, 164 span: Span, 165 global_offset: usize, 166 working_set: &StateWorkingSet, 167 _engine_guard: &EngineState, 168) -> Vec<CompletionContext> { 169 let trimmed_prefix = prefix.trim(); 170 if trimmed_prefix.starts_with('-') { 171 // This looks like a flag - find the command 172 if let Some((cmd_name, _)) = 173 find_command_and_arg_index(input, shapes, idx, local_span, global_offset) 174 { 175 vec![CompletionContext { 176 kind: CompletionKind::Flag { 177 command_name: cmd_name, 178 }, 179 prefix: trimmed_prefix.to_string(), 180 span, 181 }] 182 } else { 183 vec![CompletionContext { 184 kind: CompletionKind::Argument, 185 prefix: prefix.to_string(), 186 span, 187 }] 188 } 189 } else { 190 // This is a positional argument - find the command and argument index 191 if let Some((cmd_name, arg_index)) = 192 find_command_and_arg_index(input, shapes, idx, local_span, global_offset) 193 { 194 create_command_argument_contexts( 195 cmd_name, 196 arg_index, 197 trimmed_prefix.to_string(), 198 span, 199 working_set, 200 _engine_guard, 201 ) 202 } else { 203 vec![CompletionContext { 204 kind: CompletionKind::Argument, 205 prefix: prefix.to_string(), 206 span, 207 }] 208 } 209 } 210} 211 212pub fn handle_block_or_closure( 213 input: &str, 214 shapes: &[(Span, FlatShape)], 215 working_set: &StateWorkingSet, 216 engine_guard: &EngineState, 217 prefix: &str, 218 span: Span, 219 shape_name: &str, 220 current_idx: usize, 221 local_span: Span, 222 global_offset: usize, 223) -> Vec<CompletionContext> { 224 console_log!("[completion] Processing {shape_name} shape with prefix: {prefix:?}"); 225 226 // Check if the content ends with a pipe or semicolon 227 let prefix_ends_with_separator = ends_with_separator(prefix); 228 let last_sep_pos_in_prefix = if prefix_ends_with_separator { 229 find_last_separator_pos(prefix) 230 } else { 231 None 232 }; 233 console_log!( 234 "[completion] {shape_name}: prefix_ends_with_separator={prefix_ends_with_separator}, last_sep_pos_in_prefix={last_sep_pos_in_prefix:?}" 235 ); 236 237 if let Some((trimmed_prefix, adjusted_span, is_empty)) = handle_block_prefix(prefix, span) { 238 console_log!( 239 "[completion] {shape_name}: trimmed_prefix={trimmed_prefix:?}, is_empty={is_empty}" 240 ); 241 242 if is_empty { 243 // Empty block/closure or just whitespace 244 // Check if there's a command shape before this closure/block shape 245 // If so, we might be completing after that command 246 let mut found_command: Option<String> = None; 247 for i in (0..current_idx).rev() { 248 if let Some((prev_span, prev_shape)) = shapes.get(i) { 249 let prev_local_span = to_local_span(*prev_span, global_offset); 250 // Check if this shape is before the current closure and is a command 251 if prev_local_span.end <= local_span.start { 252 if is_command_shape(input, prev_shape, prev_local_span) { 253 let cmd_text = safe_slice(input, prev_local_span); 254 let cmd_full = cmd_text.trim().to_string(); 255 256 // Extract the full command text - if it contains spaces, it might be a subcommand 257 // We'll use the first word for parent_command to show subcommands 258 // The suggestion generator will filter appropriately 259 let cmd_first_word = extract_command_name(cmd_text).to_string(); 260 261 // If the command contains spaces, it's likely a full command (subcommand) 262 // In that case, we shouldn't show subcommands 263 if cmd_full.contains(' ') && cmd_full != cmd_first_word { 264 // It's a full command (subcommand), don't show subcommands 265 console_log!( 266 "[completion] {shape_name} is empty but found full command {cmd_full:?} before it, not showing completions" 267 ); 268 return Vec::new(); 269 } 270 271 // Use the first word to show subcommands 272 found_command = Some(cmd_first_word); 273 console_log!( 274 "[completion] {shape_name} is empty but found command {found_command:?} before it" 275 ); 276 break; 277 } 278 } 279 } 280 } 281 282 if let Some(cmd_name) = found_command { 283 // We found a command before the closure, show subcommands of that command 284 console_log!( 285 "[completion] {shape_name} is empty, showing subcommands of {cmd_name:?}" 286 ); 287 vec![CompletionContext { 288 kind: CompletionKind::Command { 289 parent_command: Some(cmd_name), 290 }, 291 prefix: String::new(), 292 span: adjusted_span, 293 }] 294 } else { 295 // Truly empty - show all commands 296 console_log!("[completion] {shape_name} is empty, setting Command context"); 297 vec![CompletionContext { 298 kind: CompletionKind::Command { 299 parent_command: None, 300 }, 301 prefix: String::new(), 302 span: adjusted_span, 303 }] 304 } 305 } else if let Some(last_sep_pos) = last_sep_pos_in_prefix { 306 // After a separator - command context 307 let after_sep = prefix[last_sep_pos..].trim_start(); 308 console_log!( 309 "[completion] {shape_name} has separator at {last_sep_pos}, after_sep={after_sep:?}, setting Command context" 310 ); 311 vec![CompletionContext { 312 kind: CompletionKind::Command { 313 parent_command: None, 314 }, 315 prefix: after_sep.to_string(), 316 span: Span::new(span.start + last_sep_pos, span.end), 317 }] 318 } else { 319 console_log!( 320 "[completion] {shape_name} has no separator, checking for variable/flag/argument context" 321 ); 322 // Check if this is a variable or cell path first 323 let trimmed = trimmed_prefix.trim(); 324 325 if trimmed.starts_with('$') { 326 // Variable or cell path completion 327 if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed) { 328 let var_id = lookup_variable_id(var_name, working_set); 329 330 if let Some(var_id) = var_id { 331 let prefix_byte_len = cell_prefix.len(); 332 let cell_span_start = adjusted_span.end.saturating_sub(prefix_byte_len); 333 console_log!( 334 "[completion] {shape_name}: Setting CellPath context with var {var_name:?}, prefix {cell_prefix:?}" 335 ); 336 vec![CompletionContext { 337 kind: CompletionKind::CellPath { 338 var_id, 339 path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(), 340 }, 341 prefix: cell_prefix.to_string(), 342 span: Span::new(cell_span_start, adjusted_span.end), 343 }] 344 } else { 345 // Unknown variable, fall back to variable completion 346 let var_prefix = trimmed[1..].to_string(); 347 console_log!( 348 "[completion] {shape_name}: Unknown var, setting Variable context with prefix {var_prefix:?}" 349 ); 350 vec![CompletionContext { 351 kind: CompletionKind::Variable, 352 prefix: var_prefix, 353 span: adjusted_span, 354 }] 355 } 356 } else { 357 // Simple variable completion (no dot) 358 let var_prefix = if trimmed.len() > 1 { 359 trimmed[1..].to_string() 360 } else { 361 String::new() 362 }; 363 console_log!( 364 "[completion] {shape_name}: Setting Variable context with prefix {var_prefix:?}" 365 ); 366 vec![CompletionContext { 367 kind: CompletionKind::Variable, 368 prefix: var_prefix, 369 span: adjusted_span, 370 }] 371 } 372 } else if trimmed.starts_with('-') { 373 // Flag completion 374 if let Some((cmd_name, _)) = find_command_and_arg_index( 375 input, 376 shapes, 377 current_idx, 378 local_span, 379 global_offset, 380 ) { 381 console_log!( 382 "[completion] {shape_name}: Found command {cmd_name:?} for flag completion" 383 ); 384 vec![CompletionContext { 385 kind: CompletionKind::Flag { 386 command_name: cmd_name, 387 }, 388 prefix: trimmed.to_string(), 389 span: adjusted_span, 390 }] 391 } else { 392 vec![CompletionContext { 393 kind: CompletionKind::Argument, 394 prefix: trimmed_prefix.to_string(), 395 span: adjusted_span, 396 }] 397 } 398 } else { 399 // Try to find the command and argument index 400 if let Some((cmd_name, arg_index)) = find_command_and_arg_index( 401 input, 402 shapes, 403 current_idx, 404 local_span, 405 global_offset, 406 ) { 407 console_log!( 408 "[completion] {shape_name}: Found command {cmd_name:?} with arg_index {arg_index} for argument completion" 409 ); 410 create_command_argument_contexts( 411 cmd_name, 412 arg_index, 413 trimmed.to_string(), 414 adjusted_span, 415 working_set, 416 engine_guard, 417 ) 418 } else { 419 // No command found, treat as regular argument 420 console_log!( 421 "[completion] {shape_name}: No command found, using Argument context" 422 ); 423 vec![CompletionContext { 424 kind: CompletionKind::Argument, 425 prefix: trimmed_prefix.to_string(), 426 span: adjusted_span, 427 }] 428 } 429 } 430 } 431 } else { 432 Vec::new() 433 } 434} 435 436pub fn handle_variable_string_shape( 437 input: &str, 438 shapes: &[(Span, FlatShape)], 439 working_set: &StateWorkingSet, 440 engine_guard: &EngineState, 441 idx: usize, 442 prefix: &str, 443 span: Span, 444 local_span: Span, 445 global_offset: usize, 446) -> Vec<CompletionContext> { 447 if idx == 0 { 448 return Vec::new(); 449 } 450 451 let prev_shape = &shapes[idx - 1]; 452 let prev_local_span = to_local_span(prev_shape.0, global_offset); 453 454 if let FlatShape::Variable(var_id) = prev_shape.1 { 455 // Check if the variable shape ends right where this shape starts (or very close) 456 // Allow for a small gap (like a dot) between shapes 457 let gap = local_span.start.saturating_sub(prev_local_span.end); 458 if gap <= 1 { 459 // This is a cell path - the String shape contains the field name(s) 460 // The prefix might be like "na" or "field.subfield" 461 let trimmed_prefix = prefix.trim(); 462 let (path_so_far, cell_prefix) = parse_cell_path_from_fields(trimmed_prefix); 463 464 let prefix_byte_len = cell_prefix.len(); 465 let cell_span_start = span.end.saturating_sub(prefix_byte_len); 466 console_log!( 467 "[completion] Detected cell path from Variable+String shapes, var_id={var_id:?}, prefix={cell_prefix:?}, path={path_so_far:?}" 468 ); 469 vec![CompletionContext { 470 kind: CompletionKind::CellPath { 471 var_id, 472 path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(), 473 }, 474 prefix: cell_prefix.to_string(), 475 span: Span::new(cell_span_start, span.end), 476 }] 477 } else { 478 // Gap between shapes, use helper to determine context 479 determine_flag_or_argument_context( 480 input, 481 shapes, 482 &prefix.trim(), 483 idx, 484 local_span, 485 span, 486 global_offset, 487 working_set, 488 engine_guard, 489 ) 490 } 491 } else { 492 // Previous shape is not a Variable, use helper to determine context 493 determine_flag_or_argument_context( 494 input, 495 shapes, 496 &prefix.trim(), 497 idx, 498 local_span, 499 span, 500 global_offset, 501 working_set, 502 engine_guard, 503 ) 504 } 505} 506 507pub fn handle_dot_shape( 508 _input: &str, 509 shapes: &[(Span, FlatShape)], 510 idx: usize, 511 prefix: &str, 512 span: Span, 513 local_span: Span, 514 global_offset: usize, 515) -> Vec<CompletionContext> { 516 if idx == 0 { 517 return vec![CompletionContext { 518 kind: CompletionKind::Argument, 519 prefix: prefix.to_string(), 520 span, 521 }]; 522 } 523 524 let prev_shape = &shapes[idx - 1]; 525 let prev_local_span = to_local_span(prev_shape.0, global_offset); 526 527 if let FlatShape::Variable(var_id) = prev_shape.1 { 528 // Check if the variable shape ends right where this shape starts 529 if prev_local_span.end == local_span.start { 530 let trimmed_prefix = prefix.trim(); 531 // Parse path members from the prefix (which is like ".field" or ".field.subfield") 532 let after_dot = &trimmed_prefix[1..]; // Remove leading dot 533 let (path_so_far, cell_prefix) = if after_dot.is_empty() { 534 (vec![], "") 535 } else { 536 parse_cell_path_from_fields(after_dot) 537 }; 538 539 let prefix_byte_len = cell_prefix.len(); 540 let cell_span_start = span.end.saturating_sub(prefix_byte_len); 541 console_log!( 542 "[completion] Detected cell path from adjacent Variable shape, var_id={var_id:?}, prefix={cell_prefix:?}" 543 ); 544 vec![CompletionContext { 545 kind: CompletionKind::CellPath { 546 var_id, 547 path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(), 548 }, 549 prefix: cell_prefix.to_string(), 550 span: Span::new(cell_span_start, span.end), 551 }] 552 } else { 553 // Gap between shapes, fall through to default handling 554 vec![CompletionContext { 555 kind: CompletionKind::Argument, 556 prefix: prefix.to_string(), 557 span, 558 }] 559 } 560 } else { 561 // Previous shape is not a Variable, this is likely a file path starting with . 562 vec![CompletionContext { 563 kind: CompletionKind::Argument, 564 prefix: prefix.to_string(), 565 span, 566 }] 567 } 568} 569 570pub fn determine_context_from_shape( 571 input: &str, 572 shapes: &[(Span, FlatShape)], 573 working_set: &StateWorkingSet, 574 engine_guard: &EngineState, 575 byte_pos: usize, 576 global_offset: usize, 577) -> Vec<CompletionContext> { 578 // First, check if cursor is within a shape 579 for (idx, (span, shape)) in shapes.iter().enumerate() { 580 let local_span = to_local_span(*span, global_offset); 581 582 if local_span.start <= byte_pos && byte_pos <= local_span.end { 583 console_log!("[completion] Cursor in shape {idx}: {shape:?} at {local_span:?}"); 584 585 // Check if there's a pipe or semicolon between this shape's end and the cursor 586 // If so, we're starting a new command and should ignore this shape 587 let has_sep = has_separator_between(input, local_span.end, byte_pos); 588 if has_sep { 589 console_log!( 590 "[completion] Separator found between shape end ({end}) and cursor ({byte_pos}), skipping shape", 591 end = local_span.end 592 ); 593 // There's a separator, so we're starting a new command - skip this shape 594 continue; 595 } 596 597 let span = Span::new(local_span.start, std::cmp::min(local_span.end, byte_pos)); 598 let prefix = safe_slice(input, span); 599 console_log!("[completion] Processing shape {idx} with prefix: {prefix:?}"); 600 601 // Special case: if prefix is just '{' (possibly with whitespace), 602 // we're at the start of a block and should complete commands 603 let trimmed_prefix = prefix.trim(); 604 if trimmed_prefix == "{" { 605 // We're right after '{' - command context 606 if let Some((_, adjusted_span, _)) = handle_block_prefix(&prefix, span) { 607 return vec![CompletionContext { 608 kind: CompletionKind::Command { 609 parent_command: None, 610 }, 611 prefix: String::new(), 612 span: adjusted_span, 613 }]; 614 } 615 } else { 616 match shape { 617 // Special case: Check if we're completing a cell path where the Variable and field are in separate shapes 618 _ if { idx > 0 && matches!(shape, FlatShape::String) } => { 619 let contexts = handle_variable_string_shape( 620 input, 621 shapes, 622 working_set, 623 engine_guard, 624 idx, 625 &prefix, 626 span, 627 local_span, 628 global_offset, 629 ); 630 if !contexts.is_empty() { 631 return contexts; 632 } 633 } 634 // Special case: Check if we're completing a cell path where the Variable and dot are in separate shapes 635 _ if { 636 let trimmed_prefix = prefix.trim(); 637 trimmed_prefix.starts_with('.') && idx > 0 638 } => 639 { 640 let contexts = handle_dot_shape( 641 input, 642 shapes, 643 idx, 644 &prefix, 645 span, 646 local_span, 647 global_offset, 648 ); 649 if !contexts.is_empty() { 650 return contexts; 651 } 652 } 653 _ if { 654 // Check if this is a variable or cell path (starts with $) before treating as command 655 let trimmed_prefix = prefix.trim(); 656 trimmed_prefix.starts_with('$') 657 } => 658 { 659 let trimmed_prefix = prefix.trim(); 660 // Check if this is a cell path (contains a dot after $) 661 if let Some((var_name, path_so_far, cell_prefix)) = 662 parse_cell_path(trimmed_prefix) 663 { 664 // Find the variable ID 665 let var_id = lookup_variable_id(var_name, working_set); 666 667 if let Some(var_id) = var_id { 668 // Calculate span for the cell path member being completed 669 let prefix_byte_len = cell_prefix.len(); 670 let cell_span_start = span.end.saturating_sub(prefix_byte_len); 671 return vec![CompletionContext { 672 kind: CompletionKind::CellPath { 673 var_id, 674 path_so_far: path_so_far 675 .iter() 676 .map(|s| s.to_string()) 677 .collect(), 678 }, 679 prefix: cell_prefix.to_string(), 680 span: Span::new(cell_span_start, span.end), 681 }]; 682 } else { 683 // Unknown variable, fall back to variable completion 684 let var_prefix = trimmed_prefix[1..].to_string(); 685 return vec![CompletionContext { 686 kind: CompletionKind::Variable, 687 prefix: var_prefix, 688 span, 689 }]; 690 } 691 } else { 692 // Variable completion context (no dot) 693 let var_prefix = if trimmed_prefix.len() > 1 { 694 trimmed_prefix[1..].to_string() 695 } else { 696 String::new() 697 }; 698 return vec![CompletionContext { 699 kind: CompletionKind::Variable, 700 prefix: var_prefix, 701 span, 702 }]; 703 } 704 } 705 _ if is_command_shape(input, shape, local_span) => { 706 let (full_prefix, full_span) = 707 build_command_prefix(input, shapes, idx, span, &prefix, global_offset); 708 return vec![CompletionContext { 709 kind: CompletionKind::Command { 710 parent_command: None, 711 }, 712 prefix: full_prefix, 713 span: full_span, 714 }]; 715 } 716 FlatShape::Block | FlatShape::Closure => { 717 let contexts = handle_block_or_closure( 718 input, 719 shapes, 720 working_set, 721 engine_guard, 722 &prefix, 723 span, 724 shape.as_str().trim_start_matches("shape_"), 725 idx, 726 local_span, 727 global_offset, 728 ); 729 if !contexts.is_empty() { 730 return contexts; 731 } 732 } 733 FlatShape::Variable(var_id) => { 734 // Variable or cell path completion context 735 let trimmed_prefix = prefix.trim(); 736 if trimmed_prefix.starts_with('$') { 737 // Check if this is a cell path (contains a dot after $) 738 if let Some((_, path_so_far, cell_prefix)) = 739 parse_cell_path(trimmed_prefix) 740 { 741 let prefix_byte_len = cell_prefix.len(); 742 let cell_span_start = span.end.saturating_sub(prefix_byte_len); 743 return vec![CompletionContext { 744 kind: CompletionKind::CellPath { 745 var_id: *var_id, 746 path_so_far: path_so_far 747 .iter() 748 .map(|s| s.to_string()) 749 .collect(), 750 }, 751 prefix: cell_prefix.to_string(), 752 span: Span::new(cell_span_start, span.end), 753 }]; 754 } else { 755 // Simple variable completion 756 let var_prefix = trimmed_prefix[1..].to_string(); 757 return vec![CompletionContext { 758 kind: CompletionKind::Variable, 759 prefix: var_prefix, 760 span, 761 }]; 762 } 763 } else { 764 // Fallback to argument context if no $ found 765 return vec![CompletionContext { 766 kind: CompletionKind::Argument, 767 prefix: prefix.to_string(), 768 span, 769 }]; 770 } 771 } 772 _ => { 773 // Check if this is a variable or cell path (starts with $) 774 let trimmed_prefix = prefix.trim(); 775 if trimmed_prefix.starts_with('$') { 776 // Check if this is a cell path (contains a dot after $) 777 if let Some((var_name, path_so_far, cell_prefix)) = 778 parse_cell_path(trimmed_prefix) 779 { 780 let var_id = lookup_variable_id(var_name, working_set); 781 if let Some(var_id) = var_id { 782 let prefix_byte_len = cell_prefix.len(); 783 let cell_span_start = span.end.saturating_sub(prefix_byte_len); 784 return vec![CompletionContext { 785 kind: CompletionKind::CellPath { 786 var_id, 787 path_so_far: path_so_far 788 .iter() 789 .map(|s| s.to_string()) 790 .collect(), 791 }, 792 prefix: cell_prefix.to_string(), 793 span: Span::new(cell_span_start, span.end), 794 }]; 795 } else { 796 let var_prefix = trimmed_prefix[1..].to_string(); 797 return vec![CompletionContext { 798 kind: CompletionKind::Variable, 799 prefix: var_prefix, 800 span, 801 }]; 802 } 803 } else { 804 // Simple variable completion 805 let var_prefix = if trimmed_prefix.len() > 1 { 806 trimmed_prefix[1..].to_string() 807 } else { 808 String::new() 809 }; 810 return vec![CompletionContext { 811 kind: CompletionKind::Variable, 812 prefix: var_prefix, 813 span, 814 }]; 815 } 816 } else { 817 // Use helper to determine flag or argument context 818 return determine_flag_or_argument_context( 819 input, 820 shapes, 821 &trimmed_prefix, 822 idx, 823 local_span, 824 span, 825 global_offset, 826 working_set, 827 engine_guard, 828 ); 829 } 830 } 831 } 832 } 833 break; 834 } 835 } 836 Vec::new() 837} 838 839pub fn determine_context_fallback( 840 input: &str, 841 shapes: &[(Span, FlatShape)], 842 working_set: &StateWorkingSet, 843 engine_guard: &EngineState, 844 byte_pos: usize, 845 global_offset: usize, 846) -> Vec<CompletionContext> { 847 use nu_parser::{TokenContents, lex}; 848 849 console_log!("[completion] Context is None, entering fallback logic"); 850 // Check if there's a command-like shape before us 851 let mut has_separator_after_command = false; 852 for (span, shape) in shapes.iter().rev() { 853 let local_span = to_local_span(*span, global_offset); 854 if local_span.end <= byte_pos { 855 if is_command_shape(input, shape, local_span) { 856 // Check if there's a pipe or semicolon between this command and the cursor 857 has_separator_after_command = 858 has_separator_between(input, local_span.end, byte_pos); 859 console_log!( 860 "[completion] Found command shape {shape:?} at {local_span:?}, has_separator_after_command={has_separator_after_command}" 861 ); 862 if !has_separator_after_command { 863 // Extract the command text (full command including subcommands) 864 let cmd = safe_slice(input, local_span); 865 let cmd_full = cmd.trim().to_string(); 866 let cmd_first_word = extract_command_name(cmd).to_string(); 867 868 // Check if we're right after the command (only whitespace between command and cursor) 869 let text_after_command = if local_span.end < input.len() { 870 &input[local_span.end..byte_pos] 871 } else { 872 "" 873 }; 874 let is_right_after_command = text_after_command.trim().is_empty(); 875 876 // If we're right after a command, check if it has positional arguments 877 if is_right_after_command { 878 // Check if the command text contains spaces (indicating it's a subcommand like "attr category") 879 let is_subcommand = cmd_full.contains(' ') && cmd_full != cmd_first_word; 880 881 // First, try the full command name (e.g., "attr category") 882 // If that doesn't exist, fall back to the first word (e.g., "attr") 883 let full_cmd_exists = 884 get_command_signature(engine_guard, &cmd_full).is_some(); 885 let cmd_name = if full_cmd_exists { 886 cmd_full.clone() 887 } else { 888 cmd_first_word.clone() 889 }; 890 891 let mut context = Vec::with_capacity(2); 892 if let Some(signature) = get_command_signature(engine_guard, &cmd_name) { 893 // Check if command has any positional arguments 894 let has_positional_args = !signature.required_positional.is_empty() 895 || !signature.optional_positional.is_empty(); 896 897 if has_positional_args { 898 // Count existing arguments before cursor 899 let mut arg_count = 0; 900 for (prev_span, prev_shape) in shapes.iter().rev() { 901 let prev_local_span = to_local_span(*prev_span, global_offset); 902 if prev_local_span.end <= byte_pos 903 && prev_local_span.end > local_span.end 904 { 905 if !is_command_shape(input, prev_shape, prev_local_span) { 906 let arg_text = safe_slice(input, prev_local_span); 907 let trimmed_arg = arg_text.trim(); 908 // Don't count flags (starting with -) or empty arguments 909 if !trimmed_arg.is_empty() 910 && !trimmed_arg.starts_with('-') 911 { 912 arg_count += 1; 913 } 914 } 915 } 916 } 917 918 console_log!( 919 "[completion] Right after command {cmd_name:?}, setting CommandArgument context with arg_index: {arg_count}" 920 ); 921 922 // Use helper to create CommandArgument context(s) - may include subcommand context 923 let arg_contexts = create_command_argument_contexts( 924 cmd_name.clone(), 925 arg_count, 926 String::new(), 927 Span::new(byte_pos, byte_pos), 928 working_set, 929 engine_guard, 930 ); 931 context.extend(arg_contexts); 932 } 933 } 934 // No positional arguments 935 // If this is a subcommand (contains spaces), don't show subcommands 936 // Only show subcommands if we're using just the base command (single word) 937 if is_subcommand && full_cmd_exists { 938 console_log!( 939 "[completion] Command {cmd_name:?} is a subcommand with no positional args, not showing completions" 940 ); 941 } else { 942 // Show subcommands of the base command 943 console_log!( 944 "[completion] Command {cmd_name:?} has no positional args, showing subcommands" 945 ); 946 context.push(CompletionContext { 947 kind: CompletionKind::Command { 948 parent_command: Some(cmd_first_word), 949 }, 950 prefix: String::new(), 951 span: Span::new(byte_pos, byte_pos), 952 }); 953 } 954 // reverse to put subcommands in the beginning 955 context.reverse(); 956 return context; 957 } else { 958 // Not right after command, complete the command itself 959 console_log!("[completion] Set Command context with prefix: {cmd:?}"); 960 return vec![CompletionContext { 961 kind: CompletionKind::Command { 962 parent_command: None, 963 }, 964 prefix: cmd.to_string(), 965 span: local_span, 966 }]; 967 } 968 } 969 } 970 break; 971 } 972 } 973 974 // No command found before, check context from tokens 975 console_log!("[completion] No command found before cursor, checking tokens"); 976 // No command before, check context from tokens 977 let (tokens, _) = lex(input.as_bytes(), 0, &[], &[], true); 978 let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last(); 979 980 let is_cmd_context = if let Some(token) = last_token { 981 let matches = matches!( 982 token.contents, 983 TokenContents::Pipe 984 | TokenContents::PipePipe 985 | TokenContents::Semicolon 986 | TokenContents::Eol 987 ); 988 console_log!( 989 "[completion] Last token: {contents:?}, is_cmd_context from token={matches}", 990 contents = token.contents 991 ); 992 matches 993 } else { 994 console_log!( 995 "[completion] No last token found, assuming start of input (is_cmd_context=true)" 996 ); 997 true // Start of input 998 }; 999 1000 // Look for the last non-whitespace token before cursor 1001 let text_before = &input[..byte_pos]; 1002 1003 // Also check if we're inside a block - if the last non-whitespace char before cursor is '{' 1004 let text_before_trimmed = text_before.trim_end(); 1005 let is_inside_block = text_before_trimmed.ends_with('{'); 1006 // If we found a separator after a command, we're starting a new command 1007 let is_cmd_context = is_cmd_context || is_inside_block || has_separator_after_command; 1008 console_log!( 1009 "[completion] is_inside_block={is_inside_block}, has_separator_after_command={has_separator_after_command}, final is_cmd_context={is_cmd_context}" 1010 ); 1011 1012 // Find the last word before cursor 1013 let last_word_start = text_before 1014 .rfind(|c: char| c.is_whitespace() || is_separator_char(c)) 1015 .map(|i| i + 1) 1016 .unwrap_or(0); 1017 1018 let last_word = text_before[last_word_start..].trim_start(); 1019 console_log!("[completion] last_word_start={last_word_start}, last_word={last_word:?}"); 1020 1021 if is_cmd_context { 1022 vec![CompletionContext { 1023 kind: CompletionKind::Command { 1024 parent_command: None, 1025 }, 1026 prefix: last_word.to_string(), 1027 span: Span::new(last_word_start, byte_pos), 1028 }] 1029 } else { 1030 // Check if this is a variable or cell path (starts with $) 1031 let trimmed_word = last_word.trim(); 1032 if trimmed_word.starts_with('$') { 1033 // Check if this is a cell path (contains a dot after $) 1034 if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed_word) { 1035 let var_id = lookup_variable_id(&var_name, working_set); 1036 1037 if let Some(var_id) = var_id { 1038 let prefix_byte_len = cell_prefix.len(); 1039 let cell_span_start = byte_pos.saturating_sub(prefix_byte_len); 1040 vec![CompletionContext { 1041 kind: CompletionKind::CellPath { 1042 var_id, 1043 path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(), 1044 }, 1045 prefix: cell_prefix.to_string(), 1046 span: Span::new(cell_span_start, byte_pos), 1047 }] 1048 } else { 1049 let var_prefix = trimmed_word[1..].to_string(); 1050 vec![CompletionContext { 1051 kind: CompletionKind::Variable, 1052 prefix: var_prefix, 1053 span: Span::new(last_word_start, byte_pos), 1054 }] 1055 } 1056 } else { 1057 // Simple variable completion 1058 let var_prefix = trimmed_word[1..].to_string(); 1059 vec![CompletionContext { 1060 kind: CompletionKind::Variable, 1061 prefix: var_prefix, 1062 span: Span::new(last_word_start, byte_pos), 1063 }] 1064 } 1065 } else if trimmed_word.starts_with('-') { 1066 // Try to find command by looking backwards through shapes 1067 let mut found_cmd = None; 1068 for (span, shape) in shapes.iter().rev() { 1069 let local_span = to_local_span(*span, global_offset); 1070 if local_span.end <= byte_pos && is_command_shape(input, shape, local_span) { 1071 let cmd_text = safe_slice(input, local_span); 1072 let cmd_name = extract_command_name(cmd_text).to_string(); 1073 found_cmd = Some(cmd_name); 1074 break; 1075 } 1076 } 1077 if let Some(cmd_name) = found_cmd { 1078 vec![CompletionContext { 1079 kind: CompletionKind::Flag { 1080 command_name: cmd_name, 1081 }, 1082 prefix: trimmed_word.to_string(), 1083 span: Span::new(last_word_start, byte_pos), 1084 }] 1085 } else { 1086 vec![CompletionContext { 1087 kind: CompletionKind::Argument, 1088 prefix: last_word.to_string(), 1089 span: Span::new(last_word_start, byte_pos), 1090 }] 1091 } 1092 } else { 1093 // Try to find command and argument index 1094 let mut found_cmd = None; 1095 let mut arg_count = 0; 1096 for (span, shape) in shapes.iter().rev() { 1097 let local_span = to_local_span(*span, global_offset); 1098 if local_span.end <= byte_pos { 1099 if is_command_shape(input, shape, local_span) { 1100 let cmd_text = safe_slice(input, local_span); 1101 let cmd_name = extract_command_name(cmd_text).to_string(); 1102 found_cmd = Some(cmd_name); 1103 break; 1104 } else { 1105 let arg_text = safe_slice(input, local_span); 1106 let trimmed_arg = arg_text.trim(); 1107 if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') { 1108 arg_count += 1; 1109 } 1110 } 1111 } 1112 } 1113 if let Some(cmd_name) = found_cmd { 1114 create_command_argument_contexts( 1115 cmd_name, 1116 arg_count, 1117 trimmed_word.to_string(), 1118 Span::new(last_word_start, byte_pos), 1119 working_set, 1120 engine_guard, 1121 ) 1122 } else { 1123 vec![CompletionContext { 1124 kind: CompletionKind::Argument, 1125 prefix: last_word.to_string(), 1126 span: Span::new(last_word_start, byte_pos), 1127 }] 1128 } 1129 } 1130 } 1131} 1132 1133pub fn determine_context( 1134 input: &str, 1135 shapes: &[(Span, FlatShape)], 1136 working_set: &StateWorkingSet, 1137 engine_guard: &EngineState, 1138 byte_pos: usize, 1139 global_offset: usize, 1140) -> Vec<CompletionContext> { 1141 // First try to determine context from shapes 1142 let contexts = determine_context_from_shape( 1143 input, 1144 shapes, 1145 working_set, 1146 engine_guard, 1147 byte_pos, 1148 global_offset, 1149 ); 1150 if !contexts.is_empty() { 1151 return contexts; 1152 } 1153 1154 // Fallback to token-based context determination 1155 determine_context_fallback( 1156 input, 1157 shapes, 1158 working_set, 1159 engine_guard, 1160 byte_pos, 1161 global_offset, 1162 ) 1163}