support multiple contexts, dont sort all suggestions to keep sorting of contexts

ptr.pet 429dc853 a1dbebe2

verified
Changed files
+161 -202
src
www
src
+1 -1
src/cmd/source.rs
··· 18 18 19 19 fn signature(&self) -> Signature { 20 20 Signature::build(self.name()) 21 - .required("filename", SyntaxShape::String, "the file to source") 21 + .required("filename", SyntaxShape::Filepath, "the file to source") 22 22 .input_output_type(Type::Nothing, Type::Nothing) 23 23 .category(Category::Core) 24 24 }
+44 -61
src/completion/context.rs
··· 734 734 engine_guard: &EngineState, 735 735 byte_pos: usize, 736 736 global_offset: usize, 737 - ) -> Option<CompletionContext> { 737 + ) -> Vec<CompletionContext> { 738 738 use nu_parser::{TokenContents, lex}; 739 739 740 740 console_log!("[completion] Context is None, entering fallback logic"); ··· 779 779 cmd_first_word.clone() 780 780 }; 781 781 782 + let mut context = Vec::with_capacity(2); 782 783 if let Some(signature) = get_command_signature(engine_guard, &cmd_name) { 783 784 // Check if command has any positional arguments 784 785 let has_positional_args = !signature.required_positional.is_empty() ··· 809 810 "[completion] Right after command {cmd_name:?}, setting CommandArgument context with arg_index: {arg_count}" 810 811 ); 811 812 812 - return Some(CompletionContext::CommandArgument { 813 + context.push(CompletionContext::CommandArgument { 813 814 prefix: String::new(), 814 815 span: Span::new(byte_pos, byte_pos), 815 - command_name: cmd_name, 816 + command_name: cmd_name.clone(), 816 817 arg_index: arg_count, 817 818 }); 818 - } else { 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 819 } 820 + } 821 + // No positional arguments 822 + // If this is a subcommand (contains spaces), don't show subcommands 823 + // Only show subcommands if we're using just the base command (single word) 824 + if is_subcommand && full_cmd_exists { 825 + console_log!( 826 + "[completion] Command {cmd_name:?} is a subcommand with no positional args, not showing completions" 827 + ); 839 828 } 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 { 844 - console_log!( 845 - "[completion] Could not find signature for subcommand {cmd_name:?}, not showing completions" 846 - ); 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 - }); 857 - } 829 + // Show subcommands of the base command 830 + console_log!( 831 + "[completion] Command {cmd_name:?} has no positional args, showing subcommands" 832 + ); 833 + context.push(CompletionContext::Command { 834 + prefix: String::new(), 835 + span: Span::new(byte_pos, byte_pos), 836 + parent_command: Some(cmd_first_word), 837 + }); 858 838 } 839 + // reverse to put subcommands in the beginning 840 + context.reverse(); 841 + return context; 859 842 } else { 860 843 // Not right after command, complete the command itself 861 844 console_log!("[completion] Set Command context with prefix: {cmd:?}"); 862 - return Some(CompletionContext::Command { 845 + return vec![CompletionContext::Command { 863 846 prefix: cmd.to_string(), 864 847 span: local_span, 865 848 parent_command: None, 866 - }); 849 + }]; 867 850 } 868 851 } 869 852 } ··· 919 902 console_log!("[completion] last_word_start={last_word_start}, last_word={last_word:?}"); 920 903 921 904 if is_cmd_context { 922 - Some(CompletionContext::Command { 905 + vec![CompletionContext::Command { 923 906 prefix: last_word.to_string(), 924 907 span: Span::new(last_word_start, byte_pos), 925 908 parent_command: None, 926 - }) 909 + }] 927 910 } else { 928 911 // Check if this is a variable or cell path (starts with $) 929 912 let trimmed_word = last_word.trim(); ··· 935 918 if let Some(var_id) = var_id { 936 919 let prefix_byte_len = cell_prefix.len(); 937 920 let cell_span_start = byte_pos.saturating_sub(prefix_byte_len); 938 - Some(CompletionContext::CellPath { 921 + vec![CompletionContext::CellPath { 939 922 prefix: cell_prefix.to_string(), 940 923 span: Span::new(cell_span_start, byte_pos), 941 924 var_id, 942 925 path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(), 943 - }) 926 + }] 944 927 } else { 945 928 let var_prefix = trimmed_word[1..].to_string(); 946 - Some(CompletionContext::Variable { 929 + vec![CompletionContext::Variable { 947 930 prefix: var_prefix, 948 931 span: Span::new(last_word_start, byte_pos), 949 - }) 932 + }] 950 933 } 951 934 } else { 952 935 // Simple variable completion 953 936 let var_prefix = trimmed_word[1..].to_string(); 954 - Some(CompletionContext::Variable { 937 + vec![CompletionContext::Variable { 955 938 prefix: var_prefix, 956 939 span: Span::new(last_word_start, byte_pos), 957 - }) 940 + }] 958 941 } 959 942 } else if trimmed_word.starts_with('-') { 960 943 // Try to find command by looking backwards through shapes ··· 969 952 } 970 953 } 971 954 if let Some(cmd_name) = found_cmd { 972 - Some(CompletionContext::Flag { 955 + vec![CompletionContext::Flag { 973 956 prefix: trimmed_word.to_string(), 974 957 span: Span::new(last_word_start, byte_pos), 975 958 command_name: cmd_name, 976 - }) 959 + }] 977 960 } else { 978 - Some(CompletionContext::Argument { 961 + vec![CompletionContext::Argument { 979 962 prefix: last_word.to_string(), 980 963 span: Span::new(last_word_start, byte_pos), 981 - }) 964 + }] 982 965 } 983 966 } else { 984 967 // Try to find command and argument index ··· 1002 985 } 1003 986 } 1004 987 if let Some(cmd_name) = found_cmd { 1005 - Some(CompletionContext::CommandArgument { 988 + vec![CompletionContext::CommandArgument { 1006 989 prefix: trimmed_word.to_string(), 1007 990 span: Span::new(last_word_start, byte_pos), 1008 991 command_name: cmd_name, 1009 992 arg_index: arg_count, 1010 - }) 993 + }] 1011 994 } else { 1012 - Some(CompletionContext::Argument { 995 + vec![CompletionContext::Argument { 1013 996 prefix: last_word.to_string(), 1014 997 span: Span::new(last_word_start, byte_pos), 1015 - }) 998 + }] 1016 999 } 1017 1000 } 1018 1001 } ··· 1025 1008 engine_guard: &EngineState, 1026 1009 byte_pos: usize, 1027 1010 global_offset: usize, 1028 - ) -> Option<CompletionContext> { 1011 + ) -> Vec<CompletionContext> { 1029 1012 // First try to determine context from shapes 1030 1013 if let Some(ctx) = 1031 1014 determine_context_from_shape(input, shapes, working_set, byte_pos, global_offset) 1032 1015 { 1033 - return Some(ctx); 1016 + return vec![ctx]; 1034 1017 } 1035 1018 1036 1019 // Fallback to token-based context determination
-51
src/completion/files.rs
··· 1 - use crate::completion::helpers::to_char_span; 2 - use crate::completion::types::Suggestion; 3 - use nu_protocol::Span; 4 - 5 - pub fn generate_file_suggestions( 6 - prefix: &str, 7 - span: Span, 8 - root: &std::sync::Arc<vfs::VfsPath>, 9 - description: Option<String>, 10 - input: &str, 11 - ) -> Vec<Suggestion> { 12 - let (dir, file_prefix) = prefix 13 - .rfind('/') 14 - .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 15 - .unwrap_or(("", prefix)); 16 - 17 - let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 18 - .then(|| &dir[..dir.len() - 1]) 19 - .unwrap_or(dir); 20 - 21 - let target_dir = if !dir.is_empty() { 22 - match root.join(dir_to_join) { 23 - Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 24 - _ => None, 25 - } 26 - } else { 27 - Some(root.join("").unwrap()) 28 - }; 29 - 30 - let mut file_suggestions = Vec::new(); 31 - if let Some(d) = target_dir { 32 - if let Ok(iterator) = d.read_dir() { 33 - let char_span = to_char_span(input, span); 34 - for entry in iterator { 35 - let name = entry.filename(); 36 - if name.starts_with(file_prefix) { 37 - let full_completion = format!("{}{}", dir, name); 38 - file_suggestions.push(Suggestion { 39 - name: full_completion.clone(), 40 - description: description.clone(), 41 - is_command: false, 42 - rendered: full_completion, 43 - span_start: char_span.start, 44 - span_end: char_span.end, 45 - }); 46 - } 47 - } 48 - } 49 - } 50 - file_suggestions 51 - }
+1 -3
src/completion/mod.rs
··· 9 9 use super::*; 10 10 11 11 pub mod context; 12 - pub mod files; 13 12 pub mod helpers; 14 13 pub mod suggestions; 15 14 pub mod types; ··· 75 74 ); 76 75 77 76 // Generate suggestions based on context 78 - let mut suggestions = generate_suggestions( 77 + let suggestions = generate_suggestions( 79 78 &input, 80 79 context, 81 80 &working_set, ··· 88 87 drop(working_set); 89 88 drop(engine_guard); 90 89 91 - suggestions.sort(); 92 90 let suggestions = serde_json::to_string(&suggestions).unwrap_or_else(|_| "[]".to_string()); 93 91 console_log!("{suggestions}"); 94 92 suggestions
+114 -82
src/completion/suggestions.rs
··· 1 1 use crate::completion::context::get_command_signature; 2 - use crate::completion::files::generate_file_suggestions; 3 2 use crate::completion::helpers::to_char_span; 4 3 use crate::completion::types::{CompletionContext, Suggestion}; 5 4 use crate::completion::variables::*; ··· 65 64 }, 66 65 name: display_name, 67 66 description: desc.map(|d| d.to_string()), 68 - is_command: true, 69 67 span_start: span.start, 70 68 span_end: span.end, 71 69 }); 72 70 cmd_count += 1; 73 71 } 74 72 console_log!("[completion] Found {cmd_count} command suggestions"); 73 + suggestions.sort(); 75 74 suggestions 76 75 } 77 76 ··· 83 82 ) -> Vec<Suggestion> { 84 83 console_log!("[completion] Generating Argument suggestions with prefix: {prefix:?}"); 85 84 // File completion 86 - let file_suggestions = generate_file_suggestions(&prefix, span, root, None, input); 87 - let file_count = file_suggestions.len(); 88 - console_log!("[completion] Found {file_count} file suggestions"); 85 + let mut file_suggestions = generate_file_suggestions(&prefix, span, root, None, input); 86 + console_log!( 87 + "[completion] Found {file_count} file suggestions", 88 + file_count = file_suggestions.len() 89 + ); 90 + file_suggestions.sort(); 89 91 file_suggestions 90 92 } 91 93 ··· 127 129 Suggestion { 128 130 name: flag_name.clone(), 129 131 description: Some(flag.desc.clone()), 130 - is_command: false, 131 132 rendered: { 132 133 let flag_colored = ansi_term::Color::Cyan.bold().paint(&flag_name); 133 134 format!("{flag_colored} {}", flag.desc) ··· 185 186 } else { 186 187 console_log!("[completion] Could not find signature for command: {command_name:?}"); 187 188 } 189 + suggestions.sort(); 188 190 suggestions 189 191 } 190 192 ··· 309 311 310 312 if let Some(arg) = arg { 311 313 // Check the SyntaxShape to determine completion type 312 - // Only suggest files/dirs for Filepath type (or "any" when type is unknown) 314 + // Only suggest files/dirs for Filepath type 313 315 match &arg.shape { 314 316 nu_protocol::SyntaxShape::Filepath | nu_protocol::SyntaxShape::Any => { 315 317 // File/directory completion ··· 325 327 console_log!( 326 328 "[completion] Found {file_count} file suggestions for argument {arg_index}" 327 329 ); 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 - } 348 330 } 349 331 _ => { 350 332 // For other types, don't suggest files ··· 369 351 let file_suggestions = generate_file_suggestions(&prefix, span, root, None, input); 370 352 suggestions.extend(file_suggestions); 371 353 } 354 + suggestions.sort(); 372 355 suggestions 373 356 } 374 357 ··· 396 379 suggestions.push(Suggestion { 397 380 name: var_name.clone(), 398 381 description: Some(var_type.clone()), 399 - is_command: false, 400 382 rendered: { 401 383 let var_colored = ansi_term::Color::Blue.bold().paint(&var_name); 402 384 format!("{var_colored} {var_type}") ··· 409 391 } 410 392 411 393 console_log!("[completion] Found {var_count} variable suggestions"); 394 + suggestions.sort(); 412 395 suggestions 413 396 } 414 397 ··· 451 434 suggestions.push(Suggestion { 452 435 name: col_name.clone(), 453 436 description: Some(type_str.to_string()), 454 - is_command: false, 455 437 rendered: { 456 438 let col_colored = ansi_term::Color::Yellow.paint(&col_name); 457 439 format!("{col_colored} {type_str}") ··· 478 460 console_log!("[completion] Variable type: {ty:?}", ty = var_info.ty); 479 461 } 480 462 } 463 + suggestions.sort(); 481 464 suggestions 482 465 } 483 466 467 + pub fn generate_file_suggestions( 468 + prefix: &str, 469 + span: Span, 470 + root: &std::sync::Arc<vfs::VfsPath>, 471 + description: Option<String>, 472 + input: &str, 473 + ) -> Vec<Suggestion> { 474 + let (dir, file_prefix) = prefix 475 + .rfind('/') 476 + .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 477 + .unwrap_or(("", prefix)); 478 + 479 + let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 480 + .then(|| &dir[..dir.len() - 1]) 481 + .unwrap_or(dir); 482 + 483 + let target_dir = if !dir.is_empty() { 484 + match root.join(dir_to_join) { 485 + Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 486 + _ => None, 487 + } 488 + } else { 489 + Some(root.join("").unwrap()) 490 + }; 491 + 492 + let mut file_suggestions = Vec::new(); 493 + if let Some(d) = target_dir { 494 + if let Ok(iterator) = d.read_dir() { 495 + let char_span = to_char_span(input, span); 496 + for entry in iterator { 497 + let name = entry.filename(); 498 + if name.starts_with(file_prefix) { 499 + let full_completion = format!("{}{}", dir, name); 500 + file_suggestions.push(Suggestion { 501 + name: full_completion.clone(), 502 + description: description.clone(), 503 + rendered: full_completion, 504 + span_start: char_span.start, 505 + span_end: char_span.end, 506 + }); 507 + } 508 + } 509 + } 510 + } 511 + file_suggestions 512 + } 513 + 484 514 pub fn generate_suggestions( 485 515 input: &str, 486 - context: Option<CompletionContext>, 516 + contexts: Vec<CompletionContext>, 487 517 working_set: &StateWorkingSet, 488 518 engine_guard: &EngineState, 489 519 stack_guard: &Stack, 490 520 root: &std::sync::Arc<vfs::VfsPath>, 491 521 byte_pos: usize, 492 522 ) -> Vec<Suggestion> { 493 - console_log!("context: {context:?}"); 523 + console_log!("contexts: {contexts:?}"); 494 524 495 - match context { 496 - Some(CompletionContext::Command { 497 - prefix, 498 - span, 499 - parent_command, 500 - }) => generate_command_suggestions(input, working_set, prefix, span, parent_command), 501 - Some(CompletionContext::Argument { prefix, span }) => { 502 - generate_argument_suggestions(input, prefix, span, root) 503 - } 504 - Some(CompletionContext::Flag { 505 - prefix, 506 - span, 507 - command_name, 508 - }) => generate_flag_suggestions(input, engine_guard, prefix, span, command_name), 509 - Some(CompletionContext::CommandArgument { 510 - prefix, 511 - span, 512 - command_name, 513 - arg_index, 514 - }) => generate_command_argument_suggestions( 515 - input, 516 - engine_guard, 517 - working_set, 518 - prefix, 519 - span, 520 - command_name, 521 - arg_index, 522 - root, 523 - ), 524 - Some(CompletionContext::Variable { prefix, span }) => { 525 - generate_variable_suggestions(input, working_set, prefix, span, byte_pos) 526 - } 527 - Some(CompletionContext::CellPath { 528 - prefix, 529 - span, 530 - var_id, 531 - path_so_far, 532 - }) => generate_cell_path_suggestions( 533 - input, 534 - working_set, 535 - engine_guard, 536 - stack_guard, 537 - prefix, 538 - span, 539 - var_id, 540 - path_so_far, 541 - ), 542 - _ => { 543 - console_log!("[completion] Context is None, no suggestions generated"); 544 - Vec::new() 545 - } 525 + let mut suggestions = Vec::new(); 526 + for context in contexts { 527 + let mut sug = match context { 528 + CompletionContext::Command { 529 + prefix, 530 + span, 531 + parent_command, 532 + } => generate_command_suggestions(input, working_set, prefix, span, parent_command), 533 + CompletionContext::Argument { prefix, span } => { 534 + generate_argument_suggestions(input, prefix, span, root) 535 + } 536 + CompletionContext::Flag { 537 + prefix, 538 + span, 539 + command_name, 540 + } => generate_flag_suggestions(input, engine_guard, prefix, span, command_name), 541 + CompletionContext::CommandArgument { 542 + prefix, 543 + span, 544 + command_name, 545 + arg_index, 546 + } => generate_command_argument_suggestions( 547 + input, 548 + engine_guard, 549 + working_set, 550 + prefix, 551 + span, 552 + command_name, 553 + arg_index, 554 + root, 555 + ), 556 + CompletionContext::Variable { prefix, span } => { 557 + generate_variable_suggestions(input, working_set, prefix, span, byte_pos) 558 + } 559 + CompletionContext::CellPath { 560 + prefix, 561 + span, 562 + var_id, 563 + path_so_far, 564 + } => generate_cell_path_suggestions( 565 + input, 566 + working_set, 567 + engine_guard, 568 + stack_guard, 569 + prefix, 570 + span, 571 + var_id, 572 + path_so_far, 573 + ), 574 + }; 575 + suggestions.append(&mut sug); 546 576 } 577 + 578 + suggestions 547 579 }
+1 -2
src/completion/types.rs
··· 5 5 pub struct Suggestion { 6 6 pub name: String, 7 7 pub description: Option<String>, 8 - pub is_command: bool, 9 8 pub rendered: String, 10 9 pub span_start: usize, // char index (not byte) 11 10 pub span_end: usize, // char index (not byte) ··· 28 27 } 29 28 } 30 29 31 - #[derive(Debug)] 30 + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] 32 31 pub enum CompletionContext { 33 32 Command { 34 33 prefix: String,
-2
www/src/main.ts
··· 14 14 type Candidate = { 15 15 name: string; 16 16 description: string; 17 - is_command: boolean; 18 17 rendered: string; 19 18 span_start: number; 20 19 span_end: number; ··· 300 299 completionCandidates = matches.map((cmd) => ({ 301 300 name: cmd, 302 301 description: "", 303 - is_command: true, 304 302 rendered: cmd, 305 303 span_start: 0, 306 304 span_end: currentLine.length,