endpoint 2.0 dysnomia.ptr.pet
0
fork

Configure Feed

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

add 'proper' argument, flag completion

dawn c6e4e970 15375ed6

+518 -17
+3
.gitignore
··· 24 24 /result 25 25 /.direnv 26 26 target 27 + 28 + # ai scratchpad 29 + /.scratchpad
+515 -17
src/completion.rs
··· 152 152 )) 153 153 }; 154 154 155 + // Helper function to find command name and count arguments before cursor 156 + let find_command_and_arg_index = 157 + |current_idx: usize, current_local_span: Span| -> Option<(String, usize)> { 158 + let mut command_name: Option<String> = None; 159 + let mut arg_count = 0; 160 + 161 + // Look backwards through shapes to find the command 162 + for i in (0..current_idx).rev() { 163 + if let Some((prev_span, prev_shape)) = shapes.get(i) { 164 + let prev_local_span = to_local_span(*prev_span); 165 + 166 + // Check if there's a separator between this shape and the next one 167 + let next_shape_start = if i + 1 < shapes.len() { 168 + to_local_span(shapes[i + 1].0).start 169 + } else { 170 + current_local_span.start 171 + }; 172 + 173 + if has_separator_between(prev_local_span.end, next_shape_start) { 174 + break; // Stop at separator 175 + } 176 + 177 + if is_command_shape(prev_shape, prev_local_span) { 178 + // Found the command 179 + let cmd_text = safe_slice(prev_local_span); 180 + // Extract just the command name (first word, no flags) 181 + let cmd_name = cmd_text 182 + .split_whitespace() 183 + .next() 184 + .unwrap_or(&cmd_text) 185 + .trim(); 186 + command_name = Some(cmd_name.to_string()); 187 + break; 188 + } else { 189 + // This is an argument - count it if it's not a flag 190 + let arg_text = safe_slice(prev_local_span); 191 + let trimmed_arg = arg_text.trim(); 192 + // Don't count flags (starting with -) or empty arguments 193 + if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') { 194 + arg_count += 1; 195 + } 196 + } 197 + } 198 + } 199 + 200 + command_name.map(|name| (name, arg_count)) 201 + }; 202 + 155 203 // Helper function to handle both Block and Closure shapes 156 204 let handle_block_or_closure = |prefix: &str, 157 205 span: Span, 158 - shape_name: &str| 206 + shape_name: &str, 207 + current_idx: usize, 208 + local_span: Span| 159 209 -> Option<CompletionContext> { 160 210 web_sys::console::log_1(&JsValue::from_str(&format!( 161 211 "[completion] Processing {} shape with prefix: {:?}", ··· 203 253 }) 204 254 } else { 205 255 web_sys::console::log_1(&JsValue::from_str(&format!( 206 - "[completion] {} has no separator, setting Argument context", 256 + "[completion] {} has no separator, checking for flag/argument context", 207 257 shape_name 208 258 ))); 209 - Some(CompletionContext::Argument { 210 - prefix: trimmed_prefix, 211 - span: adjusted_span, 212 - }) 259 + // Check if this is a flag or command argument 260 + let trimmed = trimmed_prefix.trim(); 261 + let is_flag = trimmed.starts_with('-'); 262 + 263 + // Try to find the command and argument index 264 + if let Some((cmd_name, arg_index)) = 265 + find_command_and_arg_index(current_idx, local_span) 266 + { 267 + if is_flag { 268 + web_sys::console::log_1(&JsValue::from_str(&format!( 269 + "[completion] {}: Found command {:?} for flag completion", 270 + shape_name, cmd_name 271 + ))); 272 + Some(CompletionContext::Flag { 273 + prefix: trimmed.to_string(), 274 + span: adjusted_span, 275 + command_name: cmd_name, 276 + }) 277 + } else { 278 + web_sys::console::log_1(&JsValue::from_str(&format!( 279 + "[completion] {}: Found command {:?} with arg_index {} for argument completion", 280 + shape_name, cmd_name, arg_index 281 + ))); 282 + Some(CompletionContext::CommandArgument { 283 + prefix: trimmed.to_string(), 284 + span: adjusted_span, 285 + command_name: cmd_name, 286 + arg_index, 287 + }) 288 + } 289 + } else { 290 + // No command found, treat as regular argument 291 + web_sys::console::log_1(&JsValue::from_str(&format!( 292 + "[completion] {}: No command found{}, using Argument context", 293 + shape_name, 294 + if is_flag { " for flag" } else { "" } 295 + ))); 296 + Some(CompletionContext::Argument { 297 + prefix: trimmed_prefix, 298 + span: adjusted_span, 299 + }) 300 + } 213 301 } 214 302 } else { 215 303 None ··· 219 307 // Find what we're completing 220 308 #[derive(Debug)] 221 309 enum CompletionContext { 222 - Command { prefix: String, span: Span }, 223 - Argument { prefix: String, span: Span }, 310 + Command { 311 + prefix: String, 312 + span: Span, 313 + }, 314 + Argument { 315 + prefix: String, 316 + span: Span, 317 + }, 318 + Flag { 319 + prefix: String, 320 + span: Span, 321 + command_name: String, 322 + }, 323 + CommandArgument { 324 + prefix: String, 325 + span: Span, 326 + command_name: String, 327 + arg_index: usize, 328 + }, 224 329 } 225 330 226 331 let mut context: Option<CompletionContext> = None; ··· 323 428 &prefix, 324 429 span, 325 430 shape.as_str().trim_start_matches("shape_"), 431 + idx, 432 + local_span, 326 433 ) { 327 434 context = Some(ctx); 328 435 } 329 436 } 330 437 _ => { 331 - context = Some(CompletionContext::Argument { prefix, span }); 438 + // Check if this is a flag or command argument 439 + let trimmed_prefix = prefix.trim(); 440 + if trimmed_prefix.starts_with('-') { 441 + // This looks like a flag - find the command 442 + if let Some((cmd_name, _)) = find_command_and_arg_index(idx, local_span) 443 + { 444 + context = Some(CompletionContext::Flag { 445 + prefix: trimmed_prefix.to_string(), 446 + span, 447 + command_name: cmd_name, 448 + }); 449 + } else { 450 + context = Some(CompletionContext::Argument { prefix, span }); 451 + } 452 + } else { 453 + // This is a positional argument - find the command and argument index 454 + if let Some((cmd_name, arg_index)) = 455 + find_command_and_arg_index(idx, local_span) 456 + { 457 + context = Some(CompletionContext::CommandArgument { 458 + prefix: trimmed_prefix.to_string(), 459 + span, 460 + command_name: cmd_name, 461 + arg_index, 462 + }); 463 + } else { 464 + context = Some(CompletionContext::Argument { prefix, span }); 465 + } 466 + } 332 467 } 333 468 } 334 469 } ··· 438 573 last_word 439 574 ))); 440 575 } else { 441 - context = Some(CompletionContext::Argument { 442 - prefix: last_word.to_string(), 443 - span: Span::new(last_word_start, byte_pos), 444 - }); 445 - web_sys::console::log_1(&JsValue::from_str(&format!( 446 - "[completion] Set Argument context with prefix: {:?}", 447 - last_word 448 - ))); 576 + // Check if this is a flag or command argument 577 + let trimmed_word = last_word.trim(); 578 + if trimmed_word.starts_with('-') { 579 + // Try to find command by looking backwards through shapes 580 + let mut found_cmd = None; 581 + for (span, shape) in shapes.iter().rev() { 582 + let local_span = to_local_span(*span); 583 + if local_span.end <= byte_pos && is_command_shape(shape, local_span) { 584 + let cmd_text = safe_slice(local_span); 585 + let cmd_name = cmd_text 586 + .split_whitespace() 587 + .next() 588 + .unwrap_or(&cmd_text) 589 + .trim(); 590 + found_cmd = Some(cmd_name.to_string()); 591 + break; 592 + } 593 + } 594 + if let Some(cmd_name) = found_cmd { 595 + let cmd_name_clone = cmd_name.clone(); 596 + context = Some(CompletionContext::Flag { 597 + prefix: trimmed_word.to_string(), 598 + span: Span::new(last_word_start, byte_pos), 599 + command_name: cmd_name, 600 + }); 601 + web_sys::console::log_1(&JsValue::from_str(&format!( 602 + "[completion] Set Flag context with prefix: {:?}, command: {:?}", 603 + trimmed_word, cmd_name_clone 604 + ))); 605 + } else { 606 + context = Some(CompletionContext::Argument { 607 + prefix: last_word.to_string(), 608 + span: Span::new(last_word_start, byte_pos), 609 + }); 610 + web_sys::console::log_1(&JsValue::from_str(&format!( 611 + "[completion] Set Argument context with prefix: {:?}", 612 + last_word 613 + ))); 614 + } 615 + } else { 616 + // Try to find command and argument index 617 + let mut found_cmd = None; 618 + let mut arg_count = 0; 619 + for (span, shape) in shapes.iter().rev() { 620 + let local_span = to_local_span(*span); 621 + if local_span.end <= byte_pos { 622 + if is_command_shape(shape, local_span) { 623 + let cmd_text = safe_slice(local_span); 624 + let cmd_name = cmd_text 625 + .split_whitespace() 626 + .next() 627 + .unwrap_or(&cmd_text) 628 + .trim(); 629 + found_cmd = Some(cmd_name.to_string()); 630 + break; 631 + } else { 632 + let arg_text = safe_slice(local_span); 633 + let trimmed_arg = arg_text.trim(); 634 + if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') { 635 + arg_count += 1; 636 + } 637 + } 638 + } 639 + } 640 + if let Some(cmd_name) = found_cmd { 641 + let cmd_name_clone = cmd_name.clone(); 642 + context = Some(CompletionContext::CommandArgument { 643 + prefix: trimmed_word.to_string(), 644 + span: Span::new(last_word_start, byte_pos), 645 + command_name: cmd_name, 646 + arg_index: arg_count, 647 + }); 648 + web_sys::console::log_1(&JsValue::from_str(&format!( 649 + "[completion] Set CommandArgument context with prefix: {:?}, command: {:?}, arg_index: {}", 650 + trimmed_word, cmd_name_clone, arg_count 651 + ))); 652 + } else { 653 + context = Some(CompletionContext::Argument { 654 + prefix: last_word.to_string(), 655 + span: Span::new(last_word_start, byte_pos), 656 + }); 657 + web_sys::console::log_1(&JsValue::from_str(&format!( 658 + "[completion] Set Argument context with prefix: {:?}", 659 + last_word 660 + ))); 661 + } 662 + } 449 663 } 450 664 } 451 665 } ··· 459 673 let char_start = input[..span.start].chars().count(); 460 674 let char_end = input[..span.end].chars().count(); 461 675 Span::new(char_start, char_end) 676 + }; 677 + 678 + let get_command_signature = |cmd_name: &str| -> Option<nu_protocol::Signature> { 679 + engine_guard 680 + .find_decl(cmd_name.as_bytes(), &[]) 681 + .map(|id| engine_guard.get_decl(id).signature()) 462 682 }; 463 683 464 684 match context { ··· 545 765 "[completion] Found {} file suggestions", 546 766 file_count 547 767 ))); 768 + } 769 + Some(CompletionContext::Flag { 770 + prefix, 771 + span, 772 + command_name, 773 + }) => { 774 + web_sys::console::log_1(&JsValue::from_str(&format!( 775 + "[completion] Generating Flag suggestions for command: {:?}, prefix: {:?}", 776 + command_name, prefix 777 + ))); 778 + 779 + if let Some(signature) = get_command_signature(&command_name) { 780 + let span = to_char_span(span); 781 + let mut flag_count = 0; 782 + 783 + // Get switches from signature 784 + // Signature has a named field that contains named arguments (including switches) 785 + for flag in &signature.named { 786 + // Check if this is a switch (has no argument) 787 + // Switches have arg: None, named arguments have arg: Some(SyntaxShape) 788 + let is_switch = flag.arg.is_none(); 789 + 790 + if is_switch { 791 + let long_name = format!("--{}", flag.long); 792 + let short_name = flag.short.map(|c| format!("-{}", c)); 793 + 794 + // Check if prefix matches long or short form 795 + let matches_long = long_name.starts_with(&prefix) || prefix.is_empty(); 796 + let matches_short = short_name 797 + .as_ref() 798 + .map(|s| s.starts_with(&prefix) || prefix.is_empty()) 799 + .unwrap_or(false); 800 + 801 + if matches_long { 802 + suggestions.push(Suggestion { 803 + name: long_name.clone(), 804 + description: Some(flag.desc.clone()), 805 + is_command: false, 806 + rendered: { 807 + let flag_colored = 808 + ansi_term::Color::Cyan.bold().paint(&long_name); 809 + format!("{flag_colored} {}", flag.desc) 810 + }, 811 + span_start: span.start, 812 + span_end: span.end, 813 + }); 814 + flag_count += 1; 815 + } 816 + 817 + if matches_short { 818 + if let Some(short) = short_name { 819 + suggestions.push(Suggestion { 820 + name: short.clone(), 821 + description: Some(flag.desc.clone()), 822 + is_command: false, 823 + rendered: { 824 + let flag_colored = 825 + ansi_term::Color::Cyan.bold().paint(&short); 826 + format!("{flag_colored} {}", flag.desc) 827 + }, 828 + span_start: span.start, 829 + span_end: span.end, 830 + }); 831 + flag_count += 1; 832 + } 833 + } 834 + } 835 + } 836 + 837 + web_sys::console::log_1(&JsValue::from_str(&format!( 838 + "[completion] Found {} flag suggestions", 839 + flag_count 840 + ))); 841 + } else { 842 + web_sys::console::log_1(&JsValue::from_str(&format!( 843 + "[completion] Could not find signature for command: {:?}", 844 + command_name 845 + ))); 846 + } 847 + } 848 + Some(CompletionContext::CommandArgument { 849 + prefix, 850 + span, 851 + command_name, 852 + arg_index, 853 + }) => { 854 + web_sys::console::log_1(&JsValue::from_str(&format!( 855 + "[completion] Generating CommandArgument suggestions for command: {:?}, arg_index: {}, prefix: {:?}", 856 + command_name, arg_index, prefix 857 + ))); 858 + 859 + if let Some(signature) = get_command_signature(&command_name) { 860 + // Get positional arguments from signature 861 + // Combine required and optional positional arguments 862 + let mut all_positional = Vec::new(); 863 + all_positional.extend_from_slice(&signature.required_positional); 864 + all_positional.extend_from_slice(&signature.optional_positional); 865 + 866 + // Find the argument at the given index 867 + if let Some(arg) = all_positional.get(arg_index) { 868 + // Check the SyntaxShape to determine completion type 869 + match &arg.shape { 870 + nu_protocol::SyntaxShape::String | nu_protocol::SyntaxShape::Filepath => { 871 + // File/directory completion 872 + let (dir, file_prefix) = prefix 873 + .rfind('/') 874 + .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 875 + .unwrap_or(("", prefix.as_str())); 876 + 877 + let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 878 + .then(|| &dir[..dir.len() - 1]) 879 + .unwrap_or(dir); 880 + 881 + let target_dir = if !dir.is_empty() { 882 + match root.join(dir_to_join) { 883 + Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 884 + _ => None, 885 + } 886 + } else { 887 + Some(root.join("").unwrap()) 888 + }; 889 + 890 + let span = to_char_span(span); 891 + let mut file_count = 0; 892 + if let Some(d) = target_dir { 893 + if let Ok(iterator) = d.read_dir() { 894 + for entry in iterator { 895 + let name = entry.filename(); 896 + if name.starts_with(file_prefix) { 897 + let full_completion = format!("{}{}", dir, name); 898 + suggestions.push(Suggestion { 899 + name: full_completion.clone(), 900 + description: Some(arg.desc.clone()), 901 + is_command: false, 902 + rendered: full_completion, 903 + span_start: span.start, 904 + span_end: span.end, 905 + }); 906 + file_count += 1; 907 + } 908 + } 909 + } 910 + } 911 + web_sys::console::log_1(&JsValue::from_str(&format!( 912 + "[completion] Found {} file suggestions for argument {}", 913 + file_count, arg_index 914 + ))); 915 + } 916 + _ => { 917 + // For other types, fall back to file completion 918 + let (dir, file_prefix) = prefix 919 + .rfind('/') 920 + .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 921 + .unwrap_or(("", prefix.as_str())); 922 + 923 + let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 924 + .then(|| &dir[..dir.len() - 1]) 925 + .unwrap_or(dir); 926 + 927 + let target_dir = if !dir.is_empty() { 928 + match root.join(dir_to_join) { 929 + Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 930 + _ => None, 931 + } 932 + } else { 933 + Some(root.join("").unwrap()) 934 + }; 935 + 936 + let span = to_char_span(span); 937 + if let Some(d) = target_dir { 938 + if let Ok(iterator) = d.read_dir() { 939 + for entry in iterator { 940 + let name = entry.filename(); 941 + if name.starts_with(file_prefix) { 942 + let full_completion = format!("{}{}", dir, name); 943 + suggestions.push(Suggestion { 944 + name: full_completion.clone(), 945 + description: Some(arg.desc.clone()), 946 + is_command: false, 947 + rendered: full_completion, 948 + span_start: span.start, 949 + span_end: span.end, 950 + }); 951 + } 952 + } 953 + } 954 + } 955 + } 956 + } 957 + } else { 958 + // Argument index out of range, fall back to file completion 959 + web_sys::console::log_1(&JsValue::from_str(&format!( 960 + "[completion] Argument index {} out of range, using file completion", 961 + arg_index 962 + ))); 963 + // Use the same file completion logic as Argument context 964 + let (dir, file_prefix) = prefix 965 + .rfind('/') 966 + .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 967 + .unwrap_or(("", prefix.as_str())); 968 + 969 + let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 970 + .then(|| &dir[..dir.len() - 1]) 971 + .unwrap_or(dir); 972 + 973 + let target_dir = if !dir.is_empty() { 974 + match root.join(dir_to_join) { 975 + Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 976 + _ => None, 977 + } 978 + } else { 979 + Some(root.join("").unwrap()) 980 + }; 981 + 982 + let span = to_char_span(span); 983 + if let Some(d) = target_dir { 984 + if let Ok(iterator) = d.read_dir() { 985 + for entry in iterator { 986 + let name = entry.filename(); 987 + if name.starts_with(file_prefix) { 988 + let full_completion = format!("{}{}", dir, name); 989 + suggestions.push(Suggestion { 990 + name: full_completion.clone(), 991 + description: None, 992 + is_command: false, 993 + rendered: full_completion, 994 + span_start: span.start, 995 + span_end: span.end, 996 + }); 997 + } 998 + } 999 + } 1000 + } 1001 + } 1002 + } else { 1003 + // No signature found, fall back to file completion 1004 + web_sys::console::log_1(&JsValue::from_str(&format!( 1005 + "[completion] Could not find signature for command: {:?}, using file completion", 1006 + command_name 1007 + ))); 1008 + let (dir, file_prefix) = prefix 1009 + .rfind('/') 1010 + .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 1011 + .unwrap_or(("", prefix.as_str())); 1012 + 1013 + let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 1014 + .then(|| &dir[..dir.len() - 1]) 1015 + .unwrap_or(dir); 1016 + 1017 + let target_dir = if !dir.is_empty() { 1018 + match root.join(dir_to_join) { 1019 + Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 1020 + _ => None, 1021 + } 1022 + } else { 1023 + Some(root.join("").unwrap()) 1024 + }; 1025 + 1026 + let span = to_char_span(span); 1027 + if let Some(d) = target_dir { 1028 + if let Ok(iterator) = d.read_dir() { 1029 + for entry in iterator { 1030 + let name = entry.filename(); 1031 + if name.starts_with(file_prefix) { 1032 + let full_completion = format!("{}{}", dir, name); 1033 + suggestions.push(Suggestion { 1034 + name: full_completion.clone(), 1035 + description: None, 1036 + is_command: false, 1037 + rendered: full_completion, 1038 + span_start: span.start, 1039 + span_end: span.end, 1040 + }); 1041 + } 1042 + } 1043 + } 1044 + } 1045 + } 548 1046 } 549 1047 _ => { 550 1048 web_sys::console::log_1(&JsValue::from_str(